"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OutputBudgetError = exports.APPROVED_BOUNDED_READ_COMMANDS = exports.JSON_SPACING_VALUES = exports.DEFAULT_COMPACT_OUTPUT_BUDGET_BYTES = exports.MINIMUM_OUTPUT_BUDGET_BYTES = void 0;
exports.isApprovedBoundedReadCommand = isApprovedBoundedReadCommand;
exports.serializeBudgetedEnvelope = serializeBudgetedEnvelope;
const collection_pagination_1 = require("./collection-pagination");
exports.MINIMUM_OUTPUT_BUDGET_BYTES = 2048;
exports.DEFAULT_COMPACT_OUTPUT_BUDGET_BYTES = 8192;
exports.JSON_SPACING_VALUES = ['compact', 'pretty'];
exports.APPROVED_BOUNDED_READ_COMMANDS = [
    'about',
    'agent',
    'agent context',
    'agent remind',
    'commands inventory',
    'commands manifest',
    'commands search',
    'config inventory',
    'doctor',
    'metadata inventory',
    'planning gap-report',
    'planning inventory',
    'planning next-action',
    'resume run compare',
    'resume run explain',
    'resume run friction-report',
    'resume run resume',
    'resume run status',
    'resume inspect',
    'resume resume',
    'spec list',
    'spec search',
    'structural-pressure report',
    'templates list',
    'validation links-check',
    'validation rollup',
    'validation smoke',
    'version doctor',
    'work candidates',
    'work current',
    'work next',
    'workflow compile',
    'workflow explain',
];
class OutputBudgetError extends Error {
    code;
    minimumBytes;
    constructor(code, message, minimumBytes) {
        super(message);
        this.name = 'OutputBudgetError';
        this.code = code;
        this.minimumBytes = minimumBytes;
    }
}
exports.OutputBudgetError = OutputBudgetError;
function isApprovedBoundedReadCommand(command) {
    return exports.APPROVED_BOUNDED_READ_COMMANDS.includes(command);
}
function serializeBudgetedEnvelope(source, options) {
    validateBudget(options.byteBudget, options.spacing);
    const envelope = cloneEnvelope(source);
    setBudgetMetadata(envelope, {
        byteBudget: options.byteBudget,
        renderedBytes: 1,
        serialization: options.spacing,
        strategy: 'within-budget',
        recovery: [],
    });
    const withinBudget = stabilizeEnvelope(envelope, options.spacing);
    if (withinBudget.bytes <= options.byteBudget) {
        return withinBudget;
    }
    const bounded = createBoundedSummary(envelope, options.byteBudget);
    const boundedResult = stabilizeEnvelope(bounded, 'compact');
    if (boundedResult.bytes <= options.byteBudget) {
        return boundedResult;
    }
    const minimal = createMinimalFallback(bounded, options.byteBudget);
    const minimalResult = stabilizeEnvelope(minimal, 'compact');
    if (minimalResult.bytes <= options.byteBudget) {
        return minimalResult;
    }
    throw new OutputBudgetError('OUTPUT_BUDGET_IRREDUCIBLE', `The smallest safe protocol envelope requires ${minimalResult.bytes} bytes.`, minimalResult.bytes);
}
function createBoundedSummary(source, byteBudget) {
    const envelope = cloneEnvelope(source);
    const findings = getFindings(envelope);
    const blockers = compactBlockerRepresentatives(findings);
    const terminalAgentBootstrap = envelope.status === 'ok' && envelope.command === 'agent' && findings.length === 0;
    const recovery = terminalAgentBootstrap
        ? []
        : [
            'Rerun with --fields, --limit, or --page-lane/--page-size to request bounded detail.',
            'Increase --output-budget or use --detail full without a budget for audit output.',
        ];
    setFindings(envelope, blockers);
    envelope.output.complete = terminalAgentBootstrap;
    envelope.output.truncated = !terminalAgentBootstrap;
    envelope.output.findingGroups = groupBlockers(findings);
    envelope.output.lanes = {
        findings: {
            originalCount: findings.length,
            returnedCount: blockers.length,
            omittedCount: Math.max(0, findings.length - blockers.length),
            complete: blockers.length === findings.length,
        },
    };
    delete envelope.output.page;
    setBudgetMetadata(envelope, {
        byteBudget,
        renderedBytes: 1,
        serialization: 'compact',
        strategy: 'bounded-summary',
        recovery,
    });
    if (envelope.status === 'ok') {
        envelope.data = terminalAgentBootstrap
            ? summarizeAgentOrientationData(envelope.data)
            : summarizeData(envelope.command, envelope.data, recovery);
    }
    else {
        envelope.error.message = truncateText(envelope.error.message, 256);
        envelope.error.details = envelope.error.details.slice(0, 3).map((detail) => truncateText(detail, 256));
        if (envelope.data !== undefined) {
            envelope.data = summarizeData(envelope.command, envelope.data, recovery);
        }
    }
    return envelope;
}
function summarizeAgentOrientationData(data) {
    const source = isRecord(data) ? data : {};
    const rootContext = isRecord(source.rootContext) ? source.rootContext : {};
    const entrypoint = isRecord(rootContext.agentEntrypoint) ? rootContext.agentEntrypoint : {};
    return {
        compactOrientation: true,
        lane: 'bootstrap',
        ...(isRecord(source.runtimeProvenance)
            ? {
                runtimeProvenance: source.runtimeProvenance,
            }
            : {}),
        workspace: {
            effectiveRoot: rootContext.effectiveRoot ?? '.',
            detectionSource: rootContext.detectionSourceLabel ?? null,
            markerPath: rootContext.markerPath ?? null,
            agentEntrypoint: entrypoint.path ?? '.day-shift/agents.md',
        },
        complete: true,
        scopedBootstrap: [
            'day-shift work candidates --format json --detail compact',
            'day-shift agent context --artifact <path> --format json --detail compact',
            'day-shift doctor --artifact <path> --format json',
        ],
        routes: {
            noArtifact: ['day-shift work candidates --format json --detail compact'],
            explicitArtifact: [
                'day-shift agent context --artifact <path> --format json --detail compact',
                'day-shift doctor --artifact <path> --format json',
            ],
        },
        recovery: [
            'Follow only typed recovery returned by the selected command when decision-relevant evidence is incomplete.',
        ],
        audit: {
            command: 'day-shift agent orient --format json',
            automatic: false,
            useWhen: 'Run only when the caller explicitly requests full repository-orientation audit evidence.',
        },
    };
}
function isRecord(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function createMinimalFallback(source, byteBudget) {
    const envelope = cloneEnvelope(source);
    const findings = getFindings(envelope);
    const recovery = ['Use pagination, selected fields, or a larger output budget.'];
    envelope.output.complete = false;
    envelope.output.truncated = true;
    envelope.output.findingGroups = groupBlockers(findings);
    envelope.output.lanes = {
        findings: {
            originalCount: findings.length,
            returnedCount: 0,
            omittedCount: findings.length,
            complete: findings.length === 0,
        },
    };
    delete envelope.output.page;
    setFindings(envelope, []);
    setBudgetMetadata(envelope, {
        byteBudget,
        renderedBytes: 1,
        serialization: 'compact',
        strategy: 'minimal-fallback',
        recovery,
    });
    if (envelope.status === 'ok') {
        const data = isRecord(envelope.data) ? envelope.data : {};
        envelope.data = envelope.command === 'resume run status' || envelope.command === 'resume run resume'
            ? createMinimalLifecycleDecisionData(data, recovery)
            : {
                bounded: true,
                ...projectMandatoryDecisionData(data),
                recovery,
            };
    }
    else {
        envelope.error.message = 'Output was reduced to fit the selected byte budget.';
        envelope.error.details = recovery;
        delete envelope.data;
    }
    return envelope;
}
function createMinimalLifecycleDecisionData(data, outputRecovery) {
    const hasCurrentNextAction = Object.hasOwn(data, 'currentNextAction');
    const resumeCapsule = projectResumeCapsule(data.resumeCapsule, !hasCurrentNextAction);
    const recovery = projectLifecycleRecovery(data.recovery, true);
    const retainedFields = [
        'state',
        'runId',
        'runDirectory',
        'currentGate',
        'terminalState',
        'manifestComplete',
        'entryCount',
        'blockerCount',
        'nextPermittedGate',
        'recordedGate',
        'historyAuthoritative',
    ];
    return {
        bounded: true,
        decisionComplete: true,
        ...Object.fromEntries(retainedFields.filter((field) => Object.hasOwn(data, field)).map((field) => [field, data[field]])),
        ...(hasCurrentNextAction ? { currentNextAction: data.currentNextAction } : {}),
        ...(Object.hasOwn(data, 'validationRecoveryAction')
            ? { validationRecoveryAction: data.validationRecoveryAction }
            : {}),
        ...(!hasCurrentNextAction && Object.hasOwn(data, 'nextAction') ? { nextAction: data.nextAction } : {}),
        ...(resumeCapsule === undefined ? {} : { resumeCapsule }),
        ...(recovery === undefined ? {} : { recovery }),
        outputRecovery,
    };
}
function stabilizeEnvelope(envelope, spacing) {
    for (let attempt = 0; attempt < 8; attempt += 1) {
        const text = `${JSON.stringify(envelope, null, spacing === 'pretty' ? 2 : undefined)}\n`;
        const bytes = Buffer.byteLength(text, 'utf8');
        const budget = envelope.output.budget;
        if (budget?.renderedBytes === bytes) {
            return { envelope, text, bytes };
        }
        if (budget === undefined) {
            throw new TypeError('Budgeted envelope metadata is missing.');
        }
        budget.renderedBytes = bytes;
    }
    throw new TypeError('Budgeted envelope byte metadata did not stabilize.');
}
function summarizeData(command, data, recovery) {
    if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
        if (command === 'resume run status' || command === 'resume run resume') {
            return summarizeLifecycleRunData(data, recovery);
        }
        const commandProjection = (0, collection_pagination_1.projectDecisionCompleteCommandData)(command, data, {
            retainPageLane: false,
        });
        if (commandProjection !== undefined) {
            return {
                bounded: true,
                ...projectMandatoryDecisionData(data),
                ...commandProjection,
                recovery,
            };
        }
        const fields = Object.keys(data);
        const nestedCollections = collectNestedCollectionSummaries(data);
        return {
            bounded: true,
            ...projectMandatoryDecisionData(data),
            originalTopLevelFieldCount: fields.length,
            omittedTopLevelFields: fields,
            nestedCollections: nestedCollections.entries,
            nestedCollectionTraversal: {
                originalCount: nestedCollections.originalCount,
                returnedCount: nestedCollections.entries.length,
                omittedCount: Math.max(0, nestedCollections.originalCount - nestedCollections.entries.length),
                complete: nestedCollections.originalCount === nestedCollections.entries.length,
                nextCursor: nestedCollections.originalCount === nestedCollections.entries.length ? null : 'detail:full',
            },
            ...(isRecord(data.runtimeProvenance)
                ? {
                    runtimeProvenance: data.runtimeProvenance,
                }
                : {}),
            recovery,
        };
    }
    return {
        bounded: true,
        originalKind: Array.isArray(data) ? 'array' : typeof data,
        recovery,
    };
}
function summarizeLifecycleRunData(data, outputRecovery) {
    const retainedFields = [
        'state',
        'runId',
        'runDirectory',
        'currentGate',
        'activeArtifact',
        'lastAcceptedGate',
        'terminalState',
        'manifestComplete',
        'entryCount',
        'inactiveEntryCount',
        'evidenceBytes',
        'contractRevisionCount',
        'structuredFileCounts',
        'commandEnvelopeOutcomes',
        'validationOutcomes',
        'blockerCount',
        'nextPermittedGate',
        'recordedGate',
        'historyAuthoritative',
        'noWriteBoundary',
    ];
    const hasCurrentNextAction = Object.hasOwn(data, 'currentNextAction');
    const resumeCapsule = projectResumeCapsule(data.resumeCapsule, !hasCurrentNextAction);
    const recovery = projectLifecycleRecovery(data.recovery, false);
    return {
        bounded: true,
        decisionComplete: true,
        ...Object.fromEntries(retainedFields.filter((field) => Object.hasOwn(data, field)).map((field) => [field, data[field]])),
        ...(Array.isArray(data.blockers)
            ? { blockers: data.blockers.slice(0, 4).map((blocker) => truncateText(String(blocker), 160)) }
            : {}),
        ...(hasCurrentNextAction ? { currentNextAction: data.currentNextAction } : {}),
        ...(Object.hasOwn(data, 'validationRecoveryAction')
            ? { validationRecoveryAction: data.validationRecoveryAction }
            : {}),
        ...(!hasCurrentNextAction && Object.hasOwn(data, 'nextAction') ? { nextAction: data.nextAction } : {}),
        ...(resumeCapsule === undefined ? {} : { resumeCapsule }),
        ...(recovery === undefined ? {} : { recovery }),
        outputRecovery,
    };
}
const MANDATORY_DECISION_FIELDS = [
    'state',
    'status',
    'outcome',
    'writeOutcome',
    'readOnly',
    'complete',
    'manifestComplete',
    'runId',
    'runDirectory',
    'manifestPath',
    'scopePath',
    'artifactPath',
    'currentGate',
    'blockerCount',
    'nextPermittedGate',
    'recordedGate',
    'historyAuthoritative',
    'nextAction',
    'currentNextAction',
    'recommendedNextAction',
    'nextCallableAction',
    'validationRecoveryAction',
    'recoveryActions',
];
function projectMandatoryDecisionData(data) {
    return Object.fromEntries(MANDATORY_DECISION_FIELDS
        .filter((field) => Object.hasOwn(data, field))
        .map((field) => [field, data[field]]));
}
function projectResumeCapsule(value, retainNextAction) {
    if (!isRecord(value))
        return undefined;
    const retainedFields = [
        'activeArtifactPath',
        'activeArtifactRevision',
        'currentGate',
        'lastAcceptedGate',
        'completedActionCount',
        'omittedCompletedActionCount',
        'doNotRepeat',
        'targetedPagingCommands',
    ];
    const completedActionIds = Array.isArray(value.completedActionIds)
        ? value.completedActionIds.slice(-4)
        : undefined;
    return {
        ...Object.fromEntries(retainedFields.filter((field) => Object.hasOwn(value, field)).map((field) => [field, value[field]])),
        ...(completedActionIds === undefined ? {} : { completedActionIds }),
        ...(retainNextAction && Object.hasOwn(value, 'nextAction') ? { nextAction: value.nextAction } : {}),
    };
}
function projectLifecycleRecovery(value, minimal) {
    if (!isRecord(value))
        return undefined;
    const retainedFields = minimal
        ? ['fullDetailCommand', 'pagingCommand']
        : ['fullDetailCommand', 'pagingCommand', 'pagingCommands', 'auditHistoryPreserved'];
    return Object.fromEntries(retainedFields.filter((field) => Object.hasOwn(value, field)).map((field) => [field, value[field]]));
}
function collectNestedCollectionSummaries(value, maximumEntries = 8) {
    const entries = [];
    let originalCount = 0;
    const visit = (candidate, path, seen) => {
        if (typeof candidate !== 'object' || candidate === null || seen.has(candidate))
            return;
        seen.add(candidate);
        if (Array.isArray(candidate)) {
            originalCount += 1;
            if (entries.length < maximumEntries) {
                entries.push({
                    path,
                    originalCount: candidate.length,
                    returnedCount: 0,
                    omittedCount: candidate.length,
                    complete: candidate.length === 0,
                    nextCursor: candidate.length === 0 ? null : 'detail:full',
                });
            }
            for (let index = 0; index < candidate.length; index += 1) {
                visit(candidate[index], `${path}[${index}]`, seen);
            }
            return;
        }
        for (const key of Object.keys(candidate).sort()) {
            visit(candidate[key], path.length === 0 ? key : `${path}.${key}`, seen);
        }
    };
    visit(value, '', new Set());
    return { entries, originalCount };
}
function compactBlockerRepresentatives(findings) {
    const seen = new Set();
    const blockers = [];
    for (const finding of findings) {
        if (!isBlockerFindingLike(finding)) {
            continue;
        }
        const key = `${finding.code}\u0000${finding.severity}`;
        if (!seen.has(key)) {
            seen.add(key);
            blockers.push({
                ...(typeof finding.id === 'string' ? { id: finding.id } : {}),
                code: finding.code,
                severity: 'error',
                message: truncateText(String(finding.message ?? finding.code), 160),
            });
        }
    }
    return blockers;
}
function groupBlockers(findings) {
    const counts = new Map();
    for (const finding of findings) {
        if (isBlockerFindingLike(finding)) {
            counts.set(finding.code, (counts.get(finding.code) ?? 0) + 1);
        }
    }
    return [...counts].map(([code, count]) => ({ code, severity: 'error', count }));
}
function getFindings(envelope) {
    return envelope.status === 'ok' ? envelope.findings : envelope.error.findings;
}
function setFindings(envelope, findings) {
    if (envelope.status === 'ok') {
        envelope.findings = findings;
    }
    else {
        envelope.error.findings = findings;
    }
}
function setBudgetMetadata(envelope, budget) {
    envelope.output.budget = budget;
}
function cloneEnvelope(source) {
    return JSON.parse(JSON.stringify(source));
}
function isBlockerFindingLike(value) {
    return (typeof value === 'object' &&
        value !== null &&
        !Array.isArray(value) &&
        value.severity === 'error' &&
        typeof value.code === 'string');
}
function truncateText(value, maximum) {
    return value.length <= maximum ? value : `${value.slice(0, maximum - 1)}…`;
}
function validateBudget(byteBudget, spacing) {
    if (!Number.isSafeInteger(byteBudget) || byteBudget < exports.MINIMUM_OUTPUT_BUDGET_BYTES) {
        throw new OutputBudgetError('OUTPUT_BUDGET_INVALID', `Output budget must be a safe integer of at least ${exports.MINIMUM_OUTPUT_BUDGET_BYTES} bytes.`, exports.MINIMUM_OUTPUT_BUDGET_BYTES);
    }
    if (!exports.JSON_SPACING_VALUES.includes(spacing)) {
        throw new OutputBudgetError('OUTPUT_BUDGET_INVALID', `JSON spacing must be one of: ${exports.JSON_SPACING_VALUES.join(', ')}.`, exports.MINIMUM_OUTPUT_BUDGET_BYTES);
    }
}
