#!/usr/bin/env node
// Aggregate overlay-load telemetry. Cross-references the objective read log
// (.baldart/telemetry/overlay-loads.jsonl, written by the overlay-telemetry
// PostToolUse hook) against the overlays that actually EXIST on disk
// (.baldart/overlays/), and reports per-overlay load counts.
//
// The headline signal is the LAST column: a SKILL overlay that exists but is
// read ~never is the alarm — runtime concatenation is not structurally
// enforced, so a never-read skill overlay is knowledge the project authored but
// the agent isn't picking up. (Agent/command overlays are compile-time merged
// into the generated file, so they're NOT expected to be read at runtime — a
// zero count there is normal, not a problem. The table marks them accordingly.)
//
// This is a CONSERVATIVE proxy. The hook can only see explicit `Read` calls, so
// it UNDER-counts: overlay content already in context (no Read) and reads done
// in ways the main-loop hook doesn't observe are invisible. A low count is a
// reason to investigate, not proof of a miss. See framework/docs/OVERLAY-TELEMETRY.md.
//
// Usage:
//   node framework/scripts/analyze-overlay-loads.js
//   node framework/scripts/analyze-overlay-loads.js --log <path.jsonl> --overlays <dir>

'use strict';
const fs = require('fs');
const path = require('path');

function argVal(flag, fallback) {
  const i = process.argv.indexOf(flag);
  return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
}

const LOG_PATH = argVal('--log', path.join('.baldart', 'telemetry', 'overlay-loads.jsonl'));
const OVERLAYS_DIR = argVal('--overlays', path.join('.baldart', 'overlays'));

// --- read the objective read log -------------------------------------------

const reads = new Map(); // "kind/name" → { count, sessions:Set, first, last }
let logLines = 0;
let logBad = 0;

if (fs.existsSync(LOG_PATH)) {
  const raw = fs.readFileSync(LOG_PATH, 'utf8');
  for (const line of raw.split(/\r?\n/)) {
    if (!line.trim()) continue;
    logLines += 1;
    let row;
    try { row = JSON.parse(line); } catch (_) { logBad += 1; continue; }
    if (!row || row.event !== 'overlay-read' || !row.kind || !row.name) { logBad += 1; continue; }
    const key = `${row.kind}/${row.name}`;
    const e = reads.get(key) || { count: 0, sessions: new Set(), first: null, last: null };
    e.count += 1;
    if (row.session_id) e.sessions.add(row.session_id);
    if (row.ts) {
      if (!e.first || row.ts < e.first) e.first = row.ts;
      if (!e.last || row.ts > e.last) e.last = row.ts;
    }
    reads.set(key, e);
  }
}

// --- enumerate overlays that exist on disk ---------------------------------

function listOverlays(dir) {
  const out = []; // { kind, name, key }
  if (!fs.existsSync(dir)) return out;
  const add = (sub, kind) => {
    const d = sub ? path.join(dir, sub) : dir;
    if (!fs.existsSync(d)) return;
    for (const f of fs.readdirSync(d)) {
      if (!f.endsWith('.md') || f.endsWith('.example.md') || f === 'README.md') continue;
      const name = f.replace(/\.md$/, '');
      out.push({ kind, name, key: `${kind}/${name}` });
    }
  };
  add('', 'skill');
  add('agents', 'agent');
  add('commands', 'command');
  return out;
}

const overlays = listOverlays(OVERLAYS_DIR);
const overlayKeys = new Set(overlays.map((o) => o.key));

// --- build rows -------------------------------------------------------------

const rows = overlays.map((o) => {
  const r = reads.get(o.key);
  return {
    kind: o.kind,
    name: o.name,
    reads: r ? r.count : 0,
    sessions: r ? r.sessions.size : 0,
    last: r && r.last ? r.last.slice(0, 10) : '-',
    // Only skills are expected to be read at runtime; agent/command overlays are
    // fused at build time, so a zero there is expected, not a finding.
    flag: (o.kind === 'skill' && (!r || r.count === 0)) ? 'NEVER-LOADED' : '',
  };
});
rows.sort((a, b) => a.reads - b.reads || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));

// Reads of overlays that no longer exist on disk (stale/renamed) — surfaced separately.
const orphanReads = [...reads.keys()].filter((k) => !overlayKeys.has(k));

// --- render -----------------------------------------------------------------

const headers = ['Kind', 'Name', 'Reads', 'Sessions', 'Last-read', 'Flag'];
const cols = ['kind', 'name', 'reads', 'sessions', 'last', 'flag'];
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[cols[i]]).length), 1));
const fmt = (cells) => cells.map((c, i) => String(c).padEnd(widths[i])).join('  ');

console.log(fmt(headers));
console.log(widths.map((w) => '-'.repeat(w)).join('  '));
for (const r of rows) console.log(fmt(cols.map((c) => r[c])));

console.log('');
console.log(`Overlays on disk: ${overlays.length}  |  Log rows: ${logLines}${logBad ? ` (${logBad} skipped)` : ''}  |  Log: ${fs.existsSync(LOG_PATH) ? LOG_PATH : 'NOT FOUND (no telemetry yet)'}`);

const neverLoaded = rows.filter((r) => r.flag === 'NEVER-LOADED');
if (neverLoaded.length) {
  console.log('');
  console.log('⚠  Skill overlays that EXIST but were never read (runtime concat may not be loading them):');
  for (const r of neverLoaded) console.log(`   ${r.kind}/${r.name}`);
  console.log('   Conservative proxy — see framework/docs/OVERLAY-TELEMETRY.md before acting (in-context reads are invisible to the hook).');
}

if (orphanReads.length) {
  console.log('');
  console.log('ℹ  Reads logged for overlays no longer on disk (renamed/removed):');
  for (const k of orphanReads) console.log(`   ${k}`);
}
