Ctrl K
ComunidadGuías…

scripts/check-resource-schemes.ts

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bun
/**
 * Lint: every URI scheme this runtime resolves itself is named in
 * `RESERVED_RESOURCE_SCHEMES` (`src/tools/resource-schemes.ts`).
 *
 * That set is what `parseNotificationsDeclaration` refuses an outbox against,
 * so a scheme missing from it is a scheme a server may claim for an outbox
 * while the host also resolves it — one URI carrying two meanings, decided by
 * whichever reader gets there first. The set has to be **complete** or the
 * refusal is decorative, and completeness is exactly the property a doc comment
 * cannot hold.
 *
 * The rule is not "these schemes are forbidden" — it is "a scheme appearing in
 * `src/` has been classified". Every hit resolves to one of two lists, and
 * adding either entry is the decision this lint exists to force:
 *
 *   - {@link RESERVED_RESOURCE_SCHEMES} — the host resolves it, so an outbox
 *     may not land on it.
 *   - {@link NOT_HOST_RESOLVED} below — the runtime only ever *speaks* it
 *     outward (fetching a URL, say). Nothing in the host answers it, so it
 *     collides with nothing.
 *
 * What it scans, and why both shapes are needed: string and template literals
 * carrying `<scheme>://` catch a scheme written out (`"artifact://"`,
 * `` `skill://x` ``), and a `*_URI_SCHEME` constant catches one whose
 * only literal spelling is the bare name (`FILE_URI_SCHEME = "files"`, whose
 * `files://` form exists only as a template). Checking one shape alone misses
 * half the population: a scheme spelled only through a constant is invisible to
 * a literal scan, and one spelled only as a literal to a constant scan.
 *
 * Scope: `src/**\/*.ts`, minus vendored trees. Comments are not scanned — the
 * AST walk sees literals only, so prose naming a scheme is free. Tests live
 * outside `src/` and are out of scope; a fixture naming an unclassified scheme
 * is the point of a fixture. `node_modules` under each platform app's `ui/` is
 * skipped for the same reason the sibling lints skip it: it is gitignored,
 * local-only, and full of other people's URI grammars.
 */

import { readFileSync } from "node:fs";
import { join, relative } from "node:path";
import { Glob } from "bun";
import * as ts from "typescript";
import { RESERVED_RESOURCE_SCHEMES } from "../src/tools/resource-schemes.ts";

const ROOT = join(import.meta.dirname ?? __dirname, "..");
const SRC_ROOT = join(ROOT, "src");

/**
 * Schemes the runtime only ever speaks outward — it fetches these, nothing in
 * the host answers them, so no declared resource can collide with one.
 *
 * A scheme belongs here only when the host resolves *nothing* at it. If the
 * host would answer the URI, it is reserved instead, and putting it here to
 * quiet the lint reopens the collision the refusal exists to close.
 */
const NOT_HOST_RESOLVED = new Set(["http", "https"]);

/** `<scheme>://`, anchored so a `//` inside a path can't be read as the start of one. */
const SCHEME_IN_URI_RE = /\b([a-zA-Z][a-zA-Z0-9+.-]*):\/\//g;

/** A constant naming a scheme by its bare name: `const FILE_URI_SCHEME = "files"`. */
const SCHEME_CONST_RE = /_URI_SCHEME$/;

interface Violation {
  file: string;
  line: number;
  scheme: string;
  snippet: string;
}

const classified = new Set<string>([
  ...(RESERVED_RESOURCE_SCHEMES as readonly string[]),
  ...NOT_HOST_RESOLVED,
]);

const violations: Violation[] = [];

/** Record `scheme` when it is in neither list, attributing it to `node`'s line. */
function classify(scheme: string, node: ts.Node, sf: ts.SourceFile, snippet: string) {
  if (classified.has(scheme.toLowerCase())) return;
  const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
  violations.push({
    file: relative(ROOT, sf.fileName),
    line: line + 1,
    scheme,
    snippet: snippet.trim().slice(0, 120),
  });
}

/** Walk one file's literals and scheme constants. */
function scanFile(fileName: string) {
  const sf = ts.createSourceFile(
    fileName,
    readFileSync(fileName, "utf-8"),
    ts.ScriptTarget.Latest,
    true,
  );

  const visit = (node: ts.Node) => {
    // Shape 1: a scheme spelled inside a literal — "artifact://x", `skill://x`.
    if (
      ts.isStringLiteral(node) ||
      ts.isNoSubstitutionTemplateLiteral(node) ||
      ts.isTemplateHead(node) ||
      ts.isTemplateMiddle(node) ||
      ts.isTemplateTail(node)
    ) {
      for (const m of node.text.matchAll(SCHEME_IN_URI_RE)) {
        classify(m[1]!, node, sf, node.text);
      }
    }

    // Shape 2: a scheme held as a bare name — `const FILE_URI_SCHEME = "files"`.
    if (
      ts.isVariableDeclaration(node) &&
      ts.isIdentifier(node.name) &&
      SCHEME_CONST_RE.test(node.name.text) &&
      node.initializer &&
      ts.isStringLiteral(node.initializer)
    ) {
      classify(node.initializer.text, node, sf, node.getText(sf));
    }

    ts.forEachChild(node, visit);
  };

  visit(sf);
}

function main() {
  let scanned = 0;
  for (const rel of new Glob("**/*.ts").scanSync(SRC_ROOT)) {
    if (rel.split(/[\\/]/).includes("node_modules")) continue;
    scanFile(join(SRC_ROOT, rel));
    scanned++;
  }

  if (violations.length > 0) {
    console.error(`\n✗ ${violations.length} unclassified URI scheme(s) in src/:\n`);
    for (const v of violations) {
      console.error(`  ${v.file}:${v.line}  ${v.scheme}://`);
      console.error(`    ${v.snippet}\n`);
    }
    console.error(
      "Every scheme in src/ has to be classified, because RESERVED_RESOURCE_SCHEMES is what",
    );
    console.error(
      "a declared outbox is refused against — a scheme missing from it is one a server can",
    );
    console.error("claim while the host also resolves it. Add it to one of:");
    console.error("  - RESERVED_RESOURCE_SCHEMES (src/tools/resource-schemes.ts) — host-resolved.");
    console.error(
      "  - NOT_HOST_RESOLVED (this file) — the runtime only speaks it outward; nothing answers it.",
    );
    process.exit(1);
  }

  console.log(
    `✓ ${classified.size} classified URI schemes, no unclassified spelling across ${scanned} src/ files`,
  );
}

// Gate the scan on direct invocation, matching the sibling check scripts, so
// importing this module never runs it or calls process.exit.
if (import.meta.main) {
  main();
}