/**
 * Pack discovery substrate (ADR-097 § 5 Q5 + § 10, mmnto-ai/totem#1768).
 *
 * Reads `.totem/installed-packs.json` synchronously at engine boot,
 * resolves each registered pack's registration callback module, and
 * invokes the callback with a `PackRegistrationAPI` so the pack can
 * register its ChunkStrategy + ast-grep Lang + WASM grammar entries
 * before the engine seals.
 *
 * Sealing happens at the end of `loadInstalledPacks()` after every pack
 * callback has returned. Once sealed, subsequent `register()` calls on
 * either registry throw — see `chunker-registry.ts:seal()` and
 * `ast-classifier.ts:sealLangRegistry()`.
 *
 * The seal is the only synchronization boundary between the registration
 * phase and the runtime phase. CLI commands invoke `loadInstalledPacks()`
 * immediately after config load and before any other engine surface, so
 * pack registration is always complete before any chunker / language
 * lookup happens.
 *
 * Failure-mode discipline (Tenet 4):
 * - Missing manifest: silent (treated as no packs); user runs `totem sync`
 *   to generate.
 * - Malformed manifest: hard error.
 * - Pack require throws: hard error.
 * - engines['@mmnto/totem'] version mismatch: structured error per ADR-097
 *   Q6 (amended 2026-05-03 — moved from peerDependencies to engines field
 *   per mmnto-ai/totem#1803 to avoid changesets `fixed` group sibling
 *   peer-dep collision; see #1776 / #1777).
 * - Pack callback throws: hard error.
 */
import * as fs from 'node:fs';
import { createRequire } from 'node:module';
import * as path from 'node:path';
import * as semver from 'semver';
import { z } from 'zod';
import { registerLang as registerLangInRegistry, sealLangRegistry, } from './ast-classifier.js';
import { isSealed as isChunkerRegistrySealed, register as registerChunkerInRegistry, seal as sealChunkerRegistry, } from './chunkers/chunker-registry.js';
// ─── Schema ─────────────────────────────────────────
/**
 * `.totem/installed-packs.json` substrate. Written by `totem sync`,
 * consumed by `loadInstalledPacks()` at boot.
 *
 * `version: 1` is the load-bearing sentinel for forward compatibility:
 * future schema changes bump the version, callers fail loud on unknown
 * versions rather than silently mis-parsing.
 */
export const InstalledPacksManifestSchema = z
    .object({
    version: z.literal(1),
    /**
     * `@mmnto/totem` package version that wrote the manifest. Optional
     * for forward-compat with pre-1.27.0 manifests. Stamped at write
     * time by `writeInstalledPacksManifest()` from `resolveEngineVersion()`.
     *
     * Read on the lint-time stale-manifest UX-nudge fast-path
     * (`rule-engine.ts` parser-error intercept). Schema does not
     * enforce semver-validity here so a malformed cohort doesn't make
     * the entire manifest unreadable; the consumer treats malformed
     * or missing values as "stale" and surfaces the same nudge
     * (mmnto-ai/totem#1811, ADR-101).
     */
    cohort: z.string().optional(),
    packs: z.array(z
        .object({
        /** Pack package name as it appears in npm (e.g., `@mmnto/pack-rust-architecture`). */
        name: z.string().min(1),
        /**
         * Absolute filesystem path to the pack's package root. Refined to
         * `path.isAbsolute()` so a relative entry can't pass schema
         * validation and then probe two different locations downstream
         * (`existsSync` resolves from cwd, `require.resolve` from this
         * module).
         */
        resolvedPath: z
            .string()
            .min(1)
            .refine((value) => path.isAbsolute(value), 'resolvedPath must be an absolute path'),
        /**
         * The pack's `engines['@mmnto/totem']` semver range, verbatim.
         * (Pre-1.26.0 packs used `peerDependencies['@mmnto/totem']`;
         * mmnto-ai/totem#1803 moved the constraint to the `engines`
         * field to free `peerDependencies` for actual peer packages
         * and avoid changesets fixed-group sibling collisions.)
         */
        declaredEngineRange: z.string().min(1),
    })
        .strict()),
})
    .strict()
    // Duplicate pack names would let two callbacks run while `PACK_REGISTRY`
    // silently keeps only the first entry — the second callback's chunker
    // or language registrations surface later as a registry collision
    // instead of a clear manifest-boundary error. Fail loud here.
    .superRefine((manifest, ctx) => {
    const seen = new Set();
    manifest.packs.forEach((pack, index) => {
        if (seen.has(pack.name)) {
            ctx.addIssue({
                code: z.ZodIssueCode.custom,
                path: ['packs', index, 'name'],
                message: `duplicate pack entry '${pack.name}'`,
            });
            return;
        }
        seen.add(pack.name);
    });
});
// ─── Internal state ─────────────────────────────────
const PACK_REGISTRY = new Map();
let engineSealed = false;
// ─── Core: loadInstalledPacks ───────────────────────
/**
 * Read `.totem/installed-packs.json` and run every registered pack's
 * registration callback synchronously. After all callbacks return, seal
 * both the chunker registry and the language registry.
 *
 * Idempotent: a second call after the first throws because the engine is
 * already sealed (callers must not "re-load" packs at runtime). For
 * tests, see `__resetForTests()`.
 */
