Ctrl K
ComunidadGuías…

scripts/check-local-artifacts.cjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
'use strict';

// Local-only artifact policy. Keep test SOURCE (including synthetic fixture helpers)
// and product artwork; never publish generated reviews, captures, or recordings.
// Usage: node scripts/check-local-artifacts.cjs [--index | --range base..tip | --pre-push]
// Pre-push reads Git's four-column ref updates from stdin. A new remote ref scans
// its entire history, deliberately preferring a false positive to an unseen leak.
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');

const raster = /\.(?:png|jpe?g|gif|webp|avif|bmp|tiff?|ico|cur|icns|heic|heif|jxl)$/i;
const recording = /\.(?:har|mp4|m4v|mov|webm|mkv|avi|mpeg|mpg|wmv|flv|ogv|3gp)$/i;
const archive = /\.(?:zip|tar|tgz|gz|bz2|xz|7z|rar|zst|zstd)$/i;
const generatedDir = /(?:^|[-_.])(?:screenshots?|mockups?|mockup-data|captures?|outputs?|artifacts?|evidence|test-results|playwright-report|blob-report|graphify-out|backups)(?:$|[-_.])/i;
const imageAsset = p => /^web\/static\/(?:icons|img)\//.test(p);

function pathReason(p) {
  if (/^docs\/(?:anwendungsreview|screenshots)(?:\/|$)/i.test(p) ||
      /^\.impeccable\/(?:review|critique)(?:\/|$)/i.test(p) ||
      /^\.impeccable\/[^/]+\.html$/i.test(p) ||
      /^docs\/[^/]*20\d{2}[^/]*\.(?:html|css|js)$/i.test(p)) return 'local review';
  if (p.split('/').slice(0, -1).some(part => generatedDir.test(part) || /^(?:\.?tmp|temp|coverage)$/i.test(part))) return 'generated output';
  if (raster.test(p) && !imageAsset(p)) return 'raster capture outside product assets';
  if (recording.test(p) || archive.test(p)) return 'recording or archive';
  return null;
}

