Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
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.
Resolved

**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
1 change: 1 addition & 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
38 changes: 38 additions & 0 deletions scripts/verify-exact-tree.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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?: GitOptions & { timeoutMs?: number },
) => Promise<GitResult & { timedOut: boolean; interrupted: boolean }>;
Comment thread
qnbs marked this conversation as resolved.
Resolved
runLocalBinaryDetailed?: (
binary: string,
args: string[],
options?: { root?: string; cwd?: string; timeoutMs?: number },
) => Promise<GitResult & { timedOut: boolean; interrupted: boolean }>;
mkdtempFn?: () => Promise<string>;
rmFn?: (path: string) => Promise<void>;
symlinkFn?: (target: string, linkPath: string) => void;
dependencyStateForRef?: (sha: string) => string;
nodeModulesSource?: string;
tsgoArgs?: string[];
runGitSync?: (args: string[]) => { status: number | null; stdout: string };
}

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

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

export function main(argv?: string[]): Promise<void>;
156 changes: 156 additions & 0 deletions scripts/verify-exact-tree.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { spawnSync } from 'node:child_process';
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
import { symlinkSync } from 'node:fs';
import { mkdtemp as mkdtempAsync, rm as rmAsync } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import process from 'node:process';
import { isMainModule } from './ci-prepush-range-resolver.mjs';
import { computeDependencyState } from './dependency-state.mjs';
import { runBounded, runLocalBinaryDetailed } from './hooks/shared.mjs';

const DEFAULT_TSGO_ARGS = ['--project', 'tsconfig.tsgo.json', '--noEmit', '--checkers', '1'];
Comment thread
qnbs marked this conversation as resolved.
Resolved

// 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.
Resolved
}

async function createIsolatedWorktree(sha, repoRoot, dependencies) {
const runGit = dependencies.runBounded ?? runBounded;
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.
Resolved
if (result.error || result.timedOut || result.interrupted || result.status !== 0) {
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.
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 (result.error || result.timedOut || result.interrupted || result.status !== 0) {
try {
await removeDir(worktreePath);
} catch {
Comment thread
qnbs marked this conversation as resolved.
Resolved
// Best-effort: nothing more can be done from here; the directory is under os.tmpdir().
}
await runGit('git', ['worktree', 'prune'], { cwd: repoRoot });
}
}

// QNBS-v3: real node_modules install per verification is exactly the unbounded cost this avoids.
function linkNodeModules(worktreePath, source, dependencies) {
const symlinkFn =
dependencies.symlinkFn ??
((target, linkPath) => symlinkSync(target, linkPath, process.platform === 'win32' ? 'junction' : 'dir'));
try {
symlinkFn(source, join(worktreePath, 'node_modules'));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
return true;
} catch {
return false;
}
}

// 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 = {}) {
try {
const dependencyStateForRef =
dependencies.dependencyStateForRef ?? ((ref) => computeDependencyState(ref, repoRoot));
// QNBS-v3: precondition for a trustworthy symlink, not a skip gate -- the check always runs.
if (dependencyStateForRef(sha) !== 'MATCHES') return 'UNKNOWN';

const created = await createIsolatedWorktree(sha, repoRoot, dependencies);
if (!created.ok) {
await removeIsolatedWorktree(created.path, repoRoot, dependencies);
return 'UNKNOWN';
}

try {
const nodeModulesSource = dependencies.nodeModulesSource ?? join(repoRoot, 'node_modules');
if (!linkNodeModules(created.path, nodeModulesSource, dependencies)) return 'UNKNOWN';
Comment thread
qnbs marked this conversation as resolved.
Resolved
Comment thread
qnbs marked this conversation as resolved.
Resolved

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.
Resolved
root: created.path,
cwd: created.path,
});
Comment thread
qnbs marked this conversation as resolved.
Resolved
if (result.error || result.timedOut || result.interrupted) return 'UNKNOWN';
return result.status === 0 ? 'PASS' : 'FAIL';
Comment thread
qnbs marked this conversation as resolved.
Resolved
Comment thread
qnbs marked this conversation as resolved.
Resolved
} finally {
await removeIsolatedWorktree(created.path, repoRoot, 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.
Resolved
}
return aggregateExactTreeState(states);
}

function resolveRef(ref, dependencies) {
const runGitSync =
dependencies.runGitSync ?? ((args) => spawnSync('git', args, { encoding: 'utf8' }));
const result = runGitSync(['rev-parse', '--verify', `${ref}^{commit}`]);
if (result.status !== 0) return null;
return result.stdout.trim();
}
Comment thread
qnbs marked this conversation as resolved.
Resolved

export async function main(argv = process.argv.slice(2)) {
const refs = argv.length > 0 ? argv : ['HEAD'];
const shas = [];
for (const ref of refs) {
const sha = resolveRef(ref);
Comment thread
qnbs marked this conversation as resolved.
Resolved
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);
console.log(`[verify-exact-tree] result: ${state}`);
if (state === 'UNKNOWN') {
console.log(
'[verify-exact-tree] could not be established (dependencies not reconciled/matched locally, 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