export function loadInstalledPacks(options = {}) {
    if (engineSealed) {
        throw new Error('loadInstalledPacks() called after engine seal — engine has already started serving requests. Pack registration is a boot-time-only operation.');
    }
    const projectRoot = options.projectRoot ?? process.cwd();
    const totemDir = options.totemDir ?? '.totem';
    const engineVersion = options.engineVersion ?? resolveEngineVersion();
    const packsToRegister = options.inMemoryPacks ?? readManifestAndResolveCallbacks(projectRoot, totemDir);
    // Engine version cross-check (ADR-097 Q6) — runs against every pack
    // before any callback executes, so a single mismatch fails loud before
    // touching the registries.
    for (const { pack } of packsToRegister) {
        assertEngineRangeSatisfied(pack, engineVersion);
    }
    // Run every callback. A throwing pack short-circuits registration —
    // the engine is left unsealed so a subsequent retry (e.g., test reset)
    // can recover, but the in-flight registration entries from prior packs
    // remain. Callers running in production should treat any throw here
    // as a hard error and not attempt to recover.
    for (const { pack, callback } of packsToRegister) {
        const api = {
            registerChunkStrategy(name, chunkerCtor) {
                registerChunkerInRegistry(name, chunkerCtor);
            },
            registerLanguage(extension, lang, wasmLoader) {
                registerLangInRegistry(extension, lang, wasmLoader);
            },
        };
        let callbackResult;
        try {
            callbackResult = callback(api);
        }
        catch (err) {
            throw new Error(`Pack '${pack.name}' registration callback threw. The pack at '${pack.resolvedPath}' must be fixed or removed.`, { cause: err instanceof Error ? err : new Error(String(err)) });
        }
        // Per ADR-097 § 5 Q5 the registration callback contract is
        // synchronous; an `async register(api)` would resolve AFTER the
        // engine seals, leaving partial registry state. The
        // `PackRegisterCallback` type already says `void`, but TypeScript
        // can't enforce sync vs async at runtime — a JS-authored pack or
        // one that drifts from the type can still return a Promise. Detect
        // the thenable shape here and fail loud before the seal. Kept
        // outside the try-catch above so the contract-violation error
        // surfaces with its own message rather than getting wrapped as
        // "callback threw."
        if (callbackResult !== null &&
            // totem-context: null is excluded by the `!== null` guard on the line above; rule matcher only sees the `=== 'object'` token in isolation
            (typeof callbackResult === 'object' || typeof callbackResult === 'function') &&
            'then' in callbackResult &&
            typeof callbackResult.then === 'function') {
            throw new Error(`Pack '${pack.name}' registration callback returned a Promise — registration must be synchronous per ADR-097 § 5 Q5. The pack at '${pack.resolvedPath}' must export a synchronous \`register\` (not \`async register\`).`);
        }
        if (!PACK_REGISTRY.has(pack.name)) {
            PACK_REGISTRY.set(pack.name, pack);
        }
    }
    sealChunkerRegistry();
    sealLangRegistry();
    engineSealed = true;
    return [...PACK_REGISTRY.values()];
}
/**
 * Snapshot of currently-loaded packs. Empty until `loadInstalledPacks()`
 * runs; populated thereafter and stable for the engine lifetime.
 */
