Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions scripts/check-dispatcher-error-vocabulary.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,6 +108,7 @@
*/

import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { maskComments } from './js-comment-mask.mjs';
import { join, relative, dirname, resolve } from 'node:path';

const ROOT = resolve(new URL('..', import.meta.url).pathname);
Expand DownExpand Up@@ -147,10 +148,7 @@ export function parseStandardCodes(source) {
// The scan
// ---------------------------------------------------------------------------

/** Comments stripped: prose naming a retired code is not a producer. */
export function stripComments(source) {
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
}


/**
* The recognised ways this repo stamps a semantic code onto a value.
Expand DownExpand Up@@ -283,7 +281,7 @@ export function resolveConstant(name, fileSource, fileRel, readFile, ctx = {}) {
for (const cand of [`${base}.ts`, `${base}.mts`, `${base}/index.ts`]) {
const abs = join(ROOT, cand);
if (!existsSync(abs)) continue;
const target = stripComments(readFile(abs));
const target = maskComments(readFile(abs));
const hit = new RegExp(
`\\bexport\\s+const\\s+${name}\\s*(?::[^=]+)?=\\s*'([A-Za-z][A-Za-z0-9_]*)'`,
).exec(target);
Expand DownExpand Up@@ -354,7 +352,7 @@ export function deriveSites({ registered, files, readFile, packageDirs = new Map
const unresolved = [];
// Stripped once: the workspace resolver reads these same sources, and a
// comment naming a constant must not resolve one either.
const scanned = files.map(({ rel, source }) => ({ rel, stripped: stripComments(source) }));
const scanned = files.map(({ rel, source }) => ({ rel, stripped: maskComments(source) }));
const ctx = { scanned, packageDirs };
for (const { rel, stripped } of scanned) {
for (const shape of SHAPES) {
Expand DownExpand Up@@ -402,7 +400,7 @@ export function deriveSites({ registered, files, readFile, packageDirs = new Map
export function parseDeclaration(source) {
const start = source.indexOf('export const UNREGISTERED_CODE_SITES');
if (start < 0) throw new Error(`${DECLARATION}: UNREGISTERED_CODE_SITES not found — the anchor moved.`);
const body = stripComments(source.slice(start));
const body = maskComments(source).slice(start);
const end = body.indexOf('\n];');
if (end < 0) throw new Error(`${DECLARATION}: UNREGISTERED_CODE_SITES terminator not found — the anchor moved.`);
const rows = [];
Expand DownExpand Up@@ -579,7 +577,7 @@ export function checkDoorTyping({ doorSource, files }) {
`Point REST_DOOR_FILE at the file that now owns the REST error doors.`);
return findings;
}
const stripped = stripComments(doorSource);
const stripped = maskComments(doorSource);

// ① the author-side door narrows to the closed vocabulary
const decl = /export\s+function\s+sendDeclaredFault\s*\(([\s\S]*?)\)\s*:\s*void/.exec(stripped);
Expand All@@ -604,7 +602,7 @@ export function checkDoorTyping({ doorSource, files }) {
// ③ decided refusals do not travel through the `any` door
for (const { rel, source } of files) {
if (!rel.startsWith('packages/rest/')) continue;
const s = stripComments(source);
const s = maskComments(source);
DECIDED_THROUGH_THROWN_RE.lastIndex = 0;
for (const m of s.matchAll(DECIDED_THROUGH_THROWN_RE)) {
add(`${rel}: hands the author-declared code '${m[1]}' to \`sendThrownError\`, whose \`error\` ` +
Expand Down
10 changes: 3 additions & 7 deletions scripts/check-error-code-casing.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,6 +51,7 @@
*/

import { readFileSync, readdirSync, statSync } from 'node:fs';
import { maskComments } from './js-comment-mask.mjs';
import { join, relative, sep } from 'node:path';

const ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, '');
Expand DownExpand Up@@ -124,15 +125,10 @@ function walk(dir, out = []) {
return out;
}

/** Strip line and block comments so a docblock naming an old code is not a hit. */
function stripComments(src) {
return src
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '))
.replace(/(^|[^:])\/\/[^\n]*/g, (m, p) => p + m.slice(p.length).replace(/./g, ' '));
}


