Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ jobs:
NODE_OPTIONS: "--no-experimental-webstorage --max-old-space-size=4096"
CI: "true"

# QNBS-v3: node:test tooling scripts (.mjs) fall outside Vitest's include glob (.ts/.tsx only) --
# this is the one authoritative step admitting them to routine CI, not ad-hoc scattered invocation.
- name: Unit tests (node:test, tooling scripts)
run: pnpm run test:node

- name: Coverage ratchet check (informational, non-blocking)
if: always()
continue-on-error: true
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pnpm run token:audit # audit-tokens.mjs — design-token usage gate (CI b

**Quality gate (local pre-push subset):** `pnpm run ci:prepush` runs dependency-state/docs/CSP/native-readiness checks unconditionally (never full-repository lint — see the pre-push gate note above for what runs lint locally), and the single-checker local typecheck and i18n/content-guard checks only for changes the classifier marks as potentially impacting them (fail-closed to "run everything conditional" when evidence is incomplete); CI additionally runs full-repository lint, the 4-checker typecheck, full-suite coverage, and heavy jobs regardless of what the local gate ran. Locally use only the targeted form `pnpm exec vitest run <path> --coverage` when debugging coverage. Full pipeline graph: [`docs/CI.md`](docs/CI.md). Coverage thresholds: lines 74, branches 60, functions 67, statements 72 (see `vitest.config.ts`).

**Exact-tree typecheck verification (opt-in, not part of `ci:prepush`):** `pnpm run verify:exact-tree [ref...]` (default `HEAD`) creates an isolated `git worktree` at the exact commit, symlinks in the real `node_modules` (skipped — reports `UNKNOWN` — when `scripts/dependency-state.mjs`'s reconciled fingerprint doesn't match that commit's manifests, since the symlink would then misrepresent the commit's real dependency graph), and runs the same single-checker `tsgo --noEmit` there. This closes the gap where `ci-prepush-lowend.mjs`'s normal typecheck runs against whatever is currently on disk, not necessarily the exact tree about to be pushed. Deliberately **not** wired into the default `ci:prepush` path — it roughly doubles local typecheck cost, unacceptable as an always-on default on this hardware; run it manually before a risky push or when investigating a CI/local typecheck mismatch. Diagnostic-only: reports `PASS | FAIL | NOT_APPLICABLE | UNKNOWN` and never blocks a push on its own; required CI remains the sole merge-safety authority regardless of its result.
Comment thread
qnbs marked this conversation as resolved.
Outdated

