qnce-engine
Version: 
Core QNCE (Quantum Narrative Convergence Engine) - Framework agnostic narrative engine with performance optimization
191 lines • 8.51 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = require("fs");
const path_1 = require("path");
const core_js_1 = require("../telemetry/core.js");
const core_js_2 = require("../engine/core.js");
const CustomJSONAdapter_js_1 = require("../adapters/story/CustomJSONAdapter.js");
const TwisonAdapter_js_1 = require("../adapters/story/TwisonAdapter.js");
const InkAdapter_js_1 = require("../adapters/story/InkAdapter.js");
const validateStoryData_js_1 = require("../schemas/validateStoryData.js");
/**
 * QNCE Import CLI
 * Detects story format (Custom JSON, Twison, basic Ink) and outputs normalized QNCE StoryData JSON
 */
function detectAdapter(payload) {
    const candidates = [
        { key: 'json', inst: new CustomJSONAdapter_js_1.CustomJSONAdapter() },
        { key: 'twison', inst: new TwisonAdapter_js_1.TwisonAdapter() },
        { key: 'ink', inst: new InkAdapter_js_1.InkAdapter() }
    ];
    for (const c of candidates) {
        try {
            if (c.inst.detect?.(payload))
                return c;
        }
        catch { }
    }
    return candidates[0];
}
async function readStdin() {
    const chunks = [];
    return await new Promise((resolve) => {
        process.stdin.on('data', (d) => chunks.push(Buffer.from(d)));
        process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
    });
}
async function main() {
    const args = process.argv.slice(2);
    const showHelp = args.length === 0 && process.stdin.isTTY;
    if (showHelp || args.includes('--help') || args.includes('-h')) {
        console.log(`\nQNCE Import CLI\nUsage: qnce-import <input-file>|(read from stdin) [--out <file>|stdout] [--id-prefix <prefix>] [--format json|twison|ink] [--strict] [--experimental-ink] [--telemetry <console|file|none>] [--telemetry-file <path>] [--telemetry-sample <0..1>]\n`);
        process.exit(0);
    }
    const formatIdx = args.indexOf('--format');
    const format = formatIdx >= 0 ? args[formatIdx + 1] : undefined;
    const strict = args.includes('--strict');
    const experimentalInk = args.includes('--experimental-ink');
    const idPrefixIndex = args.indexOf('--id-prefix');
    const idPrefix = idPrefixIndex >= 0 ? args[idPrefixIndex + 1] : '';
    const outIndex = args.indexOf('--out');
    const outArg = outIndex >= 0 ? args[outIndex + 1] : undefined;
    // Telemetry flags
    const telemetryIdx = args.indexOf('--telemetry');
    const telemetryKind = telemetryIdx >= 0 ? (args[telemetryIdx + 1] || 'none') : 'none';
    const telemetryFileIdx = args.indexOf('--telemetry-file');
    const telemetryPath = telemetryFileIdx >= 0 ? args[telemetryFileIdx + 1] : undefined;
    const telemetrySampleIdx = args.indexOf('--telemetry-sample');
    const telemetrySample = telemetrySampleIdx >= 0 ? Math.max(0, Math.min(1, parseFloat(args[telemetrySampleIdx + 1]))) : undefined;
    let telemetry;
    if (telemetryKind && telemetryKind !== 'none') {
        const adapter = telemetryKind === 'file' ? (0, core_js_1.createTelemetryAdapter)('file', { path: telemetryPath || 'qnce-import.ndjson' }) : (0, core_js_1.createTelemetryAdapter)('console');
        telemetry = (0, core_js_1.createTelemetry)({ adapter, enabled: true, sampleRate: telemetrySample ?? (process.env.NODE_ENV === 'production' ? 0 : 0.25), defaultCtx: { engineVersion: '1.3.1', env: process.env.NODE_ENV || 'dev', sessionId: `import-${Date.now()}` } });
    }
    let raw;
    let inputName = 'stdin';
    if (args[0] && !args[0].startsWith('--')) {
        const inPath = (0, path_1.resolve)(args[0]);
        inputName = inPath;
        raw = (0, fs_1.readFileSync)(inPath, 'utf-8');
    }
    else {
        raw = await readStdin();
    }
    let exitCode = 0;
    try {
        const t0 = Date.now();
        const json = JSON.parse(raw);
        let selected;
        if (format) {
            const map = {
                json: new CustomJSONAdapter_js_1.CustomJSONAdapter(),
                twison: new TwisonAdapter_js_1.TwisonAdapter(),
                ink: new InkAdapter_js_1.InkAdapter()
            };
            if (!map[format])
                throw new Error(`Unknown format: ${format}`);
            selected = { key: format, inst: map[format] };
        }
        else {
            selected = detectAdapter(json);
            console.log(`ℹ️  Detected format: ${selected.key} (from ${inputName})`);
        }
        const normalized = await selected.inst.load(json, { idPrefix, strict, experimentalInk });
        // Schema validation (strict enforces failure)
        const schema = (0, validateStoryData_js_1.validateStoryData)(normalized);
        if (!schema.valid) {
            const msg = `Schema validation failed with ${schema.errors?.length || 0} error(s).`;
            const fmtErrors = (schema.errors || []).map((e) => ` - ${(e.instancePath ?? e.dataPath) || ''} ${e.message || ''}`).join('\n');
            if (strict) {
                console.error(`❌ ${msg}`);
                if (fmtErrors)
                    console.error(fmtErrors);
                process.exit(2);
            }
            else {
                console.warn(`⚠️  ${msg}`);
                if (fmtErrors)
                    console.warn(fmtErrors);
                exitCode = Math.max(exitCode, 1);
            }
        }
        // Additional semantic checks: initial node exists, dangling nextNodeId targets
        const nodeIds = new Set(normalized.nodes.map(n => n.id));
        const invalidLinks = [];
        for (const n of normalized.nodes) {
            for (const c of n.choices) {
                if (c.nextNodeId && !nodeIds.has(c.nextNodeId)) {
                    invalidLinks.push({ from: n.id, to: c.nextNodeId });
                }
            }
        }
        const initialExists = nodeIds.has(normalized.initialNodeId);
        if (!initialExists) {
            const msg = `Initial node '${normalized.initialNodeId}' does not exist in nodes`;
            if (strict) {
                console.error(`❌ ${msg}`);
                process.exit(2);
            }
            else {
                console.warn(`⚠️  ${msg}`);
                exitCode = Math.max(exitCode, 1);
            }
        }
        if (invalidLinks.length > 0) {
            const msg = `Found ${invalidLinks.length} dangling link(s)`;
            const lines = invalidLinks.slice(0, 10).map(l => ` - ${l.from} -> ${l.to} (missing)`);
            if (strict) {
                console.error(`❌ ${msg}`);
                if (lines.length)
                    console.error(lines.join('\n'));
                process.exit(2);
            }
            else {
                console.warn(`⚠️  ${msg}`);
                if (lines.length)
                    console.warn(lines.join('\n'));
                exitCode = Math.max(exitCode, 1);
            }
        }
        const story = (0, core_js_2.loadStoryData)(normalized);
        // Emit single load event with duration and warnings
        const durationMs = Date.now() - t0;
        try {
            telemetry?.emit({ type: 'import.load', payload: { inputName, format: selected.key, durationMs, warnings: exitCode }, ts: Date.now(), ctx: { sessionId: `import-${t0}`, engineVersion: '1.3.1' } });
        }
        catch { }
        if (outArg && outArg !== 'stdout') {
            const outPath = (0, path_1.resolve)(outArg);
            (0, fs_1.writeFileSync)(outPath, JSON.stringify(story, null, 2));
            console.log(`✅ Imported and normalized to ${outPath}`);
        }
        else if (!outArg && inputName !== 'stdin') {
            const outPath = (0, path_1.resolve)((0, path_1.basename)(inputName).replace(/\.[^.]+$/, '') + '.qnce.json');
            (0, fs_1.writeFileSync)(outPath, JSON.stringify(story, null, 2));
            console.log(`✅ Imported and normalized to ${outPath}`);
        }
        else {
            // stdout
            process.stdout.write(JSON.stringify(story, null, 2));
        }
        await telemetry?.flush();
        process.exit(exitCode);
    }
    catch (err) {
        console.error('❌ Import failed:', err?.message || err);
        try {
            await telemetry?.flush();
        }
        catch { }
        process.exit(2);
    }
}
const isMainModule = require.main === module;
if (isMainModule) {
    main().catch((e) => {
        console.error('❌ Import failed:', e?.message || e);
        process.exit(2);
    });
}
//# sourceMappingURL=import.js.map