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
20 changes: 20 additions & 0 deletions .github/workflows/lint.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -152,6 +152,26 @@ jobs:
- name: Raw control-byte guard
run: pnpm check:nul-bytes

# Every `scripts/**` entry guard goes through ONE predicate (#10086).
# The hand-typed forms of "did node run me, or did someone import me?"
# had drifted into ELEVEN spellings across 33 files, and NINE were wrong
# in the same invisible direction: node resolves symlinks for the module
# graph but leaves `process.argv[1]` as the caller typed it, so a script
# reached through a symlink compared two different paths, answered false,
# and did NOTHING — exit 0, no output. The CI wrappers here hold the
# child's exit STATUS only, so an inert child read as a green gate; the
# governed-surface register (`scripts/pm/check-governed-merges.mjs`) was
# among the affected, where the inert run and its "NOT governed, ordinary
# queue landing applies" verdict are the SAME exit code.
# The sweep alone would not have held — nothing stopped a twelfth
# spelling. This gate is the part that closes the class: only
# `scripts/invoked-as.mjs` may read `process.argv[1]`, and that module's
# own self-test drives a real probe through a real symlink. Rationale and
# the rejected behavioural-sweep alternative: the gate script's header.
# Scans ~115 files, no spawns; ~0.2s.
- name: scripts/ entry guards go through one predicate
run: pnpm check:entry-guard

# Stack-collection enumerations vs the schema (#6242). `stack.zod.ts`
# decides which collections a stack may declare; eight other enumerations
# of that same set are hand-maintained (the map-format list, the
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,7 @@
"check:i18n-coverage": "node scripts/check-i18n-coverage.mjs --self-test && node scripts/check-i18n-coverage.mjs",
"check:app-nav-i18n": "pnpm --filter @objectstack/cli run check:app-nav-i18n",
"check:nul-bytes": "node scripts/check-nul-bytes.mjs --self-test && node scripts/check-nul-bytes.mjs",
"check:entry-guard": "node scripts/check-entry-guard.mjs --self-test && node scripts/check-entry-guard.mjs",
"check:stack-collection-maps": "node scripts/check-stack-collection-maps.mjs --self-test && node scripts/check-stack-collection-maps.mjs",
"check:doc-authoring": "node scripts/check-doc-authoring.mjs --self-test && node scripts/check-doc-authoring.mjs",
"check:doc-anchors": "node scripts/check-doc-anchors.mjs --self-test && node scripts/check-doc-anchors.mjs",
Expand Down
41 changes: 40 additions & 1 deletion packages/create-objectstack/src/template-version-stamps.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,14 +88,53 @@ const writeFixtureFile = (file: string, content: string) => {
fs.writeFileSync(file, content);
};

/**
* Copy `absFile` into the fixture at the SAME repo-relative position, then do
* the same for every RELATIVE import it makes, transitively.
*
* Hand-listing the file was the bug: this fixture used to copy exactly one
* named script, and when `sync-template-versions.mjs` gained an
* `import { isEntrypoint } from './invoked-as.mjs'` (#10086) the copy began
* throwing ERR_MODULE_NOT_FOUND on its first statement — both when imported
* (`loadSync`) and when spawned. Nothing in this file mentioned the sibling, so
* nothing here had to be edited for it to break.
*
* Deriving the closure removes the class rather than the instance: the NEXT
* sibling import someone adds to a copied script travels on its own. Copying
* the whole `scripts/` tree would also work, but it is 6 MB and 207 files for a
* closure that is currently two; and a directory SYMLINK is not an option here
* because node resolves symlinks for the module graph, so the script would
* resolve its repo root to the real checkout instead of this fixture — which is
* the very thing the copy exists to prevent.
*/
const copyWithLocalImports = (absFile: string, seen = new Set<string>()): void => {
if (!fs.existsSync(absFile)) return;
const real = fs.realpathSync(absFile);
if (seen.has(real)) return;
seen.add(real);

const rel = path.relative(repoRoot, real);
// Only files inside the checkout have a meaningful position in the fixture.
if (rel.startsWith('..') || path.isAbsolute(rel)) return;

const source = fs.readFileSync(real, 'utf8');
writeFixtureFile(path.join(fixture, rel), source);

const specifiers = [
...source.matchAll(/\bfrom\s+'(\.[^']*)'/g),
...source.matchAll(/\bimport\s*\(\s*'(\.[^']*)'/g),
];
for (const m of specifiers) copyWithLocalImports(path.resolve(path.dirname(real), m[1]), seen);
};

