Source: database/repositories/metricSamplesRepo.js

/**
 * @module database/repositories/metricSamplesRepo
 * @description Repository for the generic time-series `metric_samples` table
 * (MET-001). Backs every "value over time per project" surface — healing
 * savings (CAP-004), Web Vitals trends (AUTO-017.3), flaky-rate (DIF-004),
 * accessibility violations (AUTO-016) — so each consumer doesn't reinvent
 * its own aggregation table.
 *
 * Schema (migration `016_metric_samples.sql`):
 *   metric_samples(id, projectId, metricKey, ts, value, tags, createdAt)
 *   indexed on (projectId, metricKey, ts)
 *
 * `tags` is JSON-serialised on write and parsed on read so callers can
 * attach structured context (e.g. `{ testId, strategy }`) without a
 * separate join table.
 */

import { getDatabase } from "../sqlite.js";

/**
 * Insert a single time-series sample.
 *
 * @param {Object} sample
 * @param {string} sample.projectId
 * @param {string} sample.metricKey - Stable metric identifier (e.g. `"healing.savings"`, `"webVitals.lcp"`).
 * @param {number} [sample.ts=Date.now()] - Sample timestamp, epoch ms.
 * @param {number} sample.value - Numeric sample value (must be a finite number — validate at the call site or use `recordMetric`).
 * @param {Object|null} [sample.tags=null] - Optional structured context; JSON-serialised on write.
 */
export function insertSample({ projectId, metricKey, ts = Date.now(), value, tags = null }) {
  const db = getDatabase();
  db.prepare(`INSERT INTO metric_samples (projectId, metricKey, ts, value, tags) VALUES (?, ?, ?, ?, ?)`)
    .run(projectId, metricKey, ts, value, tags ? JSON.stringify(tags) : null);
}

/**
 * Read a project's samples for a metric, ordered ascending by timestamp.
 *
 * @param {string} projectId
 * @param {string} metricKey
 * @param {Object} [opts]
 * @param {number} [opts.since=0] - Lower-bound timestamp (epoch ms, inclusive). Default `0` returns all samples.
 * @param {number} [opts.limit=200] - Row cap; oldest-first within the window.
 * @returns {Array<{ts: number, value: number, tags: (Object|null)}>}
 */
export function getSeries(projectId, metricKey, { since = 0, limit = 200 } = {}) {
  const db = getDatabase();
  const rows = db.prepare(`SELECT ts, value, tags FROM metric_samples WHERE projectId = ? AND metricKey = ? AND ts >= ? ORDER BY ts ASC LIMIT ?`)
    .all(projectId, metricKey, since, limit);
  return rows.map(r => ({ ...r, tags: r.tags ? JSON.parse(r.tags) : null }));
}