export function loadedPacks() {
    return [...PACK_REGISTRY.values()];
}
/** True iff the engine has sealed (registration phase complete). */
export function isEngineSealed() {
    return engineSealed;
}
// ─── Manifest read + callback resolution ────────────
function readManifestAndResolveCallbacks(projectRoot, totemDir) {
    const manifestPath = path.join(projectRoot, totemDir, 'installed-packs.json');
    let raw;
    try {
        raw = fs.readFileSync(manifestPath, 'utf-8');
    }
    catch (err) {
        const code = err.code;
        if (code === 'ENOENT') {
            // Pre-sync repo or fresh checkout. Treat as no packs — user can
            // run `totem sync` to populate.
            return [];
        }
        throw new Error(`Failed to read installed-packs manifest at '${manifestPath}'. Re-run \`totem sync\` to regenerate.`, { cause: err instanceof Error ? err : new Error(String(err)) });
    }
    let parsedJson;
    try {
        parsedJson = JSON.parse(raw);
    }
    catch (err) {
        throw new Error(`installed-packs manifest at '${manifestPath}' is not valid JSON. Re-run \`totem sync\` to regenerate.`, { cause: err instanceof Error ? err : new Error(String(err)) });
    }
    const result = InstalledPacksManifestSchema.safeParse(parsedJson);
    if (!result.success) {
        throw new Error(`installed-packs manifest at '${manifestPath}' failed schema validation: ${result.error.issues
            .map((issue) => `${issue.path.join('.')}: ${issue.message}`)
            .join('; ')}. Re-run \`totem sync\` to regenerate.`);
    }
    const require = createRequire(import.meta.url);
    return result.data.packs.map((entry) => {
        const callback = resolvePackCallback(entry.name, entry.resolvedPath, require);
        const pack = {
            name: entry.name,
            resolvedPath: entry.resolvedPath,
            declaredEngineRange: entry.declaredEngineRange,
        };
        return { pack, callback };
    });
}
function resolvePackCallback(name, resolvedPath, require) {
    if (!fs.existsSync(resolvedPath)) {
        throw new Error(`Pack '${name}' is registered in installed-packs.json at '${resolvedPath}' but the path does not exist. Re-install the pack or re-run \`totem sync\`.`);
    }
    // Probe for an entry point before attempting to load. Data-only packs
    // intentionally ship workflows + templates only with no
    // `main`/`exports`/`register.*`. For these packs, return a no-op
    // callback so the pack is registered as known-but-data-only and its
    // workflows/templates remain available to other consumers (session
    // hooks, totem describe, etc.). See mmnto-ai/totem#1848 for the
    // 1.30.0 regression that this guards against.
    //
    // Scoping the try/catch tightly to `require.resolve` is load-bearing —
    // wrapping the actual `require()` would mask MODULE_NOT_FOUND errors
    // thrown by code INSIDE the pack's entry point.
    try {
        require.resolve(resolvedPath);
    }
    catch (err) {
        const code = err.code;
        if (code === 'MODULE_NOT_FOUND' || code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
            return () => { };
        }
        throw new Error(`Pack '${name}' at '${resolvedPath}' could not be loaded.`, {
            cause: err instanceof Error ? err : new Error(String(err)),
        });
    }
    // Synchronous `require()` is mandated by ADR-097 § 5 Q5: pack
    // registration runs at boot before the engine seal, and boot is
    // synchronous. This means the pack's registration entry must be
    // CommonJS-resolvable — either authored as CJS, or ESM packs must
    // ship a CJS-compatible registration entry (e.g., a built
    // `dist/register.cjs`). Pure-ESM registration entries will throw
    // `ERR_REQUIRE_ESM` at boot. Async-boot support (`await import()`)
    // would lift this constraint and is tracked separately
    // (mmnto-ai/totem#1771).
    let mod;
    try {
        mod = require(resolvedPath);
    }
    catch (err) {
        throw new Error(`Pack '${name}' at '${resolvedPath}' could not be loaded.`, {
            cause: err instanceof Error ? err : new Error(String(err)),
        });
    }
    // Pack callbacks may default-export OR named-export `register`. Default
    // export is the ADR-097 reference shape; `register` named export is the
    // CommonJS-friendly alternative.
    const candidate = mod.default ?? mod.register;
    if (typeof candidate !== 'function') {
        throw new Error(`Pack '${name}' at '${resolvedPath}' did not export a registration callback (expected default export or named \`register\` of type function).`);
    }
    return candidate;
}
// ─── Engine version cross-check (ADR-097 Q6) ────────
function assertEngineRangeSatisfied(pack, engineVersion) {
    if (!semver.validRange(pack.declaredEngineRange)) {
        throw new Error(`Pack '${pack.name}' declares engines['@mmnto/totem'] = '${pack.declaredEngineRange}', which is not a valid semver range. Fix the pack's package.json.`);
    }
    if (!semver.satisfies(engineVersion, pack.declaredEngineRange, { includePrerelease: true })) {
        throw new Error(`Pack '${pack.name}' requires @mmnto/totem '${pack.declaredEngineRange}' but the running engine is ${engineVersion}. Upgrade the pack to a version compatible with this engine, or pin the engine to a version satisfied by the pack's range.`);
    }
}
/**
 * Resolve the running `@mmnto/totem` package version from disk.
 *
 * Exported (mmnto-ai/totem#1811) so `pack-manifest-writer.ts` can
 * stamp the manifest's `cohort` field at write time and the
 * `rule-engine.ts` parser-error intercept can compare the running
 * engine against the manifest's recorded cohort.
 */