beforeAll(() => {
fixture = fs.mkdtempSync(path.join(os.tmpdir(), 'sync-template-versions-9554-'));

// The script resolves its repo root from its OWN location
// (`dirname(dirname(import.meta.url))`), so a copy two levels above the
// template tree makes the fixture a complete, self-consistent checkout.
fixtureScript = path.join(fixture, 'scripts', 'sync-template-versions.mjs');
writeFixtureFile(fixtureScript, fs.readFileSync(SYNC_SCRIPT, 'utf8'));
copyWithLocalImports(SYNC_SCRIPT);

writeFixtureFile(
path.join(fixture, 'packages', 'create-objectstack', 'package.json'),
Expand Down
6 changes: 5 additions & 1 deletion scripts/check-adr-0087-registration.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -305,6 +305,7 @@ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'nod
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isEntrypoint } from './invoked-as.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, '..');
Expand DownExpand Up@@ -3640,6 +3641,9 @@ function selfTest() {
};
const copy = 'scripts/check-adr-0087-registration.mjs';
w(copy, readFileSync(fileURLToPath(import.meta.url), 'utf8'));
// The entry guard is imported from `scripts/invoked-as.mjs`, so the sibling
// travels with the copy or the fixture dies on ERR_MODULE_NOT_FOUND.
w('scripts/invoked-as.mjs', readFileSync(new URL('./invoked-as.mjs', import.meta.url), 'utf8'));
w(
'importer.mjs',
"import { readDisposition } from './scripts/check-adr-0087-registration.mjs';\n" +
Expand DownExpand Up@@ -3710,7 +3714,7 @@ function selfTest() {
// the I1/I2 self-test assertions pin BOTH halves of the separation -- silent as
// an import, unchanged as an entry point.

if (resolve(process.argv[1] ?? '') === resolve(fileURLToPath(import.meta.url))) {
if (isEntrypoint(import.meta.url)) {
const argv = process.argv.slice(2);
const readFlag = (name) => {
const i = argv.indexOf(name);
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-adr-links.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,7 +109,7 @@
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isEntrypoint } from './invoked-as.mjs';

const ADR_DIR = 'docs/adr';

Expand DownExpand Up@@ -457,7 +457,7 @@ function selfTest() {
/* Run only when invoked as a program. The extractor is exported so a future
* caller (or a REPL session chasing a false positive) can import it without the
* import itself sweeping the repo. */
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
if (isEntrypoint(import.meta.url)) {
if (process.argv.includes('--self-test')) selfTest();
else runCheck();
}
3 changes: 2 additions & 1 deletion scripts/check-agent-model-declared.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,6 +83,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync,
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isEntrypoint } from './invoked-as.mjs';

const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const AGENTS_DIR = '.claude/agents';
Expand DownExpand Up@@ -562,6 +563,6 @@ function main() {
);
}

if (resolve(process.argv[1] ?? '') === resolve(fileURLToPath(import.meta.url))) {
if (isEntrypoint(import.meta.url)) {
main();
}
3 changes: 2 additions & 1 deletion scripts/check-console-injection.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,6 +77,7 @@ import {
readSpecBlob,
readStamp,
} from './console-spec-probes.mjs';
import { isEntrypoint } from './invoked-as.mjs';

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');

Expand DownExpand Up@@ -474,7 +475,7 @@ function selfTest() {
// evaluate() from a script exited 0 with "no console dist" and never reached the
// caller's code.
const invokedDirectly =
process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
isEntrypoint(import.meta.url);

if (!invokedDirectly) {
// imported as a module — expose evaluate() and do nothing else
Expand Down
10 changes: 10 additions & 0 deletions scripts/check-cross-package-test-inputs.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -430,6 +430,16 @@ const CROSS_PACKAGE_TEST_INPUTS = {
globs: [
'content/**',
'scripts/sync-template-versions.mjs',
// The stamper's own import closure, and a live input for the same reason
// the stamper is: template-version-stamps.test.ts copies the script into
// a fixture and both IMPORTS and SPAWNS it there, so the copy needs every
// relative import the script makes. That fixture derives the closure
// rather than naming files, so this path appears in NO quoted string the
// flat literal collector can see — but a change to it really does break
// that test (measured: drop the closure walk and the same 3 cases fail
// with ERR_MODULE_NOT_FOUND), which is exactly the trigger radius this
// declaration exists to keep honest.
'scripts/invoked-as.mjs',
'.github/workflows/scaffold-e2e.yml',
'packages/cli/src/commands/serve.ts',
'scripts/gen-sdui-manifest.sh',
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-cross-repo-closer-outcome.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -109,8 +109,8 @@ import { execFileSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { isMap, isSeq, parseDocument } from 'yaml';
import { isEntrypoint } from './invoked-as.mjs';

const WORKFLOW = '.github/workflows/cross-repo-issue-closer.yml';
const JOB = 'close-foreign-issues';
Expand DownExpand Up@@ -1146,7 +1146,7 @@ async function selfTest() {
// reverse-verification route needs `extractScript` / `judge` pointed at another
// tree (a pre-fix checkout), and a module that runs its gate on import would
// silently judge THIS repo instead and print a pass about the wrong subject.
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
if (isEntrypoint(import.meta.url)) {
if (process.argv.includes('--self-test')) await selfTest();
else if (process.argv.includes('--list')) list();
else await main();
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-doc-anchors.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -119,10 +119,10 @@
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import Slugger from 'github-slugger';

import { stripCodeSpans, stripFencedBlocks } from './check-adr-links.mjs';
import { isEntrypoint } from './invoked-as.mjs';

/**
* The page population this gate sweeps, as the repo-relative glob it really
Expand DownExpand Up@@ -604,7 +604,7 @@ function selfTest() {
/* Run only when invoked as a program — the extractor and the slug helpers are
* exported so a caller chasing a false positive can import them without the
* import itself sweeping the repo. */
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
if (isEntrypoint(import.meta.url)) {
if (process.argv.includes('--self-test')) selfTest();
else runCheck();
}
3 changes: 2 additions & 1 deletion scripts/check-docs-image-tag.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isEntrypoint } from './invoked-as.mjs';

/**
* The package whose `version` every concrete pin below must equal.
Expand DownExpand Up@@ -736,7 +737,7 @@ function main() {
// check-kernel-hook-pairs). Nothing about what this gate ASSERTS changes: both
// `check:docs-image-tag` invocations run this file directly, where argv[1] is this
// file and the branch is taken exactly as before.
if (resolve(process.argv[1] ?? '') === resolve(fileURLToPath(import.meta.url))) {
if (isEntrypoint(import.meta.url)) {
if (process.argv.includes('--self-test')) {
await selfTest();
} else {
Expand Down
3 changes: 2 additions & 1 deletion scripts/check-docs-redirects.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } f
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { isEntrypoint } from './invoked-as.mjs';

/** The docs site's `baseUrl` (apps/docs/lib/source.ts). */
const DOCS_BASE = '/docs';
Expand DownExpand Up@@ -689,7 +690,7 @@ async function main() {
* `firstMatchingSource` are exported so a sibling gate can ask "would Fumadocs
* serve this /docs/... URL?" without the import itself checking the redirect
* table (and calling `process.exit` out from under its caller). */
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
if (isEntrypoint(import.meta.url)) {
if (process.argv.includes('--self-test')) {
await selfTest();
} else {
Expand Down
Loading
Loading