OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / check-relaykernel-gateway-compat.mjs

273 lines · javascript ref:
#!/usr/bin/env node

import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import {
  BLINDSPARK_HTTP_ROUTE_MATRIX,
  BLINDSPARK_HTTP_SURFACES
} from '../packages/core/core/protocol/relaykernel-profile.js'

const here = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(here, '..')

const usage = `
Usage:
  node scripts/check-relaykernel-gateway-compat.mjs [--json] [--root <repo-root>]

Verifies that the RelayKernel-profile Blindspark HTTP compatibility matrix is
still bound to the concrete Node/Bare/data-plane gateway route handlers. This
is intentionally source-level: it catches route-contract drift before a future
RelayKernel extraction drops PearBrowser's browser bootstrap surfaces.
`

function main () {
  let args
  try {
    args = parseArgs(process.argv.slice(2))
  } catch (err) {
    console.error(err.message)
    console.error(usage.trim())
    process.exit(1)
  }

  if (args.help) {
    console.log(usage.trim())
    return
  }

  const report = checkRelayKernelGatewayCompatibility({
    repoRoot: path.resolve(args.root || repoRoot)
  })

  if (args.json) console.log(JSON.stringify(report, null, 2))
  else console.log(formatReport(report))

  if (report.status !== 'pass') process.exit(1)
}

export function checkRelayKernelGatewayCompatibility ({
  repoRoot = path.resolve(here, '..'),
  matrix = BLINDSPARK_HTTP_ROUTE_MATRIX,
  surfaces = BLINDSPARK_HTTP_SURFACES,
  sourceTextByFile = null
} = {}) {
  const items = []
  const matrixSurfaces = new Set()
  const exportedSurfaces = new Set(surfaces)

  for (const row of Array.isArray(matrix) ? matrix : []) {
    const surface = typeof row?.surface === 'string' ? row.surface : '(missing surface)'
    matrixSurfaces.add(surface)

    if (!exportedSurfaces.has(surface)) {
      items.push(failItem({
        id: `matrix.${surface}`,
        surface,
        summary: 'surface is missing from BLINDSPARK_HTTP_SURFACES',
        detail: 'RelayKernel validation would no longer require this route.'
      }))
    }

    if (!Array.isArray(row?.methods) || !row.methods.includes('GET')) {
      items.push(failItem({
        id: `matrix.${surface}.methods`,
        surface,
        summary: 'surface does not declare GET support',
        detail: 'PearBrowser and browser clients rely on GET bootstrap reads.'
      }))
    }

    if (Array.isArray(row?.capabilities) && row.capabilities.includes('Range') &&
      (!Array.isArray(row?.methods) || !row.methods.includes('HEAD'))) {
      items.push(failItem({
        id: `matrix.${surface}.range-head`,
        surface,
        summary: 'Range-capable surface does not declare HEAD support',
        detail: 'Browser bootstrap probes and range-aware media fetches depend on HEAD staying paired with Range.'
      }))
    }

    const handlers = Array.isArray(row?.handlers) ? row.handlers : []
    if (handlers.length === 0) {
      items.push(failItem({
        id: `matrix.${surface}.handlers`,
        surface,
        summary: 'surface has no concrete route handlers',
        detail: 'Compatibility surfaces must point at at least one implementation file.'
      }))
      continue
    }

    for (const handler of handlers) {
      items.push(checkHandler({ repoRoot, sourceTextByFile, surface, handler }))
    }
  }

  for (const surface of exportedSurfaces) {
    if (!matrixSurfaces.has(surface)) {
      items.push(failItem({
        id: `surfaces.${surface}`,
        surface,
        summary: 'exported surface is missing from route matrix',
        detail: 'BLINDSPARK_HTTP_SURFACES and BLINDSPARK_HTTP_ROUTE_MATRIX must stay in lockstep.'
      }))
    }
  }

  if (!Array.isArray(matrix) || matrix.length === 0) {
    items.push(failItem({
      id: 'matrix.present',
      surface: '(all)',
      summary: 'route matrix is empty or malformed',
      detail: 'RelayKernel gateway compatibility cannot be proven.'
    }))
  }

  const failed = items.filter(item => item.status === 'fail')
  const passed = items.filter(item => item.status === 'pass')

  return {
    schemaVersion: 1,
    kind: 'relaykernel-gateway-compatibility',
    status: failed.length === 0 ? 'pass' : 'fail',
    surfaces: [...exportedSurfaces],
    totals: {
      pass: passed.length,
      fail: failed.length
    },
    items
  }
}

