mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
- test/test-of-test-ratchet.test.ts records the 228 free tests that import only test/ code and fails on a new one, naming the owner test to extend instead; a stale baseline entry fails with the remove instruction. - test/helpers/resolve-repo-path.ts is the one specifier/literal resolver for the ratchet and the touchfile closure invariant, with its own unit tests. - CONTRIBUTING "Test tiers" describes the paid-failure workflow (fix, then one row in the detector's owner test) and the ratchet; TEST_PORTFOLIO gains the detector -> owner-test table and no longer claims an Autoplan chain eval. - TODOS: automatic exclusion policy for chronically red periodic files (P3), the deferred native-completion table collapse, the unused CEO payment seeder; the PTY readiness item is narrowed to the paid runner. - docs/test-audit-2026-09.md collects the triage, security mapping, inventories, selection proof, behavior-commit decisions and retained false positives.
45 lines
2.1 KiB
TypeScript
45 lines
2.1 KiB
TypeScript
/**
|
|
* Resolve a module specifier or path literal from a repo file to a repo-relative
|
|
* path, or null when it names no file in the checkout. Bare packages and
|
|
* `bun:` / `node:` builtins never resolve. Shared by the touchfile closure
|
|
* invariant and the test-of-test ratchet.
|
|
*/
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
const isFile = (candidate: string) => fs.existsSync(candidate) && fs.statSync(candidate).isFile();
|
|
|
|
/** A relative module specifier (`./x`, `../lib/y.js`) → repo-relative file, or null. */
|
|
export function resolveRepoSpecifier(root: string, fromFile: string, specifier: string): string | null {
|
|
if (!specifier.startsWith('.')) return null;
|
|
const base = path.resolve(path.dirname(path.join(root, fromFile)), specifier);
|
|
for (const candidate of [base, `${base}.ts`, base.replace(/\.js$/, '.ts'), `${base}.tsx`, path.join(base, 'index.ts')]) {
|
|
if (isFile(candidate)) return toRelative(root, candidate);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** A repo-rooted path literal (`test/fixtures/x.json`, `bin/gstack-x`) → itself when it exists, else null. */
|
|
export function resolveRepoLiteral(root: string, literal: string): string | null {
|
|
const clean = literal.replace(/\/+$/, '');
|
|
if (!clean || path.isAbsolute(clean) || clean.startsWith('..')) return null;
|
|
return fs.existsSync(path.join(root, clean)) ? clean : null;
|
|
}
|
|
|
|
function toRelative(root: string, absolute: string): string | null {
|
|
const relative = path.relative(root, absolute).split(path.sep).join('/');
|
|
return relative.startsWith('..') ? null : relative;
|
|
}
|
|
|
|
/** Direct module specifiers of a source file: static imports/re-exports, require() and literal dynamic import(); `import type` excluded. */
|
|
export function directSpecifiers(source: string): string[] {
|
|
const out: string[] = [];
|
|
const typeOnly = /^\s*(?:import|export)\s+type\s/;
|
|
for (const match of source.matchAll(/^[ \t]*(?:import|export)\b[^;'"`]*?from\s*(['"])([^'"]+)\1|^[ \t]*import\s*(['"])([^'"]+)\3/gm)) {
|
|
if (typeOnly.test(match[0])) continue;
|
|
out.push(match[2] ?? match[4]!);
|
|
}
|
|
for (const match of source.matchAll(/\b(?:require|import)\s*\(\s*(['"])([^'"]+)\1\s*\)/g)) out.push(match[2]!);
|
|
return out;
|
|
}
|