export function findViolations(src, file) {
const text = stripComments(src);
const text = maskComments(src);
const hits = [];
for (const { name, re } of CODE_POSITION_PATTERNS) {
re.lastIndex = 0;
Expand Down
64 changes: 17 additions & 47 deletions scripts/check-error-status-conformance.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,6 +110,7 @@
// `scripts/error-status-unpinned-baseline.json`; a NEW one fails the gate, and a
// row that becomes pinned fails it too (ratchet down with `--update`).
import { readdirSync, readFileSync, writeFileSync, statSync, existsSync } from 'node:fs';
import { maskComments } from './js-comment-mask.mjs';
import { join, relative } from 'node:path';

const SCAN_ROOT = 'packages';
Expand DownExpand Up@@ -176,7 +177,7 @@ export function buildConstantIndex(sources) {
// error family and reporting three declarations it could have resolved.
const COMPUTED_ENTRY = /\[\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)\s*\]\s*:\s*(?:'([^'\n]*)'|"([^"\n]*)"|(\d{3}))/g;

const clean = [...sources.values()].map(stripComments);
const clean = [...sources.values()].map((src) => maskComments(src));
const objectBodies = [];
for (const src of clean) {
for (const m of src.matchAll(SCALAR)) put(m[1], m[2] ?? m[3] ?? Number(m[4]));
Expand DownExpand Up@@ -231,54 +232,23 @@ function lookup(e, index) {
// ───────────────────────────────────────────────────────────────────────────

/**
* Blank out comments (keeping byte offsets, so reported line numbers stay
* true), because this repo DOCUMENTS envelopes in prose.
* Comment masking, shared. See `scripts/js-comment-mask.mjs` for the scanner.
*
* Not a tidiness measure it is load-bearing. Two of this gate's first findings
* on `main` were docblocks: `quickjs-runner.ts` narrates the bug it fixed
* ("the action surface answered `{ code: 'RECORD_NOT_FOUND', httpStatus: 400 }`")
* and `protocol.ts` names a shape it exists to PREVENT ("would mint incoherent
* rows `{ code: 'INTERNAL_ERROR', httpStatus: 409 }`"). Read as producers,
* both manufacture a status disagreement out of a sentence saying the opposite.
* A deriver that mines prose is not a deriver.
* Not a tidiness measure -- it is load-bearing here. Two of this gate's first
* findings on `main` were docblocks: `quickjs-runner.ts` narrates the bug it
* fixed ("the action surface answered `{ code: 'RECORD_NOT_FOUND', httpStatus:
* 400 }`") and `protocol.ts` names a shape it exists to PREVENT ("would mint
* incoherent rows -- `{ code: 'INTERNAL_ERROR', httpStatus: 409 }`"). Read as
* producers, both manufacture a status disagreement out of a sentence saying
* the opposite. A deriver that mines prose is not a deriver.
*
* Strings and template literals are tracked only so a `//` or `/*` inside one
* cannot open a phantom comment; their contents are left intact.
* The private scanner this replaces tracked strings and templates but read a
* `/` as division, so a regex literal holding a quote character opened a
* phantom string -- and because a scanner SKIPS string spans, every comment
* inside one went unmasked, which is the FABRICATING direction, not the
* blinding one. Byte offsets still survive the mask, so reported line numbers
* stay true.
*/
export function stripComments(src) {
const out = src.split('');
let i = 0;
const n = src.length;
while (i < n) {
const c = src[i];
const d = src[i + 1];
if (c === '/' && d === '/') {
while (i < n && src[i] !== '\n') { out[i] = ' '; i++; }
continue;
}
if (c === '/' && d === '*') {
while (i < n && !(src[i] === '*' && src[i + 1] === '/')) { if (src[i] !== '\n') out[i] = ' '; i++; }
if (i < n) { out[i] = ' '; out[i + 1] = ' '; i += 2; }
continue;
}
if (c === "'" || c === '"' || c === '`') {
const quote = c;
// A `'` or `"` never spans a line in JS, so a quote character mis-read
// out of a regex character class (`/["'`]/`) recovers at end of line
// instead of swallowing the rest of the file.
const bounded = quote !== '`';
i++;
while (i < n && src[i] !== quote && !(bounded && src[i] === '\n')) {
if (src[i] === '\\') i++;
i++;
}
i++;
continue;
}
i++;
}
return out.join('');
}

/** Brace-matched class bodies, so a property read never escapes its class. */
function classBodies(src) {
Expand DownExpand Up@@ -324,7 +294,7 @@ export function deriveRuntimeStatuses(sources, index) {
unresolved.push(`${where}: code=${String(code).trim()} status=${String(status).trim()}`);

for (const [path, raw] of sources) {
const src = stripComments(raw);
const src = maskComments(raw);
// R1 — error classes declaring their own code + status/statusCode.
for (const { name, body } of classBodies(src)) {
const codeM = /^[ \t]*(?:public\s+|protected\s+|private\s+)?readonly\s+code\s*(?::[^=\n]+)?=\s*([^;\n]+);/m.exec(body);
Expand Down
55 changes: 11 additions & 44 deletions scripts/check-examples-live-imports.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,6 +106,7 @@
// that has one AND mentions `examples/` in its text, under `unresolved`.

import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
import { maskComments } from './js-comment-mask.mjs';
import { join, resolve, relative, dirname, sep, posix } from 'node:path';
import { fileURLToPath } from 'node:url';
import process from 'node:process';
Expand DownExpand Up@@ -163,54 +164,20 @@ const TEST_DIR_RE = /(?:^|\/)(?:test|tests|__tests__)(?:\/|$)/;
const rel = (abs) => relative(REPO_ROOT, abs).split(sep).join('/');

/**
* Strip line and block comments while preserving string and template contents.
* Comment masking, shared. See `scripts/js-comment-mask.mjs` for the scanner.
*
* A comment mentioning `examples/app-showcase` is NOT a coupling -- that is the
* false-positive class a bare grep produces -- but the specifiers we DO want
* are themselves string literals, so a blunt "drop everything quoted" pass
* would erase the signal. Hence a real scanner rather than a regex.
*
* The private copy this replaces was string-aware but read a `/` as division,
* so a regex literal holding a quote character opened a phantom string that ran
* to the next matching quote -- to end of file for a backtick. Comments inside
* that span were never removed, so the gate read commented-out imports as live
* ones. Measured on this gate's own corpus, the two worst files were showcase
* tests carrying exactly that shape.
*/
function stripComments(src) {
let out = '';
let i = 0;
const n = src.length;
while (i < n) {
const c = src[i];
const d = src[i + 1];
if (c === '/' && d === '/') {
while (i < n && src[i] !== '\n') i++;
continue;
}
if (c === '/' && d === '*') {
i += 2;
while (i < n && !(src[i] === '*' && src[i + 1] === '/')) i++;
i += 2;
continue;
}
if (c === '"' || c === "'" || c === '`') {
const quote = c;
out += c;
i++;
while (i < n) {
if (src[i] === '\\') {
out += src[i] + (src[i + 1] ?? '');
i += 2;
continue;
}
out += src[i];
if (src[i] === quote) {
i++;
break;
}
i++;
}
continue;
}
out += c;
i++;
}
return out;
}

/** Every string-literal specifier reachable as a live module reference. */
function moduleSpecifiers(code) {
Expand DownExpand Up@@ -441,7 +408,7 @@ function collect() {
continue;
}
if (!raw.includes('example')) continue;
const code = stripComments(raw);
const code = maskComments(raw);
const refs = [...moduleSpecifiers(code), ...readPathLiterals(code)];

const couplings = [];
Expand DownExpand Up@@ -705,7 +672,7 @@ function selfTest() {
const fakeAbs = join(REPO_ROOT, 'packages/cli/test/x.test.ts');

const detects = (src) => {
const code = stripComments(src);
const code = maskComments(src);
const refs = [...moduleSpecifiers(code), ...readPathLiterals(code)];
return refs.some(({ spec }) => couplingTarget(spec, fakeAbs, apps) !== null);
};
Expand Down
12 changes: 8 additions & 4 deletions scripts/check-platform-checklist.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,7 @@
// Usage: node scripts/check-platform-checklist.mjs (pnpm check:platform-checklist)

import { readdirSync, readFileSync, existsSync } from 'node:fs';
import { maskComments } from './js-comment-mask.mjs';
import { join, basename } from 'node:path';

const ROOT = new URL('..', import.meta.url).pathname;
Expand DownExpand Up@@ -167,7 +168,12 @@ for (const file of files) {
// exists to force. Extractor rot is loud, not fail-open: a missing file or
// export is an error, never a silent skip.
function extractEnumMembers(absFile, exportName) {
const src = readFileSync(absFile, 'utf8');
// Masked ONCE, up front, rather than per-segment: comment spans are blanked
// in place, so every offset below still indexes the real file, and the
// bracket walk can no longer be closed early by a `]` that lives in a
// comment. Masking a SLICE would be the same defect one level down -- a
// fragment starting mid-file has no literal context to scan from.
const src = maskComments(readFileSync(absFile, 'utf8'));
const decl = src.match(new RegExp(`(?:export\\s+)?const\\s+${exportName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b[^=]*=`));
if (!decl) return null;
const start = src.indexOf('[', decl.index + decl[0].length);
Expand All@@ -178,9 +184,7 @@ function extractEnumMembers(absFile, exportName) {
else if (src[j] === ']') {
depth--;
if (depth === 0) {
const seg = src.slice(start, j + 1)
.replace(/\/\/[^\n]*/g, '')
.replace(/\/\*[\s\S]*?\*\//g, '');
const seg = src.slice(start, j + 1);
const seen = new Set();
for (const m of seg.matchAll(/'([a-zA-Z0-9_\-]+)'/g)) seen.add(m[1]);
return [...seen];
Expand Down
74 changes: 16 additions & 58 deletions scripts/check-test-source-alias.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,6 +172,7 @@
// node scripts/check-test-source-alias.mjs --self-test

import { readFileSync, readdirSync, statSync, existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { stripComments } from './js-comment-mask.mjs';
import { join, resolve, relative, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { tmpdir } from 'node:os';
Expand DownExpand Up@@ -672,66 +673,23 @@ function findVitestConfig(dir) {
return null;
}

const REGEX_CAN_START_AFTER = new Set(['(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '+', '-', '*', '%', '~', '^', '<', '>', '']);

/**
* Remove comments, leaving strings, template literals and regex literals
* intact. Needed because these configs carry long rationale comments that name
* the very specifiers being matched — reading one as an alias would report a
* package as safe on the strength of a paragraph about why it is not.
* Comment stripping, shared. See `scripts/js-comment-mask.mjs` for the scanner
* and for why this gate takes the deleting projection rather than the blanking
* one (its import regex is lazy, and blanking is quadratic over what it leaves).
*
* Needed because these configs carry long rationale comments that name the very
* specifiers being matched -- reading one as an alias would report a package as
* safe on the strength of a paragraph about why it is not.
*
* The private copy this replaces knew about strings AND regex literals, but
* decided "is this `/` a regex?" from the preceding CHARACTER alone. That misses
* the keyword forms -- `return /["`]/.test(s)`, `case /['`]/.test(x)` -- where a
* value character precedes and only the keyword tells regex from division. The
* shared scanner carries the keyword set. Latent on today's corpus (34 vitest
* configs, none carrying the shape), which is why it is pinned by shape in the
* shared module's self-test rather than by this gate's corpus.
*/
function stripComments(src) {
let out = '';
let previous = '';
let i = 0;
while (i < src.length) {
const c = src[i];
const next = src[i + 1];
if (c === '/' && next === '/') {
while (i < src.length && src[i] !== '\n') i++;
continue;
}
if (c === '/' && next === '*') {
i += 2;
while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i++;
i += 2;
continue;
}
if (c === '"' || c === "'" || c === '`') {
out += c;
i++;
while (i < src.length) {
if (src[i] === '\\') {
out += src[i] + (src[i + 1] ?? '');
i += 2;
continue;
}
out += src[i];
if (src[i] === c) {
i++;
break;
}
i++;
}
previous = c;
continue;
}
if (c === '/' && REGEX_CAN_START_AFTER.has(previous)) {
const end = scanRegexLiteral(src, i);
if (end > 0) {
out += src.slice(i, end);
i = end;
previous = '/';
continue;
}
}
out += c;
if (!/\s/.test(c)) previous = c;
i++;
}
return out;
}

/** End index (exclusive) of the regex literal starting at `start`, or -1. */
function scanRegexLiteral(src, start) {
let i = start + 1;
Expand Down
Loading
Loading