aiax-validate / tree / src/validate.js ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
import { readFileSync } from 'node:fs';
import { check } from './schema.js';

const load = (f) => JSON.parse(readFileSync(new URL(`./schemas/${f}`, import.meta.url), 'utf8'));
export const MANIFEST_SCHEMA = load('manifest.schema.json');
export const SECTION_SCHEMA = load('section.schema.json');
export const MANIFEST_PATHS = ['/.well-known/aiax.json', '/aiax/manifest.json'];
/** Score: 100 with no error and no warning; each error costs 15 points, each warning 2; never below 0. */
export const ERROR_COST = 15;
export const WARNING_COST = 2;
export const estTokens = (text) => Math.ceil(text.length / 4);

async function getJson(fetchFn, url) {
  let res;
  try { res = await fetchFn(url, { headers: { accept: 'application/json' }, redirect: 'follow' }); }
  catch (e) { return { error: `could not be fetched: ${e?.message ?? e}` }; }
  if (!res || !res.ok) return { error: `could not be fetched: HTTP ${res?.status ?? 'error'}`, status: res?.status };
  const text = await res.text();
  try { return { json: JSON.parse(text), text }; } catch { return { error: 'is not JSON', text }; }
}

export async function validateSite(url, opts = {}) {
  const fetchFn = opts.fetch ?? globalThis.fetch;
  const base = new URL(/^[a-z]+:\/\//i.test(url) ? url : `https://${url}`);
  const errors = [], warnings = [], sections = [];
  let manifestUrl = null, manifest = null, mtext = '';
  const tried = [];
  for (const p of MANIFEST_PATHS) {
    const u = new URL(p, base).toString();
    const r = await getJson(fetchFn, u);
    if (r.json !== undefined) { manifestUrl = u; manifest = r.json; mtext = r.text; break; }
    tried.push(`${u} ${r.error}`);
    if (r.error === 'is not JSON') { manifestUrl = u; errors.push({ url: u, path: '', message: 'the manifest is not JSON' }); break; }
  }
  if (!manifestUrl) errors.push({ url: new URL(MANIFEST_PATHS[0], base).toString(), path: '', message: `no AIAX manifest found: ${tried.join('; ')}` });
  if (manifest !== null) {
    for (const e of check(MANIFEST_SCHEMA, manifest)) errors.push({ url: manifestUrl, ...e });
    if (estTokens(mtext) > 600) warnings.push({ url: manifestUrl, path: '', message: `the manifest is about ${estTokens(mtext)} tokens; AIAX targets 100 to 300` });
    const eps = Array.isArray(manifest.entry_points) ? manifest.entry_points : [];
    const ids = new Set();
    for (const [i, ep] of eps.entries()) {
      if (!ep || typeof ep !== 'object' || typeof ep.url !== 'string') continue;
      if (ids.has(ep.id)) errors.push({ url: manifestUrl, path: `/entry_points/${i}/id`, message: `entry_points.${i}.id ${JSON.stringify(ep.id)} is repeated` });
      ids.add(ep.id);
      let su;
      try { su = new URL(ep.url, manifestUrl).toString(); } catch { continue; }
      const r = await getJson(fetchFn, su);
      const sec = { id: ep.id, url: su, ok: true, errors: [] };
      const err = (path, message) => { const e = { url: su, path, message }; sec.errors.push(e); errors.push(e); };
      if (r.json === undefined) err('', `the section ${JSON.stringify(ep.id)} ${r.error}`);
      else {
        for (const e of check(SECTION_SCHEMA, r.json)) err(e.path, e.message);
        if (r.json && typeof r.json === 'object' && 'section_id' in r.json && r.json.section_id !== ep.id) err('/section_id', `section_id ${JSON.stringify(r.json.section_id)} differs from its entry point id ${JSON.stringify(ep.id)}`);
        const real = estTokens(r.text);
        if (Number.isInteger(ep.est_tokens) && (real > ep.est_tokens * 2 || real * 2 < ep.est_tokens)) warnings.push({ url: manifestUrl, path: `/entry_points/${i}/est_tokens`, message: `entry_points.${i}.est_tokens says ${ep.est_tokens}, the section is about ${real} tokens` });
      }
      sec.ok = sec.errors.length === 0;
      sections.push(sec);
    }
  }
  const score = Math.max(0, 100 - errors.length * ERROR_COST - warnings.length * WARNING_COST);
  return { url: base.toString(), manifest_url: manifestUrl, ok: errors.length === 0, score, errors, warnings, sections };
}

/** A fetch that answers from files: the URL path under dir (dir/.well-known/aiax.json); a missing file is a 404. */
export function fixtureFetch(dir) {
  return async (url) => {
    const { pathname } = new URL(String(url));
    if (pathname.includes('..')) return new Response('bad path', { status: 400 });
    try { return new Response(readFileSync(`${dir.replace(/\/$/, '')}${decodeURIComponent(pathname)}`), { status: 200 }); }
    catch { return new Response('not found', { status: 404 }); }
  };
}

agentgate-source: code mirror on Cloudflare Pages. All repos