export function resolveEngineVersion() {
    // Resolve relative to this module's URL so the read works regardless
    // of cwd. The package.json sits at packages/core/package.json — two
    // levels up from packages/core/dist/pack-discovery.js when the build
    // output runs, and one level up from packages/core/src/pack-discovery.ts
    // during tsx test execution. Try both.
    const require = createRequire(import.meta.url);
    for (const candidate of ['../package.json', '../../package.json']) {
        try {
            const pkg = require(candidate);
            if (typeof pkg.version === 'string')
                return pkg.version;
        }
        catch (err) {
            const code = err instanceof Error && 'code' in err ? err.code : undefined;
            if (code === 'ENOENT' || code === 'MODULE_NOT_FOUND')
                continue;
            throw new Error(`Failed to resolve engine version from '${candidate}'. The file exists but could not be read or parsed; check permissions and JSON syntax.`, { cause: err instanceof Error ? err : new Error(String(err)) });
        }
    }
    // Fall back to a sentinel that won't satisfy any reasonable range —
    // surfaces the missing-version-resolution as a peerDeps mismatch rather
    // than an undefined-behavior path. If a pack legitimately uses range
    // '*' this will still pass.
    return '0.0.0';
}
// ─── Test-only helpers ──────────────────────────────
/**
 * Test-only: reset pack-discovery local state (`PACK_REGISTRY` map and
 * `engineSealed` flag). NEVER call from production.
 *
 * **This does NOT reset downstream registries.** Tests that need a
 * fresh chunker or language registry must additionally invoke
 * `__resetForTests` from `chunker-registry.js` and `ast-classifier.js`
 * — those modules own their own lifecycle. Forgetting either reset
 * leaks built-in registrations across cases. The standard `afterEach`
 * pattern in `pack-discovery.test.ts` calls all three.
 */
export function __resetForTests() {
    PACK_REGISTRY.clear();
    engineSealed = false;
}
/** Test-only: inspect chunker registry seal state. */
export function __isChunkerRegistrySealedForTests() {
    return isChunkerRegistrySealed();
}
//# sourceMappingURL=pack-discovery.js.map