/**
 * NativeModuleHealer — in-line self-heal for better-sqlite3 NODE_MODULE_VERSION mismatch.
 *
 * Background:
 *   ServerSupervisor.preflightSelfHeal handles the supervisor-spawn path
 *   (it rebuilds better-sqlite3 in `shadow-install` before forking the server).
 *   But CLI commands (`instar memory ...`, `instar semantic ...`) and any direct
 *   instantiation of SemanticMemory / TopicMemory / MemoryIndex bypass it.
 *
 *   When Node is upgraded after Instar was installed, the native better-sqlite3
 *   binding throws NODE_MODULE_VERSION on construction. Without an in-line heal,
 *   the only fix is for the user to run `npm rebuild better-sqlite3` manually —
 *   and 1254 reports in the field show users hit this and file bug reports
 *   instead (cluster-degradation-semanticmemory-semanticmemory-init-failed-the-m).
 *
 * Strategy:
 *   1. Wrap the better-sqlite3 constructor call in `openWithHeal`.
 *   2. On NODE_MODULE_VERSION error, locate npm + the install prefix that
 *      contains the better-sqlite3 package, run `npm rebuild better-sqlite3
 *      --prefix <install_prefix>` synchronously.
 *   3. Clear better-sqlite3 from `require.cache` so a fresh native binding
 *      is loaded on retry.
 *   4. Retry the constructor once. If it still fails, throw.
 *   5. Log heal events to `<stateDir>/native-module-heals.jsonl` for
 *      observability (consumed by health checks and DegradationReporter).
 *
 * Once-per-process guard:
 *   The rebuild is expensive (~30s) and shouldn't run more than once per
 *   process. After one attempt, subsequent failures throw immediately.
 *
 * Spec: PROP-399 (chronic, 24 cycles, 1254 field reports).
 */
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import crypto from 'node:crypto';
import child_process, { spawnSync, } from 'node:child_process';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const HEAL_LOG_FILENAME = 'native-module-heals.jsonl';
/**
 * NativeModuleHealer is a process-singleton. Stateful members track
 * whether a heal has already been attempted this process so the
 * expensive rebuild doesn't run on every open() retry.
 */
