#!/usr/bin/env node
// Build-time gate: rejects `(Task NNN)` citations in operator-visible production
// strings. Citations are allowed in inline comments and test titles/bodies only.
//
// Live repro that motivated the gate: a setup script on realagent 192.168.88.8
// emitted `Re-run the installer to provision Chromium.` to the
// operator's chat surface — the citation made the operator chase task
// history rather than execute the verb. Operator surfaces strip citations;
// internal references keep them.
//
// Comment-skip is line-leading-character based, not AST. The repo's actual
// comment forms are: `#` (shell), `//` (TS/JS line), `*`/`/*`/`/**` (JSDoc
// continuation, block, doc), and `{/*` (JSX). Any line whose first non-
// whitespace token is one of these is skipped. Test files (`__tests__/` and
// `*.test.{ts,tsx,js}`) are skipped wholesale because the brief's out-of-scope
// rule covers test titles, and test bodies that assert on citations are
// equivalent to titles for this gate's purpose.

import { readFileSync, readdirSync, statSync } from 'node:fs'
import { dirname, join, relative, sep, basename } from 'node:path'
import { fileURLToPath } from 'node:url'

const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
const REPO_ROOT = join(SCRIPT_DIR, '..', '..')

const SCAN_ROOTS = [
  join(REPO_ROOT, 'platform', 'scripts'),
  join(REPO_ROOT, 'platform', 'plugins'),
  join(REPO_ROOT, 'platform', 'ui', 'server'),
  join(REPO_ROOT, 'platform', 'ui', 'app'),
  join(REPO_ROOT, 'packages', 'create-maxy', 'src'),
]

const ALLOWED_EXTS = new Set(['.sh', '.ts', '.tsx', '.js', '.mjs', '.cjs'])
const SKIP_DIR_NAMES = new Set([
  'node_modules', 'dist', 'build', '.next', 'payload', '__tests__',
])
const TASK_ID_RE = /\(Task \d+\)/

function isCommentLine(line) {
  const trimmed = line.replace(/^\s+/, '')
  if (trimmed.startsWith('#')) return true
  if (trimmed.startsWith('//')) return true
  if (trimmed.startsWith('/*')) return true
  if (trimmed.startsWith('*')) return true
  if (trimmed.startsWith('{/*')) return true
  return false
}

function shouldScanFile(filePath) {
  const name = basename(filePath)
  if (name.endsWith('.test.ts')) return false
  if (name.endsWith('.test.tsx')) return false
  if (name.endsWith('.test.js')) return false
  const ext = name.includes('.') ? '.' + name.split('.').pop() : ''
  return ALLOWED_EXTS.has(ext)
}

function* walk(root) {
  let entries
  try {
    entries = readdirSync(root)
  } catch {
    return
  }
  for (const name of entries) {
    if (SKIP_DIR_NAMES.has(name)) continue
    const full = join(root, name)
    let st
    try { st = statSync(full) } catch { continue }
    if (st.isDirectory()) {
      yield* walk(full)
    } else if (st.isFile()) {
      yield full
    }
  }
}

const leaks = []

for (const root of SCAN_ROOTS) {
  for (const file of walk(root)) {
    if (!shouldScanFile(file)) continue
    let content
    try {
      content = readFileSync(file, 'utf-8')
    } catch {
      continue
    }
    const lines = content.split('\n')
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i]
      if (!TASK_ID_RE.test(line)) continue
      if (isCommentLine(line)) continue
      leaks.push({ file: relative(REPO_ROOT, file), line: i + 1, content: line.trim() })
    }
  }
}

if (leaks.length > 0) {
  console.error(`check-no-task-id-leaks: ${leaks.length} operator-visible (Task NNN) citation(s) found:`)
  for (const leak of leaks) {
    console.error(`  ${leak.file}:${leak.line}: ${leak.content}`)
  }
  console.error('')
  console.error('Citations belong in inline comments only, never in operator-visible strings')
  console.error('(stderr/stdout, log files surfaced via logs-read.sh, systemd unit')
  console.error('--description=, error messages, console.log/warn/error, throw new Error).')
  process.exit(1)
}