**CI pipeline order:** `security` → `quality` (Biome + tsgo + Vitest matrix) → `build` / `e2e` / `storybook` (parallel) → `lighthouse` (after build) → `deploy` on `main`. `ci-success` is a required-status aggregator (`needs: [security, quality, build]`) so branch protection can require one context instead of three/four individual ones — see `docs/CI.md`. Two additional jobs run in parallel with `quality`, both path-scoped via the `changes` job (legitimately `skipping` on PRs that don't touch their directory, which `ci-success` treats as a pass for that job only): `rust-tauri` (`src-tauri/**` — fmt/check/clippy/test, needs the GTK/WebKit apt-get steps) and `core-rust` (`crates/**` — same fmt/check/clippy/test for the renderer-neutral Rust Core, no GUI deps so no apt-get steps needed).

**CI-cloud-first workflow (constrained local hardware only):** On low-end hardware, run only `ci:prepush` locally before pushing. Coverage, E2E, Lighthouse, and Stryker are CI-gate jobs. After each push, update README.md badges and AUDIT.md quality-gate line with CI-reported numbers. Local CI simulation: `act pull_request --job quality` (Docker + `act`; see `infra/low-end-ci/DAILY-DRIVER.md`).
Expand Down
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
}
},
{
"includes": ["tests/**/*.ts", "tests/**/*.tsx"],
"includes": ["tests/**/*.ts", "tests/**/*.tsx", "tests/**/*.mjs"],
"linter": {
"rules": {
"suspicious": {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"deps:verify": "node scripts/dependency-state.mjs verify",
"deps:reconcile": "node scripts/dependency-state.mjs reconcile",
"ci:prepush": "node scripts/ci-prepush-lowend.mjs",
"verify:exact-tree": "node scripts/verify-exact-tree.mjs",
"ci:local:full": "pnpm run lint && pnpm run typecheck && pnpm run i18n:check && pnpm run guardrail:desktop-imports && pnpm run native-readiness:check",
"predev": "node scripts/sync-csp.mjs && node scripts/sync-sw-version.mjs && node scripts/sync-tauri-version.mjs && node scripts/sync-readme-metrics.mjs && node scripts/build-i18n.mjs && node scripts/copy-duckdb-assets.mjs",
"dev": "vite",
Expand Down Expand Up @@ -86,6 +87,7 @@
"test:turbo": "turbo run test",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:node": "node --test tests/unit/tooling/*.test.mjs",
"bench": "vitest bench --run tests/bench",
"test:e2e": "node -e \"if (process.env.CI !== 'true') { console.error('E2E tests are CI-only. Set CI=true or run via GitHub Actions.'); process.exit(1); }\" && playwright test",
"test:e2e:ui": "node -e \"if (process.env.CI !== 'true') { console.error('E2E tests are CI-only. Set CI=true or run via GitHub Actions.'); process.exit(1); }\" && playwright test --ui",
Expand Down
70 changes: 70 additions & 0 deletions scripts/verify-exact-tree.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { BoundedResult } from './hooks/shared.d.mts';
import type { GitOptions, GitResult } from './signing/signing-core.d.mts';

// QNBS-v3: absolute correctness check, not a comparison -- distinct vocabulary from WorkingTreeState.
export type ExactTreeState = 'PASS' | 'FAIL' | 'NOT_APPLICABLE' | 'UNKNOWN';

export interface VerifyExactTreeDependencies {
runBounded?: (
command: string,
args: string[],
options?: {
timeoutMs?: number;
cwd?: string;
env?: NodeJS.ProcessEnv;
input?: string;
shell?: boolean;
root?: string;
detached?: boolean;
},
) => Promise<BoundedResult>;
runLocalBinaryDetailed?: (
binary: string,
args: string[],
options?: { root?: string; cwd?: string; timeoutMs?: number },
) => Promise<BoundedResult>;
// QNBS-v3: a distinct, pre-existing (#494) synchronous/output-capturing wrapper -- not BoundedResult.
runGit?: (args: string[], options?: GitOptions) => GitResult;
mkdtempFn?: () => Promise<string>;
rmFn?: (path: string) => Promise<void>;
installTimeoutMs?: number;
tsgoArgs?: string[];
repoRoot?: string;
}

export function createIsolatedWorktree(
sha: string,
repoRoot: string,
dependencies?: VerifyExactTreeDependencies,
Comment thread
qnbs marked this conversation as resolved.
): Promise<{ ok: boolean; path: string | undefined }>;

export function removeIsolatedWorktree(
worktreePath: string | undefined,
repoRoot: string,
dependencies?: VerifyExactTreeDependencies,
): Promise<void>;

export function installDependencies(
worktreePath: string,
dependencies?: VerifyExactTreeDependencies,
): Promise<boolean>;

export function verifyExactTreeTypecheck(
sha: string,
repoRoot?: string,
dependencies?: VerifyExactTreeDependencies,
): Promise<ExactTreeState>;

export function verifyExactTreeForShas(
shas: string[],
repoRoot?: string,
dependencies?: VerifyExactTreeDependencies,
): Promise<ExactTreeState>;

export function resolveRef(
ref: string,
repoRoot: string,
dependencies?: VerifyExactTreeDependencies,
): string | null;

export function main(argv?: string[], dependencies?: VerifyExactTreeDependencies): Promise<void>;
171 changes: 171 additions & 0 deletions scripts/verify-exact-tree.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { mkdtemp as mkdtempAsync, rm as rmAsync } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import process from 'node:process';
import { isMainModule } from './ci-prepush-range-resolver.mjs';
import { runBounded, runLocalBinaryDetailed } from './hooks/shared.mjs';
import { runGit as defaultRunGit } from './signing/signing-core.mjs';

const DEFAULT_TSGO_ARGS = ['--project', 'tsconfig.tsgo.json', '--noEmit', '--checkers', '1'];
Comment thread
qnbs marked this conversation as resolved.
Outdated
// QNBS-v3: measured ~2m20s for the full project on this hardware with a warm store; 5min gives margin.
const DEFAULT_INSTALL_TIMEOUT_MS = 300_000;

// QNBS-v3: lifecycle commands (worktree/install) -- any non-zero or unreadable exit fails outright.
function boundedCommandFailed(result) {
return Boolean(
result.error || result.timedOut || result.interrupted || result.signal || result.status !== 0,
);
}

// QNBS-v3: tsgo only -- a genuine numeric non-zero status is a real FAIL; a signal/timeout/OOM is UNKNOWN.
function tsgoResultUnknown(result) {
return Boolean(
result.error || result.timedOut || result.interrupted || result.signal || typeof result.status !== 'number',
);
}

// QNBS-v3: sweeps entries orphaned by a prior crashed/killed run before creating a new one.
async function pruneStaleWorktrees(repoRoot, dependencies) {
const runGit = dependencies.runBounded ?? runBounded;
await runGit('git', ['worktree', 'prune'], { cwd: repoRoot });
Comment thread
qnbs marked this conversation as resolved.
Outdated
}
Comment thread
qnbs marked this conversation as resolved.

export async function createIsolatedWorktree(sha, repoRoot, dependencies) {
const runGit = dependencies.runBounded ?? runBounded;
Comment thread
qnbs marked this conversation as resolved.
Outdated
const makeTempDir =
dependencies.mkdtempFn ?? (() => mkdtempAsync(join(tmpdir(), 'worldscript-exact-tree-')));
await pruneStaleWorktrees(repoRoot, dependencies);
let path;
try {
path = await makeTempDir();
} catch {
return { ok: false, path: undefined };
}
const result = await runGit('git', ['worktree', 'add', '--detach', path, sha], { cwd: repoRoot });
Comment thread
qnbs marked this conversation as resolved.
Outdated
if (boundedCommandFailed(result)) return { ok: false, path };
return { ok: true, path };
}

// QNBS-v3: fail-closed -- git's own removal failing falls back to a raw sweep plus a metadata prune.
export async function removeIsolatedWorktree(worktreePath, repoRoot, dependencies) {
if (!worktreePath) return;
const runGit = dependencies.runBounded ?? runBounded;
const removeDir = dependencies.rmFn ?? ((path) => rmAsync(path, { recursive: true, force: true }));
const result = await runGit('git', ['worktree', 'remove', '--force', worktreePath], {
cwd: repoRoot,
});
if (boundedCommandFailed(result)) {
try {
await removeDir(worktreePath);
} catch {
Comment thread
qnbs marked this conversation as resolved.
// Best-effort: nothing more can be done from here; the directory is under os.tmpdir().
}
await runGit('git', ['worktree', 'prune'], { cwd: repoRoot });
return;
}
// QNBS-v3: git leaves the now-empty mkdtemp-created dir in place after remove -- sweep it too.
try {
await removeDir(worktreePath);
} catch {
// Best-effort: nothing more can be done from here; the directory is under os.tmpdir().
}
}

// QNBS-v3: real pnpm install, not a hand-reconstructed symlink graph -- offline, fails to UNKNOWN below.
export async function installDependencies(worktreePath, dependencies) {
const runPnpm = dependencies.runBounded ?? runBounded;
const timeoutMs = dependencies.installTimeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS;
const result = await runPnpm('pnpm', ['install', '--frozen-lockfile', '--offline'], {
cwd: worktreePath,
timeoutMs,
Comment thread
qnbs marked this conversation as resolved.
Outdated
Comment thread
qnbs marked this conversation as resolved.
Outdated
Comment thread
qnbs marked this conversation as resolved.
Outdated
});
return !boundedCommandFailed(result);
}

// QNBS-v3: precedence shape mirrors aggregateDiagnosticState, kept separate -- different vocabulary.
function aggregateExactTreeState(states) {
const relevant = states.filter((state) => state !== 'NOT_APPLICABLE');
if (relevant.length === 0) return 'NOT_APPLICABLE';
if (relevant.includes('FAIL')) return 'FAIL';
if (relevant.includes('UNKNOWN')) return 'UNKNOWN';
return 'PASS';
}

// QNBS-v3: never throws -- an opt-in diagnostic tool must fail closed to UNKNOWN, not crash the caller.
export async function verifyExactTreeTypecheck(sha, repoRoot = process.cwd(), dependencies = {}) {
// QNBS-v3: a relative repoRoot would produce ambiguous git-cwd and install-cwd semantics.
const absoluteRepoRoot = resolve(repoRoot);
try {
const created = await createIsolatedWorktree(sha, absoluteRepoRoot, dependencies);
if (!created.ok) {
await removeIsolatedWorktree(created.path, absoluteRepoRoot, dependencies);
return 'UNKNOWN';
}

try {
if (!(await installDependencies(created.path, dependencies))) return 'UNKNOWN';

const runDetailed = dependencies.runLocalBinaryDetailed ?? runLocalBinaryDetailed;
const tsgoArgs = dependencies.tsgoArgs ?? DEFAULT_TSGO_ARGS;
const result = await runDetailed('tsgo', tsgoArgs, {
Comment thread
qnbs marked this conversation as resolved.
root: created.path,
cwd: created.path,
});
Comment thread
qnbs marked this conversation as resolved.
// QNBS-v3: a signal (including an external OOM kill) must yield UNKNOWN, never a false FAIL.
if (tsgoResultUnknown(result)) return 'UNKNOWN';
return result.status === 0 ? 'PASS' : 'FAIL';
} finally {
await removeIsolatedWorktree(created.path, absoluteRepoRoot, dependencies);
}
} catch {
return 'UNKNOWN';
}
}

export async function verifyExactTreeForShas(shas, repoRoot = process.cwd(), dependencies = {}) {
const unique = [...new Set(shas)];
if (unique.length === 0) return 'NOT_APPLICABLE';
const states = [];
for (const sha of unique) {
// QNBS-v3: sequential, never parallel -- bounded resource use on constrained developer hardware.
states.push(await verifyExactTreeTypecheck(sha, repoRoot, dependencies));
Comment thread
qnbs marked this conversation as resolved.
}
return aggregateExactTreeState(states);
}

// QNBS-v3: reuses signing-core's bounded, output-capturing runGit -- runBounded can't capture stdout.
export function resolveRef(ref, repoRoot, dependencies = {}) {
const runGit = dependencies.runGit ?? defaultRunGit;
const result = runGit(['rev-parse', '--verify', `${ref}^{commit}`], { cwd: repoRoot });
if (result.error || result.status !== 0) return null;
return result.stdout.trim();
}

export async function main(argv = process.argv.slice(2), dependencies = {}) {
const repoRoot = resolve(dependencies.repoRoot ?? process.cwd());
const refs = argv.length > 0 ? argv : ['HEAD'];
const shas = [];
for (const ref of refs) {
const sha = resolveRef(ref, repoRoot, dependencies);
if (!sha) {
console.error(`[verify-exact-tree] could not resolve ref: ${ref}`);
process.exitCode = 1;
return;
}
shas.push(sha);
}
console.log(`[verify-exact-tree] verifying ${shas.length} commit(s) in isolated worktree(s)...`);
const state = await verifyExactTreeForShas(shas, repoRoot, dependencies);
console.log(`[verify-exact-tree] result: ${state}`);
if (state === 'UNKNOWN') {
console.log(
'[verify-exact-tree] could not be established (offline dependency materialization failed, or a worktree/tsgo step failed); required CI remains authoritative.',
);
} else if (state === 'FAIL') {
console.log('[verify-exact-tree] the exact committed tree does not typecheck in isolation.');
process.exitCode = 1;
}
}

// QNBS-v3: guard execution so this module can be imported for testing without running the CLI.
if (isMainModule(process.argv[1], import.meta.url)) await main();
Loading
Loading