"use strict";
/**
 * Structured logging substrate (round-2-deferred §3).
 *
 * Every log line is one JSON object on stdout. The wire shape mirrors
 * pino's defaults so a future swap to pino is transparent for any
 * downstream consumer (`chorus logs --chat <id>`, log aggregators, etc):
 *
 *   {"level":30,"time":1714654200000,"pid":1234,"hostname":"box","chatId":"abc","msg":"phase done"}
 *
 * - `level` is the numeric pino value (debug=20, info=30, warn=40, error=50)
 * - `time` is ms since epoch (not `ts`)
 * - `pid` + `hostname` are emitted once per line, captured at logger-init
 *
 * Child loggers carry their context fields — call sites in the runner do
 *   `const log = chatLogger(chatId, { phase: 'review', role: 'reviewer' })`
 *   log.info({lineage:'openai'}, 'reviewer started')
 * and the chatId / phase / role get baked into every line that logger
 * emits without each call site repeating them.
 *
 * No runtime dependency. The default is `console.log(JSON.stringify(...))` —
 * if a future load profile justifies it, swap to pino without changing
 * call sites OR the wire shape.
 *
 * `CHORUS_LOG_LEVEL` env var gates the floor (debug | info | warn | error).
 * Default is info. Setting it to debug surfaces verbose internals; setting
 * it to error silences phase-progress noise during dogfood streaming.
 *
 * Round-1 dogfood (PR #7) caught:
 *   - ts/level were clobberable by user fields → core fields now win
 *   - JSON.stringify could throw on circular refs / BigInt → safe wrapper
 *     emits a degraded fallback line instead of crashing the daemon
 *   - Error objects serialized to `{}` → expanded to {message,name,stack}
 *
 * Test seam: pass `_writer` when constructing — tests inject a function
 * that pushes lines into an array and assert exact JSON shape. Production
 * call sites use the default `console.log` chain.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.logger = void 0;
exports.createLogger = createLogger;
exports.chatLogger = chatLogger;
const os_1 = require("os");
/** Pino-compatible numeric levels. Wire shape matches pino's default. */
const LEVEL_RANK = {
    debug: 20,
    info: 30,
    warn: 40,
    error: 50,
};
const defaultWriter = (line) => {
    // process.stdout.write is sync + line-buffered when stdout is a pipe (the
    // typical daemon shape). console.log adds a trailing newline that we
    // want; it's also what existing chorus call sites use, so output ordering
    // stays the same when sites migrate.
    console.log(line);
};
/** Captured once at module load. Daemon doesn't move between machines. */
const HOST = (() => {
    try {
        return (0, os_1.hostname)();
    }
    catch {
        return 'unknown';
    }
})();
const PID = process.pid;
/**
 * Replacer for JSON.stringify that:
 *   - Expands `Error` instances to {message, name, stack, cause?}
 *   - Defangs BigInt to its string form (JSON.stringify throws on raw BigInt)
 *   - Detects circular references and returns the literal string
 *     '[Circular]' rather than throwing.
 *
 * Falls through to the default stringify behaviour for everything else.
 */
function buildSafeReplacer() {
    const seen = new WeakSet();
    return function safe(_key, value) {
        if (typeof value === 'bigint')
            return value.toString();
        if (value instanceof Error) {
            const out = {
                message: value.message,
                name: value.name,
                stack: value.stack,
            };
            const cause = value.cause;
            if (cause !== undefined)
                out.cause = cause;
            return out;
        }
        if (value !== null && typeof value === 'object') {
            if (seen.has(value))
                return '[Circular]';
            seen.add(value);
        }
        return value;
    };
}
/**
 * Stringify a log line without ever throwing. The replacer handles
 * Error / BigInt / circular refs; if it still fails (toJSON that
 * throws, exotic proxies, etc.) we emit a degraded fallback line
 * referencing the failure rather than crashing the daemon's logger.
 */
function safeStringify(line) {
    try {
        return JSON.stringify(line, buildSafeReplacer());
    }
    catch (err) {
        return JSON.stringify({
            level: line.level,
            time: line.time,
            pid: line.pid,
            hostname: line.hostname,
            msg: 'log_serialize_failed',
            err: err instanceof Error ? err.message : String(err),
        });
    }
}
class LoggerImpl {
    bound;
    threshold;
    writer;
    constructor(bound, threshold, writer) {
        this.bound = bound;
        this.threshold = threshold;
        this.writer = writer;
    }
    emit(level, fieldsOrMsg, msg) {
        if (LEVEL_RANK[level] < this.threshold)
            return;
        const isFields = typeof fieldsOrMsg === 'object' && fieldsOrMsg !== null;
        const fields = isFields ? fieldsOrMsg : {};
        const message = isFields ? msg : fieldsOrMsg;
        // Spread order: user-provided spreads FIRST, then core wire fields
        // (level, time, pid, hostname). User fields cannot clobber the core
        // shape — round-1 dogfood caught a `log.info({level:'fake'})` foot-gun.
        const line = {
            ...this.bound,
            ...fields,
            ...(message !== undefined ? { msg: message } : {}),
            level: LEVEL_RANK[level],
            time: Date.now(),
            pid: PID,
            hostname: HOST,
        };
        this.writer(safeStringify(line));
    }
    debug(fields, msg) { this.emit('debug', fields, msg); }
    info(fields, msg) { this.emit('info', fields, msg); }
    warn(fields, msg) { this.emit('warn', fields, msg); }
    error(fields, msg) { this.emit('error', fields, msg); }
    child(bound) {
        return new LoggerImpl({ ...this.bound, ...bound }, this.threshold, this.writer);
    }
}
function resolveThreshold() {
    const raw = (process.env.CHORUS_LOG_LEVEL ?? '').toLowerCase();
    if (raw === 'debug' || raw === 'info' || raw === 'warn' || raw === 'error') {
        return LEVEL_RANK[raw];
    }
    return LEVEL_RANK.info;
}
/**
 * Build the root logger. The daemon constructs ONE root and passes child
 * loggers down to call sites. Tests construct their own with an in-memory
 * writer.
 */
function createLogger(opts) {
    const writer = opts?._writer ?? defaultWriter;
    const threshold = opts?._level ? LEVEL_RANK[opts._level] : resolveThreshold();
    return new LoggerImpl({}, threshold, writer);
}
/**
 * Module-singleton root logger for general-purpose call sites that don't
 * have a chat / request context. Prefer `chatLogger(chatId)` when the
 * chat correlation key is known.
 */
exports.logger = createLogger();
/** Convenience: child logger pre-bound to a chat correlation key. */
function chatLogger(chatId, extra) {
    return exports.logger.child({ chatId, ...extra });
}
//# sourceMappingURL=logger.js.map