class NativeModuleHealerImpl {
    healAttempted = false;
    lastResult = null;
    stateDir = null;
    listeners = new Set();
    /** Configure where heal events are persisted. Optional. */
    configure(opts) {
        if (opts.stateDir)
            this.stateDir = opts.stateDir;
    }
    /**
     * Subscribe to HealEvents. Listener fires after every persisted event
     * (both success and failure — the bridge filters on success internally
     * so a future health-info consumer can also subscribe without
     * duplicating the dispatch). Returns an unsubscribe function.
     */
    onHealEvent(listener) {
        this.listeners.add(listener);
        return () => {
            this.listeners.delete(listener);
        };
    }
    /** Reset for testing. */
    resetForTesting() {
        this.healAttempted = false;
        this.lastResult = null;
        this.stateDir = null;
        this.listeners.clear();
    }
    /**
     * §A3 verify the HMAC on a RemediatorInvocationContext. Mirrors the
     * canonical body layout in `src/remediation/RemediationContext.ts`. We
     * inline rather than import to avoid the `src/memory/*` → `src/remediation/*`
     * dependency that would break the legacy `openWithHeal` path on installs
     * without the remediation tree (e.g., partial CLI surfaces).
     */
    verifyContextHmac(ctx, keyVault) {
        if (!ctx.hmac || !Buffer.isBuffer(ctx.hmac) || ctx.hmac.length === 0) {
            return false;
        }
        if (!ctx.runbookId)
            return false;
        let leaf;
        try {
            leaf = keyVault.deriveLeafKey('capability', ctx.runbookId);
        }
        catch {
            // @silent-fallback-ok — §A3 verification is fail-closed by design.
            // KeyVault derivation failure means we cannot verify the ctx; the
            // surface MUST refuse to act on Remediator-claimed authority. The
            // caller routes this to the in-line legacy heal path with a warning.
            return false;
        }
        const HMAC_TAG = Buffer.from('instar-f8-ctx-v1\x00', 'utf-8');
        const writeStr = (s) => {
            const body = Buffer.from(s, 'utf-8');
            const len = Buffer.alloc(4);
            len.writeUInt32BE(body.length, 0);
            return Buffer.concat([len, body]);
        };
        const expiresAtBuf = Buffer.alloc(8);
        expiresAtBuf.writeBigUInt64BE(BigInt(Math.max(0, Math.floor(ctx.expiresAt ?? 0))), 0);
        const monoBuf = Buffer.alloc(8);
        const mono = ctx.monotonicDeadline >= 0n ? ctx.monotonicDeadline : 0n;
        monoBuf.writeBigUInt64BE(mono, 0);
        const body = Buffer.concat([
            HMAC_TAG,
            writeStr(ctx.attemptId),
            writeStr(ctx.runbookId),
            expiresAtBuf,
            monoBuf,
        ]);
        const expected = crypto.createHmac('sha256', leaf).update(body).digest();
        if (expected.length !== ctx.hmac.length)
            return false;
        try {
            return crypto.timingSafeEqual(expected, ctx.hmac);
        }
        catch {
            // @silent-fallback-ok — timingSafeEqual throws only on length mismatch
            // or non-Buffer inputs. We already length-checked above; this catch is
            // defensive and fails-closed per §A3.
            return false;
        }
    }
    /**
     * Fall-back path when the surface-side HMAC verification rejects a ctx.
     * Runs the legacy in-line `openWithHeal` heal step (no opener — we just
     * want the rebuild + retry behavior). Returns an `ExecutionResult`-shaped
     * object so the caller's contract is preserved.
     */
    async fallbackToInlineHeal(ctx) {
        if (ctx.abortSignal.aborted) {
            return {
                outcome: 'failure',
                details: {
                    reason: 'aborted-before-fallback',
                    attemptId: ctx.attemptId,
                    invalidContext: true,
                },
            };
        }
        const succeeded = await this.healBetterSqlite3(`InvalidCtx:${ctx.runbookId}`);
        if (ctx.abortSignal.aborted) {
            return {
                outcome: 'failure',
                details: {
                    reason: 'aborted-during-fallback',
                    attemptId: ctx.attemptId,
                    invalidContext: true,
                },
            };
        }
        return {
            outcome: succeeded ? 'success' : 'failure',
            details: {
                attemptId: ctx.attemptId,
                invalidContext: true,
                fallbackPath: 'in-line-openWithHeal-heal-step',
                previousOutcome: this.lastResult,
            },
        };
    }
    /** Detect NODE_MODULE_VERSION errors. Tolerant of message wording variants. */
    isNodeModuleVersionError(err) {
        const msg = err instanceof Error ? (err.message ?? '') : String(err);
        // Common forms:
        //   "The module '...' was compiled against a different Node.js version using
        //    NODE_MODULE_VERSION 108. This version of Node.js requires NODE_MODULE_VERSION 115."
        //   "NODE_MODULE_VERSION mismatch"
        return /NODE_MODULE_VERSION/i.test(msg);
    }
    /** Returns the last heal attempt result (or null if none). */
    getLastResult() {
        return this.lastResult;
    }
    /**
     * Wrap an open() / new Database(path) call. If it throws with
     * NODE_MODULE_VERSION, run the heal once, then retry. Otherwise rethrow.
     *
     * The opener is passed in (rather than calling `new Database` directly)
     * because each caller has different construction logic — TopicMemory does
     * integrity checks, SemanticMemory does pragma setup, etc.
     *
     * The opener may be sync or async; both return paths are awaited.
     */
    async openWithHeal(component, opener) {
        try {
            return await opener();
        }
        catch (err) {
            if (!this.isNodeModuleVersionError(err))
                throw err;
            // The expensive rebuild runs at most once per process. But "rebuild
            // already ran" does NOT mean "this caller can't recover" — it depends on
            // whether that rebuild SUCCEEDED.
            if (this.healAttempted) {
                const last = this.lastResult;
                // Multi-subsystem case: the FIRST sqlite subsystem to open (e.g.
                // SemanticMemory) consumes the once-per-process rebuild. When that
                // rebuild succeeded, the on-disk binding is already correct — a LATER
                // subsystem (e.g. TokenLedger) that is still holding the stale cached
                // binding just needs a fresh require. Clear the cache and retry the
                // open once (cheap; no second rebuild). Without this, every sqlite
                // subsystem opened after the first one stays broken on a node-upgraded
                // agent — observed live as TokenLedger 503 while SemanticMemory 200.
                if (last?.success) {
                    this.clearBetterSqlite3Cache();
                    return await opener();
                }
                // The prior rebuild FAILED — retrying the open won't help; surface the
                // original error with heal context.
                const hint = ` (heal previously attempted and failed: ${last?.errorTail ?? 'unknown'})`;
                const wrapped = err instanceof Error ? err : new Error(String(err));
                wrapped.message = `${wrapped.message}${hint}`;
                throw wrapped;
            }
            const healed = await this.healBetterSqlite3(component);
            if (!healed) {
                // Don't swallow the original error; rethrow with heal context
                const wrapped = err instanceof Error ? err : new Error(String(err));
                wrapped.message = `${wrapped.message} (in-line heal failed — see ${HEAL_LOG_FILENAME})`;
                throw wrapped;
            }
            // Clear cached better-sqlite3 require entries so the fresh native
            // binding is loaded on retry.
            this.clearBetterSqlite3Cache();
            // Retry once. If this still throws, surface the new error directly
            // so the caller sees the post-rebuild failure mode.
            return await opener();
        }
    }
    /**
     * Sync variant of openWithHeal. Use when the caller cannot be async
     * (e.g. a class constructor). The internal rebuild path is already
     * synchronous (spawnSync + fs.appendFileSync), so this is the same
     * safety as openWithHeal — it just removes the async surface for
     * sync-only call sites like TokenLedger's constructor.
     *
     * The opener must be sync. If you have an async opener, use
     * openWithHeal instead.
     */
    openWithHealSync(component, opener) {
        try {
            return opener();
        }
        catch (err) {
            if (!this.isNodeModuleVersionError(err))
                throw err;
            // The expensive rebuild runs at most once per process — but recovery for
            // THIS caller depends on whether that rebuild succeeded (see openWithHeal).
            if (this.healAttempted) {
                const last = this.lastResult;
                // A prior rebuild succeeded → binding on disk is already correct. Clear
                // this caller's stale cached binding and retry the open once. This is
                // the path TokenLedger's sync constructor takes when SemanticMemory
                // healed first.
                if (last?.success) {
                    this.clearBetterSqlite3Cache();
                    return opener();
                }
                const hint = ` (heal previously attempted and failed: ${last?.errorTail ?? 'unknown'})`;
                const wrapped = err instanceof Error ? err : new Error(String(err));
                wrapped.message = `${wrapped.message}${hint}`;
                throw wrapped;
            }
            const healed = this.healBetterSqlite3Sync(component);
            if (!healed) {
                const wrapped = err instanceof Error ? err : new Error(String(err));
                wrapped.message = `${wrapped.message} (in-line heal failed — see ${HEAL_LOG_FILENAME})`;
                throw wrapped;
            }
            this.clearBetterSqlite3Cache();
            return opener();
        }
    }
    /**
     * Run `npm rebuild better-sqlite3 --prefix <install_prefix>` synchronously.
     * Returns true if the rebuild succeeded, false otherwise. Always logs
     * a HealEvent. Async wrapper over healBetterSqlite3Sync so existing
     * `await` call sites are unchanged.
     */
    async healBetterSqlite3(component) {
        return this.healBetterSqlite3Sync(component);
    }
    /**
     * Synchronous implementation of the better-sqlite3 rebuild. The
     * internals (spawnSync, fs.appendFileSync) were already synchronous;
     * this just exposes that fact through a sync surface for callers
     * like openWithHealSync that cannot use `await`.
     */
    healBetterSqlite3Sync(component) {
        if (this.healAttempted)
            return false;
        this.healAttempted = true;
        const started = Date.now();
        const event = {
            component,
            timestamp: new Date().toISOString(),
            success: false,
            nodeVersion: process.version,
        };
        const installPrefix = this.findBetterSqlite3InstallPrefix();
        if (!installPrefix) {
            event.errorTail = 'could not locate better-sqlite3 install prefix';
            console.error(`[${component}] NativeModuleHealer: ${event.errorTail}`);
            this.logHealEvent(event);
            this.lastResult = event;
            return false;
        }
        event.installPrefix = installPrefix;
        const npmPath = this.findNpmPath();
        if (!npmPath) {
            event.errorTail = 'npm not found on PATH';
            console.error(`[${component}] NativeModuleHealer: ${event.errorTail}`);
            this.logHealEvent(event);
            this.lastResult = event;
            return false;
        }
        event.npmPath = npmPath;
        console.log(`[${component}] NativeModuleHealer: rebuilding better-sqlite3 for Node ${process.version} (prefix=${installPrefix}). This may take ~30s.`);
        // FLEET FIX (wrong-ABI rebuild on a PATH-shadowed Node + compile-only heal,
        // 2026-05-29 — mirrors the supervisor preflight; instar-codey sqlite offline
        // 16h). Two changes:
        //  (1) Pin the toolchain to THIS process's Node dir so node-gyp /
        //      prebuild-install / any `#!/usr/bin/env node` shebang resolve the
        //      correct ABI even when another Node (e.g. an asdf 22.x) is first on
        //      PATH — otherwise the rebuild "succeeds" but targets the wrong ABI.
        //  (2) Prefer the PREBUILT via `npm install` (runs better-sqlite3's install
        //      script → prebuild-install → fetches the correct-ABI prebuilt, ~2s, NO
        //      compiler) and only compile from source as a fallback. `npm rebuild`
        //      ALWAYS node-gyp-compiles and never fetches a prebuilt, so it cannot
        //      heal a box without a working C++ toolchain.
        const nodeDir = path.dirname(process.execPath);
        const rebuildEnv = {
            ...process.env,
            npm_config_node_gyp: undefined,
            npm_node_execpath: process.execPath,
            PATH: `${nodeDir}${path.delimiter}${process.env.PATH ?? ''}`,
        };
        let pkgVersion = '';
        try {
            pkgVersion = JSON.parse(fs.readFileSync(path.join(installPrefix, 'node_modules', 'better-sqlite3', 'package.json'), 'utf-8')).version || '';
        }
        catch { /* version optional */ }
        const installSpec = pkgVersion ? `better-sqlite3@${pkgVersion}` : 'better-sqlite3';
        // `--ignore-scripts` on the from-source fallback keeps it scoped (no arbitrary
        // postinstalls). The prebuilt attempt MUST run scripts (that is how
        // prebuild-install fetches the binary), so it is a plain `npm install`.
        const attempts = [
            [npmPath, 'install', installSpec, '--no-save', '--prefix', installPrefix],
            [npmPath, 'rebuild', '--build-from-source', '--ignore-scripts', 'better-sqlite3', '--prefix', installPrefix],
        ];
        let result = null;
        for (const args of attempts) {
            try {
                // Namespace access so tests can monkey-patch child_process.spawnSync.
                result = child_process.spawnSync(process.execPath, args, {
                    encoding: 'utf-8',
                    timeout: 120_000,
                    cwd: installPrefix,
                    env: rebuildEnv,
                });
            }
            catch (spawnErr) {
                event.errorTail = `spawn failed: ${spawnErr instanceof Error ? spawnErr.message : String(spawnErr)}`;
                continue;
            }
            if (result && result.status === 0)
                break;
        }
        event.durationMs = Date.now() - started;
        if (result && result.status === 0) {
            event.success = true;
            console.log(`[${component}] NativeModuleHealer: rebuild succeeded in ${event.durationMs}ms`);
            this.logHealEvent(event);
            this.lastResult = event;
            return true;
        }
        const stderrTail = (result?.stderr || result?.stdout || '').slice(-300);
        event.errorTail = stderrTail || event.errorTail || `npm exited ${result?.status ?? 'n/a'}`;
        console.error(`[${component}] NativeModuleHealer: rebuild failed (status=${result?.status ?? 'n/a'}): ${event.errorTail}`);
        this.logHealEvent(event);
        this.lastResult = event;
        return false;
    }
    /**
     * Resolve the npm-installable prefix for better-sqlite3. We want the
     * directory whose `node_modules/better-sqlite3` is the one that just
     * failed to load — that's the install prefix npm needs.
     *
     * Strategy: use require.resolve('better-sqlite3'), then walk up to find
     * the parent of the `node_modules/better-sqlite3` segment.
     */
    findBetterSqlite3InstallPrefix() {
        let resolved;
        try {
            resolved = require.resolve('better-sqlite3');
        }
        catch {
            // Module not installed at all — heal can't help.
            return null;
        }
        // resolved points at a JS file inside node_modules/better-sqlite3/...
        // Walk up to find ".../node_modules/better-sqlite3", then the install
        // prefix is the parent of node_modules.
        const segments = resolved.split(path.sep);
        for (let i = segments.length - 1; i > 0; i--) {
            if (segments[i] === 'better-sqlite3' && segments[i - 1] === 'node_modules') {
                // Install prefix = path up to (but not including) "node_modules"
                return segments.slice(0, i - 1).join(path.sep) || path.sep;
            }
        }
        return null;
    }
    /**
     * Find npm on disk. Mirrors ServerSupervisor.findNpmPath logic.
     */
    findNpmPath() {
        // Try the node sibling first (most reliable — matches the Node version)
        const currentNodeDir = path.dirname(process.execPath);
        const siblingNpm = path.join(currentNodeDir, 'npm');
        if (fs.existsSync(siblingNpm))
            return siblingNpm;
        // Platform-common locations
        const candidates = os.platform() === 'win32'
            ? ['C:\\Program Files\\nodejs\\npm.cmd', 'C:\\Program Files (x86)\\nodejs\\npm.cmd']
            : ['/opt/homebrew/bin/npm', '/usr/local/bin/npm', '/usr/bin/npm'];
        for (const candidate of candidates) {
            if (fs.existsSync(candidate))
                return candidate;
        }
        // PATH lookup as a last resort
        try {
            const which = spawnSync(os.platform() === 'win32' ? 'where' : 'which', ['npm'], {
                encoding: 'utf-8',
                timeout: 5000,
            });
            if (which.status === 0 && which.stdout.trim()) {
                return which.stdout.trim().split(/\r?\n/)[0];
            }
        }
        catch {
            /* ignore */
        }
        return null;
    }
    /**
     * Clear better-sqlite3 entries from Node's require cache so the fresh
     * native binding is loaded on retry.
     */
    clearBetterSqlite3Cache() {
        try {
            for (const key of Object.keys(require.cache)) {
                if (key.includes(`${path.sep}better-sqlite3${path.sep}`)) {
                    delete require.cache[key];
                }
            }
        }
        catch {
            /* ignore — best-effort */
        }
    }
    /**
     * Remediator entry point — F-8 / W-1 (SELF-HEALING-REMEDIATOR-V2-SPEC
     * §A28, §A45, §A55). Wraps the same rebuild logic as `openWithHeal`'s
     * heal step but accepts a `RemediatorInvocationContext` so:
     *   - the abort signal is honoured (deadline / parent cancel),
     *   - the monotonic deadline gates `npm rebuild` timeout,
     *   - the rebuilt `.node` binary's sha256 is recorded in `lastResult` so
     *     the Remediator's audit-projection can detect cross-process binary
     *     divergence (A28),
     *   - prebuilt-first heal (parity with the boot heal paths): attempt 1 is
     *     `npm install better-sqlite3@<pinned> --no-save` so prebuild-install
     *     fetches the correct-ABI prebuilt (no compiler needed — the only path
     *     that heals a node-ABI bump on a box without a C++ toolchain); attempt 2
     *     is the from-source `--ignore-scripts --build-from-source` fallback.
     *     NOTE — this deliberately RELAXES the original §A45 from-source-only
     *     stance: the scoped prebuilt fetch (single pinned package, official
     *     better-sqlite3 release — the same source a normal `npm install` uses)
     *     is accepted in exchange for healability, matching what the boot heal
     *     paths already do fleet-wide. The from-source fallback retains
     *     `--ignore-scripts` (no arbitrary postinstalls).
     *
     * Returns an `ExecutionResult`-shaped object compatible with F-8's
     * `ApprovedRunbook.surfaceCallable` contract. Errors are caught and
     * mapped to `{outcome: 'failure', details: {...}}` — the Remediator's
     * verify step (A21) decides whether the heal actually worked; this
     * method ONLY reports whether the rebuild succeeded.
     *
     * The legacy in-line `openWithHeal` entry point remains the canonical
     * safety net for direct-construction CLI paths. This method is the
     * Remediator-orchestrated parallel path; both acquire the same
     * MachineLock tuple (§A2 lock-bound co-existence), so only one rebuild
     * runs at a time on a given machine.
     */
    async invokeFromRemediator(ctx, keyVault) {
        // §A3 / §A23 — surface-side capability-token verification. When a
        // keyVault is wired AND the ctx claims an HMAC, verify it. Invalid →
        // fall back to the in-line legacy path + emit a warning so the audit
        // tail records the rejection.
        if (keyVault && ctx.hmac !== undefined) {
            const ok = this.verifyContextHmac(ctx, keyVault);
            if (!ok) {
                console.warn(`[NativeModuleHealer] remediation.surface.invalid-context ` +
                    `runbookId=${ctx.runbookId} attemptId=${ctx.attemptId} — ` +
                    `falling back to in-line openWithHeal path`);
                return this.fallbackToInlineHeal(ctx);
            }
        }
        if (ctx.abortSignal.aborted) {
            return {
                outcome: 'failure',
                details: {
                    reason: 'aborted-before-start',
                    attemptId: ctx.attemptId,
                },
            };
        }
        if (this.healAttempted) {
            const last = this.lastResult;
            return {
                outcome: last?.success ? 'success' : 'failure',
                details: {
                    reason: 'heal-already-attempted-this-process',
                    previousOutcome: last,
                    attemptId: ctx.attemptId,
                },
            };
        }
        // Compute remaining ns budget from monotonic deadline. Cap below 120s.
        const nowHr = process.hrtime.bigint();
        let remainingMs = 120_000;
        if (ctx.monotonicDeadline > nowHr) {
            const remainingNs = ctx.monotonicDeadline - nowHr;
            const computed = Number(remainingNs / 1000000n);
            if (computed < remainingMs)
                remainingMs = computed;
        }
        else {
            return {
                outcome: 'failure',
                details: {
                    reason: 'deadline-already-elapsed',
                    attemptId: ctx.attemptId,
                },
            };
        }
        if (remainingMs < 1_000) {
            return {
                outcome: 'failure',
                details: {
                    reason: 'insufficient-deadline-budget',
                    remainingMs,
                    attemptId: ctx.attemptId,
                },
            };
        }
        const result = await this.healBetterSqlite3FromRemediator(ctx, remainingMs);
        return result;
    }
    /**
     * Remediator-side rebuild path. Mirrors `healBetterSqlite3`: prebuilt-first
     * (`npm install better-sqlite3@<pinned>` → prebuild-install fetches the
     * correct-ABI prebuilt, no compiler) with a scoped `--ignore-scripts
     * --build-from-source` fallback (A28 + A45), toolchain PATH-pinned to this
     * process's Node, and records the sha256 of the rebuilt `.node` binary in the
     * heal log for cross-process binary-divergence detection (A28).
     *
     * Returns `{outcome: 'success' | 'failure', details: {...}}` directly so
     * the caller (W-1 runbook surfaceCallable) doesn't need a second mapping
     * step. The HealEvent is still appended to the in-line log for
     * observability parity with the legacy path.
     */
    async healBetterSqlite3FromRemediator(ctx, timeoutMs) {
        this.healAttempted = true;
        const component = `Remediator:${ctx.runbookId}`;
        const started = Date.now();
        const event = {
            component,
            timestamp: new Date().toISOString(),
            success: false,
            nodeVersion: process.version,
        };
        const installPrefix = this.findBetterSqlite3InstallPrefix();
        if (!installPrefix) {
            event.errorTail = 'could not locate better-sqlite3 install prefix';
            this.logHealEvent(event);
            this.lastResult = event;
            return {
                outcome: 'failure',
                details: { reason: event.errorTail, attemptId: ctx.attemptId },
            };
        }
        event.installPrefix = installPrefix;
        const npmPath = this.findNpmPath();
        if (!npmPath) {
            event.errorTail = 'npm not found on PATH';
            this.logHealEvent(event);
            this.lastResult = event;
            return {
                outcome: 'failure',
                details: { reason: event.errorTail, attemptId: ctx.attemptId },
            };
        }
        event.npmPath = npmPath;
        // A45 pre-rebuild integrity check: read package-lock.json's `resolved`
        // URL + integrity hash for better-sqlite3. The check is best-effort —
        // surface a non-fatal mismatch in the event but do NOT block the
        // rebuild (the in-line healer must keep working; the gold-standard
        // sha256 lockfile from A55 lands in a follow-up PR).
        const integrity = this.readPackageLockIntegrity(installPrefix);
        // FLEET FIX (2026-05-31 — instar-codey node-25 ABI deadlock): prebuilt-first,
        // mirroring `healBetterSqlite3` and the ServerSupervisor preflight (both made
        // prebuilt-first by #539). The remediator path was the last rebuild surface
        // still `npm rebuild --build-from-source`-ONLY, which ALWAYS node-gyp-compiles
        // and can't heal a box without a working C++ toolchain — exactly where the
        // node-25/ABI-141-vs-127 boxes got stuck.
        //  (1) Pin the toolchain to THIS process's Node dir so node-gyp /
        //      prebuild-install / any `#!/usr/bin/env node` shebang resolve the
        //      correct ABI even when another Node (e.g. an asdf 22.x) is first on PATH.
        //  (2) Try the PREBUILT first via `npm install better-sqlite3@<pinned>` (runs
        //      the install script → prebuild-install fetches the correct-ABI prebuilt,
        //      ~2s, NO compiler), then a from-source `--build-from-source` fallback.
        // A45 supply-chain: the prebuilt attempt MUST run scripts (that is how
        // prebuild-install fetches the binary) but is scoped to the single pinned
        // package with `--no-save`; the from-source fallback keeps `--ignore-scripts`
        // (no arbitrary postinstalls) — identical to the two sibling paths.
        const nodeDir = path.dirname(process.execPath);
        const rebuildEnv = {
            ...process.env,
            npm_config_node_gyp: undefined,
            npm_node_execpath: process.execPath,
            PATH: `${nodeDir}${path.delimiter}${process.env.PATH ?? ''}`,
        };
        let pkgVersion = '';
        try {
            pkgVersion = JSON.parse(fs.readFileSync(path.join(installPrefix, 'node_modules', 'better-sqlite3', 'package.json'), 'utf-8')).version || '';
        }
        catch { /* version optional */ }
        const installSpec = pkgVersion ? `better-sqlite3@${pkgVersion}` : 'better-sqlite3';
        const attempts = [
            [npmPath, 'install', installSpec, '--no-save', '--prefix', installPrefix],
            [npmPath, 'rebuild', '--build-from-source', '--ignore-scripts', 'better-sqlite3', '--prefix', installPrefix],
        ];
        let result = null;
        let lastSpawnErr = '';
        for (const args of attempts) {
            if (ctx.abortSignal.aborted)
                break;
            try {
                // Use namespace access so tests can monkey-patch `child_process.spawnSync`.
                result = child_process.spawnSync(process.execPath, args, {
                    encoding: 'utf-8',
                    timeout: timeoutMs,
                    cwd: installPrefix,
                    env: rebuildEnv,
                    signal: ctx.abortSignal,
                });
            }
            catch (spawnErr) {
                lastSpawnErr = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
                continue;
            }
            // Stop at the first attempt that npm reports as successful; the
            // post-rebuild load/sha256 checks below confirm it is actually loadable.
            if (result && result.status === 0)
                break;
        }
        if (!result) {
            event.errorTail = `spawn failed: ${lastSpawnErr || 'no rebuild attempt produced a result'}`;
            event.durationMs = Date.now() - started;
            this.logHealEvent(event);
            this.lastResult = event;
            return {
                outcome: 'failure',
                details: {
                    reason: event.errorTail,
                    attemptId: ctx.attemptId,
                    aborted: ctx.abortSignal.aborted,
                },
            };
        }
        event.durationMs = Date.now() - started;
        if (ctx.abortSignal.aborted) {
            event.errorTail = `aborted during rebuild (status=${result.status ?? 'null'})`;
            this.logHealEvent(event);
            this.lastResult = event;
            return {
                outcome: 'failure',
                details: { reason: 'aborted', attemptId: ctx.attemptId },
            };
        }
        if (result.status !== 0) {
            const stderrTail = (result.stderr || result.stdout || '').slice(-300);
            event.errorTail = stderrTail || `npm exited ${result.status}`;
            this.logHealEvent(event);
            this.lastResult = event;
            return {
                outcome: 'failure',
                details: {
                    reason: event.errorTail,
                    npmStatus: result.status,
                    attemptId: ctx.attemptId,
                },
            };
        }
        // Clear cached require entries so the next consumer sees the fresh
        // binding (parity with openWithHeal's behaviour).
        this.clearBetterSqlite3Cache();
        // A28 post-rebuild sha256 record of the rebuilt `.node` binary. Used by
        // the Remediator's audit-projection / cross-process ledger to detect
        // divergent binaries across attempts.
        const sha256 = this.computeBetterSqlite3BinarySha256(installPrefix);
        event.success = true;
        this.logHealEvent(event);
        this.lastResult = event;
        return {
            outcome: 'success',
            details: {
                attemptId: ctx.attemptId,
                installPrefix,
                npmPath,
                durationMs: event.durationMs,
                rebuiltBinarySha256: sha256,
                nodeVersion: process.version,
                packageLockIntegrity: integrity,
            },
        };
    }
    /**
     * Read `package-lock.json` and return better-sqlite3's `resolved` URL +
     * `integrity` hash, if locatable. Returns `null` on any failure — this is
     * a best-effort secondary check per A45, not authoritative.
     */
    readPackageLockIntegrity(installPrefix) {
        try {
            const lockPath = path.join(installPrefix, 'package-lock.json');
            if (!fs.existsSync(lockPath))
                return null;
            const raw = fs.readFileSync(lockPath, 'utf-8');
            const lock = JSON.parse(raw);
            // npm@7+ uses `packages` with full nested paths.
            if (lock.packages) {
                for (const [key, val] of Object.entries(lock.packages)) {
                    if (key === 'node_modules/better-sqlite3' ||
                        key.endsWith('/node_modules/better-sqlite3')) {
                        if (val.resolved && val.integrity) {
                            return { resolved: val.resolved, integrity: val.integrity };
                        }
                    }
                }
            }
            // Legacy npm@6 `dependencies` map.
            if (lock.dependencies && lock.dependencies['better-sqlite3']) {
                const dep = lock.dependencies['better-sqlite3'];
                if (dep.resolved && dep.integrity) {
                    return { resolved: dep.resolved, integrity: dep.integrity };
                }
            }
            return null;
        }
        catch {
            return null;
        }
    }
    /**
     * Compute sha256 of the rebuilt better-sqlite3 `.node` binary. Used for
     * A28 cross-process binary-divergence detection. Returns null on any
     * failure — the heal still succeeded; the divergence detector simply
     * loses this data point.
     */
    computeBetterSqlite3BinarySha256(installPrefix) {
        try {
            const binaryPath = path.join(installPrefix, 'node_modules', 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node');
            if (!fs.existsSync(binaryPath))
                return null;
            const buf = fs.readFileSync(binaryPath);
            return crypto.createHash('sha256').update(buf).digest('hex');
        }
        catch {
            return null;
        }
    }
    /**
     * Persist a heal event for observability. Best-effort; never throws.
     * Logged to <stateDir>/native-module-heals.jsonl if stateDir is configured,
     * otherwise to <os.tmpdir>/instar-native-module-heals.jsonl as a fallback.
     */
    logHealEvent(event) {
        try {
            const dir = this.stateDir || path.join(os.tmpdir(), 'instar');
            try {
                fs.mkdirSync(dir, { recursive: true });
            }
            catch {
                /* ignore */
            }
            const logPath = path.join(dir, HEAL_LOG_FILENAME);
            fs.appendFileSync(logPath, JSON.stringify(event) + '\n');
        }
        catch {
            /* ignore — observability shouldn't break the heal */
        }
        for (const listener of this.listeners) {
            try {
                listener(event);
            }
            catch {
                /* ignore — listener failures must not break the heal path */
            }
        }
    }
}
export const NativeModuleHealer = new NativeModuleHealerImpl();
//# sourceMappingURL=NativeModuleHealer.js.map