"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.BaselineStore = exports.BASELINE_PUBLISH_MIN_SAMPLES = void 0;
exports.keyHasIdentifiableInfo = keyHasIdentifiableInfo;
exports.deriveBaselineId = deriveBaselineId;
/**
 * BaselineStore — durable App/Device/Build/CUJ baseline storage with
 * curation lifecycle and publish gates.
 *
 * Plan 50 M0 scope (this file):
 * - Add / get / remove / list `BaselineRecord` with JSON persistence
 *   (atomic temp+rename, schemaVersion 1).
 * - Service-layer invariants the schema does NOT enforce so older
 *   snapshots remain readable:
 *     - `status='published'` requires `sampleCount >= 3` (§4.1).
 *     - `status='published'` requires `redactionState='redacted'` whenever
 *       the key carries identifiable info (raw appId / deviceId);
 *       pre-anonymized keys can publish with `redactionState='raw'`.
 * - Stable canonical `baselineId` derivation:
 *   `${appId}/${deviceId}/${buildId}/${cuj}`.
 *
 * Out of scope here (M1 / M2):
 * - Diff and regression-gate computation (`baselineDiffer.ts`).
 * - MCP tool registration (`lookup_baseline`, `compare_to_baseline`).
 * - Express CRUD route.
 *
 * @module baselineStore
 */
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const scopedKnowledgeStore_1 = require("./scopedKnowledgeStore");
/** Minimum sample count enforced for `status='published'`. */
exports.BASELINE_PUBLISH_MIN_SAMPLES = 3;
const KNOWLEDGE_KIND = 'baseline';
const BASELINE_ROW_SCOPE = 'baseline';
/** Normalize an appId or deviceId to detect when it carries
 * identifiable raw info. Heuristic: package-style ids
 * (`com.example.feed`) or model+os fingerprints (`pixel-9-android-15`)
 * count as identifiable; placeholder strings starting with `anon-` or
 * `redacted-` count as already anonymized. The gate is intentionally
 * conservative — a false positive (re-redacting an already anonymized
 * key) is harmless; a false negative would leak. */
function isIdentifiableComponent(value) {
    const v = value.trim().toLowerCase();
    if (!v)
        return false;
    if (v.startsWith('anon-'))
        return false;
    if (v.startsWith('redacted-'))
        return false;
    if (v.startsWith('placeholder-'))
        return false;
    return true;
}
/** Whether the key looks like it carries raw identifiable info. */
function keyHasIdentifiableInfo(key) {
    return (isIdentifiableComponent(key.appId) ||
        isIdentifiableComponent(key.deviceId));
}
/** Derive the canonical baselineId from a key. */
function deriveBaselineId(key) {
    return `${key.appId}/${key.deviceId}/${key.buildId}/${key.cuj}`;
}
/**
 * BaselineStore — local file-backed baseline storage. Single instance
 * per storage path is enough for M0; cross-process writers are not
 * supported (caller's responsibility).
 */
