"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.createCiGateRoutes = createCiGateRoutes;
/**
 * Plan 51 first slice — authenticated CI gate evaluation entry point
 * + persistent run audit log.
 *
 * Endpoints (mounted under `/api/ci`, `authenticate` applied at the
 * mount site so every verb requires a valid bearer/API key when one
 * is configured):
 *   POST /gate-eval            evaluate + persist a run
 *   GET  /gate-runs/:runId     fetch a persisted run
 *   GET  /gate-runs            list with optional filters
 *
 * Why a new entry point rather than reusing `compare_baselines` /
 * `baselineRoutes`: that surface is agent-facing and transient. A CI
 * caller needs auth + a stable `runId` so downstream IM/Bug/PR
 * adapters have a durable anchor to reference. Records for missing or
 * non-published baselines are still persisted with `status='skipped'`
 * + a concrete `skipReason` so the audit trail stays continuous —
 * silent 404s would leave gates without a paper trail.
 */
const crypto = __importStar(require("crypto"));
const express_1 = require("express");
const auth_1 = require("../middleware/auth");
const baselineDiffer_1 = require("../services/baselineDiffer");
const baselineStore_1 = require("../services/baselineStore");
const ciGateRunStore_1 = require("../services/ciGateRunStore");
const scopedKnowledgeStore_1 = require("../services/scopedKnowledgeStore");
const sparkContracts_1 = require("../types/sparkContracts");
const runtimePaths_1 = require("../runtimePaths");
const DEFAULT_BASELINE_PATH = (0, runtimePaths_1.backendLogPath)('baselines.json');
const MAX_CI_SOURCE_LEN = 64;
// Allow `/`, `:`, and `@` so qualified provider names common in CI
// systems (`org/repo`, `github.com/org/repo-ci`, `@org/pipeline`) are
// accepted. Whitespace and control chars stay rejected so the value is
// safe to log and to use as an SQLite filter column.
const CI_SOURCE_PATTERN = /^[a-zA-Z0-9_./@:-]+$/;
let cachedBaselineStore = null;
let cachedRunStore = null;
function getDefaultBaselineStore() {
    if (!cachedBaselineStore) {
        cachedBaselineStore = new baselineStore_1.BaselineStore(DEFAULT_BASELINE_PATH);
    }
    return cachedBaselineStore;
}
function getDefaultRunStore() {
    if (!cachedRunStore)
        cachedRunStore = new ciGateRunStore_1.CiGateRunStore();
    return cachedRunStore;
}
function createCiGateRoutes(deps = {}) {
    // Resolve default stores lazily inside each handler so a bare module
    // import (e.g. by a test that injects its own deps) does not trigger
    // the production sqlite mkdir + connection on module load.
    const resolveBaselineStore = () => deps.baselineStore ?? getDefaultBaselineStore();
    const resolveRunStore = () => deps.runStore ?? getDefaultRunStore();
    const router = (0, express_1.Router)();
    router.post('/gate-eval', (req, res) => {
        const baselineStore = resolveBaselineStore();
        const runStore = resolveRunStore();
        const storageScope = (0, scopedKnowledgeStore_1.knowledgeScopeFromRequestContext)((0, auth_1.requireRequestContext)(req));
        const body = (req.body ?? {});
        const gateId = stringField(body.gateId);
        const baselineId = stringField(body.baselineId);
        const ciSource = stringField(body.ciSource);
        if (!gateId || !baselineId || !ciSource) {
            return res.status(400).json({
                success: false,
                error: 'gateId, baselineId, and ciSource are required strings',
            });
        }
        if (ciSource.length > MAX_CI_SOURCE_LEN ||
            !CI_SOURCE_PATTERN.test(ciSource)) {
            return res.status(400).json({
                success: false,
                error: 'ciSource must match /^[a-zA-Z0-9_.-]+$/ and be at most 64 chars',
            });
        }
        const rules = parseRules(body.rules);
        if (!rules) {
            return res.status(400).json({
                success: false,
                error: 'rules must be a non-empty array of {metricId, threshold} objects',
            });
        }
        const candidateOrError = parseCandidate(body.candidate, baselineStore, storageScope);
        if ('error' in candidateOrError) {
            return res
                .status(400)
                .json({ success: false, error: candidateOrError.error });
        }
        const candidate = candidateOrError.candidate;
        const ciContext = parseCiContext(body.ciContext);
        const baseline = baselineStore.getBaseline(baselineId, storageScope);
        if (!baseline) {
            const skipped = makeSkippedRun({
                runId: makeRunId(),
                gateId,
                baselineId,
                baselineStatus: 'draft',
                ciSource,
                ciContext,
                authSubject: subjectFromReq(req),
                candidateSnapshot: candidate.snapshot,
                rulesSnapshot: rules,
                skipReason: 'baseline_not_found',
            });
            runStore.recordRun(skipped);
            return res.status(200).json({
                success: true,
                runId: skipped.runId,
                result: skipped.result,
                skipReason: skipped.skipReason,
            });
        }
        if (baseline.status !== 'published') {
            const skipped = makeSkippedRun({
                runId: makeRunId(),
                gateId,
                baselineId,
                baselineStatus: baseline.status,
                ciSource,
                ciContext,
                authSubject: subjectFromReq(req),
                candidateSnapshot: candidate.snapshot,
                rulesSnapshot: rules,
                skipReason: `baseline_status_${baseline.status}`,
            });
            runStore.recordRun(skipped);
            return res.status(200).json({
                success: true,
                runId: skipped.runId,
                result: skipped.result,
                skipReason: skipped.skipReason,
            });
        }
        const diffCandidate = candidate.snapshot.kind === 'trace'
            ? {
                kind: 'trace',
                traceId: candidate.snapshot.traceId ?? 'inline',
                metrics: candidate.snapshot.metrics,
            }
            : candidate.baselineRecord;
        const diff = (0, baselineDiffer_1.computeBaselineDiff)(baseline, diffCandidate);
        const knownMetricIds = new Set(diff.deltas.map(d => d.metricId));
        const ruleHasUnknownMetric = rules.some(r => !knownMetricIds.has(r.metricId));
        // evaluateRegressionGate silently skips rules that point at metrics
        // not in the diff (baselineDiffer.ts line 343–345). Without an
        // override, a rule that only mentions an unknown metric would
        // resolve to 'pass' — exactly the silent-pass trap Codex flagged.
        // Promote 'pass' to 'flaky' in that case so the CI surface
        // signals the gate's lack of confidence.
        const rawResult = (0, baselineDiffer_1.evaluateRegressionGate)(baselineId, diff, rules, {
            gateId,
        });
        const finalResult = ruleHasUnknownMetric && rawResult.status === 'pass'
            ? { ...rawResult, status: 'flaky' }
            : rawResult;
        const runId = makeRunId();
        const authSubject = subjectFromReq(req);
        const record = {
            ...(0, sparkContracts_1.makeSparkProvenance)({ source: 'ciGateRoutes.gateEval' }),
            schemaVersion: 1,
            runId,
            gateId,
            baselineId,
            baselineStatus: baseline.status,
            ciSource,
            ...(ciContext ? { ciContext } : {}),
            ...(authSubject ? { authSubject } : {}),
            candidateSnapshot: candidate.snapshot,
            rulesSnapshot: rules,
            result: finalResult,
            createdAt: Date.now(),
        };
        runStore.recordRun(record);
        return res.status(200).json({ success: true, runId, result: finalResult });
    });
    router.get('/gate-runs/:runId', (req, res) => {
        const runStore = resolveRunStore();
        const run = runStore.getRun(req.params.runId);
        if (!run) {
            return res.status(404).json({
                success: false,
                error: `Gate run '${req.params.runId}' not found`,
            });
        }
        return res.json({ success: true, run });
    });
    router.get('/gate-runs', (req, res) => {
        const runStore = resolveRunStore();
        const { gateId, status, ciSource, limit } = req.query;
        const runs = runStore.listRuns({
            gateId,
            status: status,
            ciSource,
            limit: limit ? Number(limit) : undefined,
        });
        return res.json({ success: true, runs, count: runs.length });
    });
    return router;
}
function makeRunId() {
    return `gate-run-${crypto.randomUUID()}`;
}
function stringField(value) {
    return typeof value === 'string' && value.trim() !== ''
        ? value.trim()
        : undefined;
}
function parseRules(value) {
    if (!Array.isArray(value) || value.length === 0)
        return undefined;
    const parsed = [];
    for (const raw of value) {
        if (!raw || typeof raw !== 'object')
            return undefined;
        const r = raw;
        const metricId = stringField(r.metricId);
        const threshold = typeof r.threshold === 'number' ? r.threshold : NaN;
        if (!metricId || !Number.isFinite(threshold))
            return undefined;
        const rule = { metricId, threshold };
        if (typeof r.expectIncrease === 'boolean') {
            rule.expectIncrease = r.expectIncrease;
        }
        parsed.push(rule);
    }
    return parsed;
}
function parseCandidate(value, baselineStore, storageScope) {
    if (!value || typeof value !== 'object') {
        return { error: 'candidate is required' };
    }
    const c = value;
    if (c.kind === 'trace') {
        if (!Array.isArray(c.metrics)) {
            return { error: "candidate.metrics is required for kind='trace'" };
        }
        const metrics = c.metrics;
        const traceId = stringField(c.traceId);
        const sampleCount = typeof c.sampleCount === 'number' ? c.sampleCount : undefined;
        return {
            candidate: {
                snapshot: {
                    kind: 'trace',
                    metrics,
                    ...(traceId ? { traceId } : {}),
                    ...(sampleCount !== undefined ? { sampleCount } : {}),
                },
            },
        };
    }
    if (c.kind === 'baseline') {
        const candidateBaselineId = stringField(c.baselineId);
        if (!candidateBaselineId) {
            return { error: "candidate.baselineId is required for kind='baseline'" };
        }
        const candidateBaseline = baselineStore.getBaseline(candidateBaselineId, storageScope);
        if (!candidateBaseline) {
            return {
                error: `candidate baseline '${candidateBaselineId}' not found`,
            };
        }
        return {
            candidate: {
                snapshot: {
                    kind: 'baseline',
                    baselineId: candidateBaselineId,
                    metrics: candidateBaseline.metrics,
                    sampleCount: candidateBaseline.sampleCount,
                },
                baselineRecord: candidateBaseline,
            },
        };
    }
    return { error: "candidate.kind must be 'baseline' or 'trace'" };
}
function parseCiContext(value) {
    if (!value || typeof value !== 'object')
        return undefined;
    const c = value;
    const out = {};
    for (const key of ['repo', 'commit', 'workflow', 'runUrl']) {
        const v = stringField(c[key]);
        if (v)
            out[key] = v;
    }
    if (typeof c.pr === 'number' && Number.isFinite(c.pr)) {
        out.pr = c.pr;
    }
    return Object.keys(out).length === 0
        ? undefined
        : out;
}
function subjectFromReq(req) {
    const user = req.user;
    return user?.id;
}
function makeSkippedRun(input) {
    return {
        ...(0, sparkContracts_1.makeSparkProvenance)({ source: 'ciGateRoutes.skipped' }),
        schemaVersion: 1,
        runId: input.runId,
        gateId: input.gateId,
        baselineId: input.baselineId,
        baselineStatus: input.baselineStatus,
        ciSource: input.ciSource,
        ...(input.ciContext ? { ciContext: input.ciContext } : {}),
        ...(input.authSubject ? { authSubject: input.authSubject } : {}),
        candidateSnapshot: input.candidateSnapshot,
        rulesSnapshot: input.rulesSnapshot,
        result: {
            ...(0, sparkContracts_1.makeSparkProvenance)({ source: 'ciGateRoutes.skipped.gate' }),
            gateId: input.gateId,
            baselineId: input.baselineId,
            status: 'skipped',
            skipReason: input.skipReason,
        },
        skipReason: input.skipReason,
        createdAt: Date.now(),
    };
}
const ciGateRoutes = createCiGateRoutes();
exports.default = ciGateRoutes;
//# sourceMappingURL=ciGateRoutes.js.map