function checkHandler ({ repoRoot, sourceTextByFile, surface, handler }) {
  const runtime = typeof handler?.runtime === 'string' ? handler.runtime : '(missing runtime)'
  const file = typeof handler?.file === 'string' ? handler.file : ''
  const id = `${surface}:${runtime}`
  const requiredTerms = Array.isArray(handler?.requiredTerms) ? handler.requiredTerms : []

  if (!file) {
    return failItem({
      id,
      surface,
      runtime,
      file,
      summary: 'handler file is missing from matrix row',
      detail: 'Each compatibility handler must name its implementation file.'
    })
  }

  const source = readSource({ repoRoot, sourceTextByFile, file })
  if (!source.ok) {
    return failItem({
      id,
      surface,
      runtime,
      file,
      summary: 'handler source is not readable',
      detail: source.error
    })
  }

  const missingTerms = requiredTerms.filter(term => !source.text.includes(term))
  if (missingTerms.length > 0) {
    return failItem({
      id,
      surface,
      runtime,
      file,
      summary: 'handler source is missing required route terms',
      detail: missingTerms.map(term => JSON.stringify(term)).join(', ')
    })
  }

  return {
    id,
    status: 'pass',
    surface,
    runtime,
    file,
    summary: 'handler preserves RelayKernel gateway compatibility',
    requiredTerms
  }
}

function readSource ({ repoRoot, sourceTextByFile, file }) {
  if (sourceTextByFile) {
    if (typeof sourceTextByFile.get === 'function' && sourceTextByFile.has(file)) {
      return { ok: true, text: sourceTextByFile.get(file) }
    }
    if (Object.prototype.hasOwnProperty.call(sourceTextByFile, file)) {
      return { ok: true, text: sourceTextByFile[file] }
    }
  }

  const abs = path.resolve(repoRoot, file)
  try {
    const stat = fs.lstatSync(abs)
    if (stat.isSymbolicLink()) return { ok: false, error: 'handler source must not be a symlink' }
    if (!stat.isFile()) return { ok: false, error: 'handler source is not a regular file' }
    return { ok: true, text: fs.readFileSync(abs, 'utf8') }
  } catch (err) {
    return { ok: false, error: err.message }
  }
}

function failItem ({ id, surface, runtime = null, file = null, summary, detail }) {
  return {
    id,
    status: 'fail',
    surface,
    runtime,
    file,
    summary,
    detail
  }
}

function formatReport (report) {
  const lines = [
    'RelayKernel gateway compatibility check',
    `status=${report.status}`,
    `surfaces=${report.surfaces.join(', ')}`,
    `pass=${report.totals.pass} fail=${report.totals.fail}`
  ]

  for (const item of report.items) {
    const location = [item.surface, item.runtime, item.file].filter(Boolean).join(' ')
    lines.push(`${item.status.toUpperCase()} ${location} — ${item.summary}`)
    if (item.status === 'fail') lines.push(`  ${item.detail}`)
  }

  return lines.join('\n')
}

function parseArgs (argv) {
  const out = {}
  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i]
    if (arg === '--help' || arg === '-h') {
      out.help = true
      continue
    }
    if (arg === '--json') {
      out.json = true
      continue
    }
    if (arg === '--root') {
      out.root = readValue(argv, ++i, arg)
      continue
    }
    throw new Error(`Unknown argument: ${arg}`)
  }
  return out
}

function readValue (argv, index, flag) {
  const value = argv[index]
  if (!value || value.startsWith('--')) throw new Error(`Missing value for ${flag}`)
  return value
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) main()