class BaselineStore {
    constructor(storagePath) {
        this.baselines = new Map();
        this.loaded = false;
        this.storagePath = storagePath;
    }
    /** Idempotently load the on-disk store into memory. */
    load() {
        if (this.loaded)
            return;
        this.loaded = true;
        if (!fs.existsSync(this.storagePath))
            return;
        try {
            const raw = fs.readFileSync(this.storagePath, 'utf-8');
            const parsed = JSON.parse(raw);
            if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.baselines)) {
                return;
            }
            for (const b of parsed.baselines) {
                this.baselines.set(b.baselineId, b);
            }
        }
        catch {
            // Corrupted JSON: empty cache, file preserved for inspection.
        }
    }
    /**
     * Add or replace a baseline record. Validates the publish invariants
     * before writing — invalid records throw rather than silently
     * downgrading status, so the operator sees the failure.
     */
    addBaseline(record, scope) {
        this.load();
        this.assertPublishInvariants(record);
        if ((0, scopedKnowledgeStore_1.legacyKnowledgeFilesystemWritesEnabled)()) {
            this.baselines.set(record.baselineId, record);
            this.persist();
        }
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeDbWritesEnabled)()) {
            (0, scopedKnowledgeStore_1.upsertScopedKnowledgeRecord)(KNOWLEDGE_KIND, record.baselineId, BASELINE_ROW_SCOPE, record, scope, { createdAt: record.capturedAt, updatedAt: Date.now() });
        }
    }
    /** Get a baseline by id. */
    getBaseline(baselineId, scope) {
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeStoreEnabled)()) {
            return (0, scopedKnowledgeStore_1.getScopedKnowledgeRecord)(KNOWLEDGE_KIND, baselineId, scope)?.record;
        }
        this.load();
        return this.baselines.get(baselineId);
    }
    /** Remove a baseline. Returns whether anything was actually removed. */
    removeBaseline(baselineId, scope) {
        let removed = false;
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeDbWritesEnabled)()) {
            removed = (0, scopedKnowledgeStore_1.removeScopedKnowledgeRecord)(KNOWLEDGE_KIND, baselineId, scope) || removed;
        }
        if ((0, scopedKnowledgeStore_1.legacyKnowledgeFilesystemWritesEnabled)()) {
            this.load();
            const had = this.baselines.delete(baselineId);
            if (had)
                this.persist();
            removed = had || removed;
        }
        return removed;
    }
    /**
     * List baselines with optional filters. Results are stable-ordered
     * by baselineId for deterministic consumers.
     */
    listBaselines(opts = {}, scope) {
        this.load();
        let out = (0, scopedKnowledgeStore_1.enterpriseKnowledgeStoreEnabled)()
            ? (0, scopedKnowledgeStore_1.listScopedKnowledgeRecords)(KNOWLEDGE_KIND, scope, { rowScope: BASELINE_ROW_SCOPE }).map(row => row.record)
            : Array.from(this.baselines.values());
        if (opts.status) {
            out = out.filter(b => b.status === opts.status);
        }
        if (opts.keyPrefix) {
            const prefix = opts.keyPrefix;
            out = out.filter(b => b.baselineId.startsWith(prefix));
        }
        out.sort((a, b) => a.baselineId.localeCompare(b.baselineId));
        return out;
    }
    /**
     * Validate the §4.1 publish invariants. Throws when violated so the
     * caller surfaces the cause to the operator instead of silently
     * downgrading the record's status.
     */
    assertPublishInvariants(record) {
        if (record.status !== 'published')
            return;
        const sampleCount = record.sampleCount ?? 0;
        if (sampleCount < exports.BASELINE_PUBLISH_MIN_SAMPLES) {
            throw new Error(`Baseline '${record.baselineId}' cannot be published with sampleCount=${sampleCount} (minimum ${exports.BASELINE_PUBLISH_MIN_SAMPLES})`);
        }
        if (keyHasIdentifiableInfo(record.key) &&
            record.redactionState !== 'redacted') {
            throw new Error(`Baseline '${record.baselineId}' cannot be published with redactionState='${record.redactionState}' — key carries identifiable info`);
        }
    }
    /** Atomic write: temp file + rename so a crashed process leaves the
     * existing on-disk file intact. */
    persist() {
        const dir = path.dirname(this.storagePath);
        if (!fs.existsSync(dir))
            fs.mkdirSync(dir, { recursive: true });
        // Per-process unique tmp suffix — Codex round E P1#5.
        const tmp = `${this.storagePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
        const envelope = {
            schemaVersion: 1,
            baselines: Array.from(this.baselines.values()),
        };
        fs.writeFileSync(tmp, JSON.stringify(envelope, null, 2), 'utf-8');
        fs.renameSync(tmp, this.storagePath);
    }
}
exports.BaselineStore = BaselineStore;
//# sourceMappingURL=baselineStore.js.map