function contentReason(b) {
  const hex = b.subarray(0, 12).toString('hex');
  const head = b.subarray(0, 40).toString('latin1');
  if (/^(?:89504e470d0a1a0a|ffd8ff|474946383[79]61|424d|49492a00|4d4d002a|00000100|00000200|ff0a|0000000c4a584c20)/.test(hex) ||
      /^RIFF[\s\S]{4}WEBP/.test(head) || /^.{4}ftyp(?:avif|avis|heic|heix|hevc|hevx|mif1|msf1)/s.test(head)) return 'raster';
  if (/^(?:504b0304|504b0506|504b0708|1f8b|52617221|377abcaf271c|425a68|fd377a585a00|28b52ffd|1a45dfa3)/.test(hex) ||
      /^.{4}ftyp/s.test(head) || /^RIFF[\s\S]{4}AVI /.test(head) ||
      b.subarray(257, 262).toString() === 'ustar') return 'recording or archive';
  // Require payload: source-code URL constructors and documented prefixes are not
  // captures. NUL removal also detects ASCII data URLs in UTF-16 text.
  if (/data:image\/(?:png|apng|jpe?g|gif|webp|avif|bmp|tiff?|x-icon|vnd\.microsoft\.icon|heic|heif|jxl)[^\s"'<>]*,[a-z0-9+/%=]+/i.test(b.toString('utf8').replace(/\0/g, ''))) return 'embedded raster image';
  return null;
}

function git(args, input) {
  const r = spawnSync('git', args, {
    input, maxBuffer: 256 * 1024 * 1024, windowsHide: true,
    env: { ...process.env, GIT_NO_REPLACE_OBJECTS: '1', GIT_NO_LAZY_FETCH: '1' },
  });
  if (r.error || r.status !== 0) throw new Error('Git inspection failed; refusing unchecked content.');
  return r.stdout;
}

function commit(ref) {
  if (!ref || ref.startsWith('-')) throw new Error('Invalid revision.');
  return git(['rev-parse', '--verify', '--end-of-options', ref + '^{commit}']).toString().trim();
}

function entries(raw, index) {
  return raw.toString('utf8').split('\0').filter(Boolean).map(line => {
    const tab = line.indexOf('\t');
    if (tab < 0) throw new Error('Malformed Git listing.');
    const [mode, second, third] = line.slice(0, tab).split(' ');
    if ((index && third !== '0') || mode === '160000') throw new Error('Unmerged index or submodule cannot be inspected safely.');
    if (!['100644', '100755', '120000'].includes(mode) || (!index && second !== 'blob')) throw new Error('Unsupported Git object.');
    return { oid: index ? second : third, path: line.slice(tab + 1) };
  });
}

function scanner() {
  const blobs = new Map();
  const violations = new Map();
  function scan(files) {
    const pending = new Set();
    for (const f of files) {
      const reason = pathReason(f.path);
      if (reason) violations.set(f.path, reason);
      else if (!blobs.has(f.oid)) pending.add(f.oid);
    }
    if (pending.size) {
      const result = git(['cat-file', '--batch'], [...pending].join('\n') + '\n');
      let offset = 0;
      for (const oid of pending) {
        const end = result.indexOf(10, offset);
        const header = result.subarray(offset, end).toString();
        const match = /^([a-f0-9]+) blob (\d+)$/.exec(header);
        if (!match || match[1] !== oid) throw new Error('Missing or unreadable staged/history blob.');
        const size = Number(match[2]);
        offset = end + 1;
        if (!Number.isSafeInteger(size) || offset + size >= result.length || result[offset + size] !== 10) throw new Error('Incomplete blob inspection.');
        blobs.set(oid, contentReason(result.subarray(offset, offset + size)));
        offset += size + 1;
      }
      if (offset !== result.length) throw new Error('Unexpected blob inspection output.');
    }
    for (const f of files) {
      const reason = blobs.get(f.oid);
      if (reason && !(reason === 'raster' && imageAsset(f.path))) violations.set(f.path, reason);
    }
  }
  function history(tip, base) {
    if (git(['rev-parse', '--is-shallow-repository']).toString().trim() !== 'false') throw new Error('Shallow history cannot be checked safely. Fetch complete history first.');
    const args = ['rev-list', tip];
    if (base) args.push('^' + base);
    args.push('--');
    for (const oid of git(args).toString().trim().split('\n').filter(Boolean)) {
      scan(entries(git(['ls-tree', '-rz', '--full-tree', oid]), false));
    }
  }
  return { scan, history, violations };
}

function run(args, stdin = '') {
  const check = scanner();
  if (!args.length || (args.length === 1 && args[0] === '--index')) {
    check.scan(entries(git(['ls-files', '--stage', '-z']), true));
  } else if (args.length === 2 && args[0] === '--range') {
    const range = /^(.+?)\.\.([^.].*)$/.exec(args[1]);
    if (!range || range[1].includes('..') || range[2].includes('..')) throw new Error('Use exactly --range base..tip.');
    check.history(commit(range[2]), commit(range[1]));
  } else if (args.length === 1 && args[0] === '--pre-push') {
    for (const line of stdin.split(/\r?\n/).filter(Boolean)) {
      const parts = line.trim().split(/\s+/);
      if (parts.length !== 4 || !parts[0] || !parts[2] ||
          !parts.slice(1).filter((_, i) => i % 2 === 0).every(s => /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(s))) throw new Error('Malformed pre-push ref input.');
      const [, local, , remote] = parts;
      if (/^0+$/.test(local)) continue; // Deleting a remote ref transmits no new tree.
      check.history(commit(local), /^0+$/.test(remote) ? null : commit(remote));
    }
  } else throw new Error('Usage: check-local-artifacts.cjs [--index | --range base..tip | --pre-push]');
  if (check.violations.size) {
    // JSON escaping makes filenames with tabs/newlines safe; never print blob contents.
    process.stderr.write('Local-only artifacts blocked (including intermediate outgoing commits):\n');
    for (const [p, reason] of [...check.violations].sort()) process.stderr.write('  ' + JSON.stringify(p) + ' (' + reason + ')\n');
    return 1;
  }
  process.stdout.write('Local-artifact guard passed.\n');
  return 0;
}

if (require.main === module) {
  try { process.exitCode = run(process.argv.slice(2), process.argv[2] === '--pre-push' ? fs.readFileSync(0, 'utf8') : ''); }
  catch (error) { process.stderr.write('Local-artifact guard failed closed: ' + error.message + '\n'); process.exitCode = 1; }
}
module.exports = { pathReason, contentReason };