Skip to the content

Capsid menu

src/counts.ts

144 lines and 7 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.

Functions

The functions of counts.ts
FunctionLinesComplexityCoveredCRAPMutation
authoritativeFor25 to 272100%OkCRAP 2No data: mutation testing has not run on this repo yet
quoteAround55 to 593100%OkCRAP 3No data: mutation testing has not run on this repo yet
scanCountClaims61 to 16427100%WatchCRAP 27No data: mutation testing has not run on this repo yet
flag74 to 853100%OkCRAP 3No data: mutation testing has not run on this repo yet
isConsumed90 to 901100%OkCRAP 1No data: mutation testing has not run on this repo yet
<anonymous:90>90 to 902100%OkCRAP 2No data: mutation testing has not run on this repo yet
consume91 to 911100%OkCRAP 1No data: mutation testing has not run on this repo yet

Imports

It imports src/scope.ts. Imported by src/tools/lint.ts.

Source

import { TOOL_GRANTS } from "./scope";

// Authoritative counts for lint. The tool count is derived from TOOL_GRANTS, which
// test/invariants.test.ts and test/tool-grant-descriptions.test.ts hold to the served
// surface. The rest are a cache that test/counts.test.ts derives from the artifact.
// A namespace with no entry is not scanned.
export interface AuthoritativeCounts {
  tools: number;
  liveGates: number;
  htmlEnforcedHeaders: number;
  htmlReportOnlyHeaders: number;
}

export const AUTHORITATIVE: Record<string, AuthoritativeCounts> = {
  capsid: {
    tools: Object.keys(TOOL_GRANTS).length,
    liveGates: 10,
    htmlEnforcedHeaders: 9,
    htmlReportOnlyHeaders: 1,
  },
};

// Object.hasOwn, not `?? null`. AUTHORITATIVE carries Object.prototype, so a bare
// lookup of "constructor" returned the Object function.
export function authoritativeFor(namespace: string): AuthoritativeCounts | null {
  return Object.hasOwn(AUTHORITATIVE, namespace) ? AUTHORITATIVE[namespace] : null;
}

// A four-digit year is never a count.
const YEAR = /^(?:19|20)\d{2}$/;

// Standing-claim types only. Episodics and decisions are history.
const LINTED_TYPES = new Set(["core", "concept", "semantic", "procedural", "spec", "reference", "protocol"]);

// The numbered decision volumes are history too, exempted by path as well as type,
// because a mistyped volume would report every historical count as stale.
const DECISION_VOLUME = /^decisions-vol-\d+\.md$/;

export interface CountClaim {
  path: string;
  type: string;
  noun: string;
  quote: string;
  states: string;
  authoritative: string;
  note?: string;
}

interface ScannableDoc {
  path: string;
  type: string | null;
  body: string | null;
}

function quoteAround(body: string, index: number, length: number): string {
  const start = Math.max(0, index - 40);
  const end = Math.min(body.length, index + length + 40);
  return `${start > 0 ? "..." : ""}${body.slice(start, end).replace(/\s+/g, " ")}${end < body.length ? "..." : ""}`;
}

export function scanCountClaims(docs: ScannableDoc[], namespace: string): CountClaim[] {
  const claims: CountClaim[] = [];
  const authoritative = authoritativeFor(namespace);
  // No authoritative numbers for this namespace means no claims. See AUTHORITATIVE.
  if (!authoritative) return claims;

  for (const doc of docs) {
    const body = doc.body ?? "";
    const type = doc.type ?? "note";
    if (!LINTED_TYPES.has(type)) continue;
    if (doc.path.startsWith("archive/")) continue;
    if (DECISION_VOLUME.test(doc.path)) continue;

    const flag = (noun: string, match: RegExpExecArray, states: string, auth: string, note?: string) => {
      if (states === auth) return;
      claims.push({
        path: doc.path,
        type,
        noun,
        quote: quoteAround(body, match.index, match[0].length),
        states,
        authoritative: auth,
        ...(note ? { note } : {}),
      });
    };

    // Three passes, each consuming what it matched so a later pass cannot re-read
    // the same numbers as something else.
    const consumed: Array<[number, number]> = [];
    const isConsumed = (start: number) => consumed.some(([a, b]) => start >= a && start < b);
    const consume = (m: RegExpExecArray) => consumed.push([m.index, m.index + m[0].length]);

    // "N of M tools": M is the total; N is a subset.
    for (const re of [/(\d+)\s*(?:of|\/)\s*(\d+)\s+tools\b/gi, /tools?\s*\((\d+)\s*(?:of|\/)\s*(\d+)\)/gi]) {
      let m: RegExpExecArray | null;
      while ((m = re.exec(body)) !== null) {
        consume(m);
        const [, subset, total] = m;
        if (Number(subset) > Number(total)) {
          flag("tools", m, `${subset} of ${total}`, `a subset cannot exceed its total`, "internal contradiction: the subset is larger than the total it is drawn from");
        }
        if (!YEAR.test(total)) flag("tools", m, total, String(authoritative.tools));
      }
    }

    // "N to M tools": the count became M.
    for (const re of [/(\d+)\s+to\s+(\d+)\s+tools\b/gi, /tool surface[^.\n]*?\b(\d+)\s+to\s+(\d+)\b/gi]) {
      let m: RegExpExecArray | null;
      while ((m = re.exec(body)) !== null) {
        consume(m);
        const resulting = m[2];
        if (!YEAR.test(resulting)) flag("tools", m, resulting, String(authoritative.tools));
      }
    }

    // Plain "N tools", unless subset-qualified.
    const SUBSET_PREFIX = /\b(?:other|others|remaining|rest|read-open|read open|gated|write-gated|ungated|only|another|of those|of these|first|last)\b[^.\n]{0,12}$/i;
    for (const re of [/(\d+)\s+tools\b/gi, /tool surface[^.\n]*?\b(\d+)\b/gi]) {
      let m: RegExpExecArray | null;
      while ((m = re.exec(body)) !== null) {
        if (isConsumed(m.index)) continue;
        if (YEAR.test(m[1])) continue;
        const numberAt = m.index + m[0].indexOf(m[1]);
        if (SUBSET_PREFIX.test(body.slice(Math.max(0, numberAt - 40), numberAt))) continue;
        flag("tools", m, m[1], String(authoritative.tools));
      }
    }

    // "N of M gates": M is the claim about the artifact.
    let m: RegExpExecArray | null;
    const ofForm = /(\d+)\s*(?:of|\/)\s*(\d+)\s+gates?\b/gi;
    while ((m = ofForm.exec(body)) !== null) {
      // Consumed like the tools passes, so the plain form below does not read the
      // same "M gates" a second time and flag it twice.
      consume(m);
      if (YEAR.test(m[2])) continue;
      flag("live gates", m, m[2], String(authoritative.liveGates), "the total, not the number that passed");
    }
    const plainForm = /\b(\d+)\s+(?:live\s+)?gates\b(?!\s*(?:passed|failed))/gi;
    while ((m = plainForm.exec(body)) !== null) {
      if (isConsumed(m.index)) continue;
      if (YEAR.test(m[1])) continue;
      flag("live gates", m, m[1], String(authoritative.liveGates));
    }

    // "all seven" is only a header claim in header context.
    const HEADER_CONTEXT = /header|security-policy|\bCSP\b|\bHSTS\b|nosniff|Referrer-Policy|X-Frame-Options|Permissions-Policy|COOP/i;
    const sevenForm = /all seven\b[^.\n]*/gi;
    while ((m = sevenForm.exec(body)) !== null) {
      const neighbourhood = body.slice(Math.max(0, m.index - 200), Math.min(body.length, m.index + m[0].length + 200));
      if (!HEADER_CONTEXT.test(neighbourhood)) continue;
      flag(
        "security headers",
        m,
        "all seven",
        `${authoritative.htmlEnforcedHeaders} enforced plus ${authoritative.htmlReportOnlyHeaders} Report-Only`,
        "COOP ships Report-Only pending a demonstrated case and a ruling, so 'all seven enforced' overstates what is live"
      );
    }

  }

  return claims;
}