Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ voice-spike/
*.crt
*.der
.storycraft-*
# Test fixture temp roots created under cwd by dependency-state/signing/verify-exact-tree tests --
# normally cleaned up in afterEach, but an interrupted run can leave these visible to git status.
.worldscript-*
~/

.playwright-mcp/
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`) proves the *exact committed tree* of a ref typechecks in full isolation from the live checkout. Before touching disk, it lists the target commit's own git tree (`dependency-state.mjs`'s `listTreeFiles`, reused rather than a second parser) and refuses (`UNKNOWN`) any commit force-tracking a `node_modules` path anywhere (root, nested, or a tracked `node_modules` symlink itself) — an arbitrary ref could otherwise smuggle in an attacker-controlled `node_modules/.bin/tsgo` for the tool to trust and execute. It then creates an isolated `git worktree` at the exact commit with Git hooks disabled for that one invocation (`git -c core.hooksPath=<empty temp dir> worktree add --detach`, cleaned up immediately after) — this repo's `graphify:hooks` `post-checkout` integration, or any other configured hook, must never fire during materialization. Inside that hook-free worktree it runs a real `pnpm install --frozen-lockfile --offline --ignore-scripts --ignore-pnpmfile` (`COREPACK_ENABLE_NETWORK=0` too, since `pnpm` here is a Corepack shim that could otherwise reach the network before pnpm's own `--offline` applies; `--ignore-pnpmfile` blocks pnpm's own hook-file mechanism, a separate arbitrary-code path from `--ignore-scripts`), reconstructing pnpm's own real dependency-resolution graph (root, package-local, and transitive workspace links) rather than symlinking the live checkout's `node_modules` — an earlier symlink-based design was found, in review, to leak uncommitted/live workspace-package source back into the "isolated" result. Finally it runs the same single-checker `tsgo --noEmit` inside that tree with an explicit 6-minute timeout (measured the single-checker run alone at ~56s here; the repo separately documents ~300s for the full multi-checker `pnpm run typecheck`, so 6 minutes clears both figures with real margin, not a bare 300s cutoff). A missing package in the local offline store, an unreadable tree, a hook-dir failure, or a signal/timeout always fails the install/typecheck step and reports `UNKNOWN`, never a silent wrong answer — this tool never falls back to the network and never treats an unprovable state as a pass. 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 — the real install alone measures over a minute on this hardware, unacceptable as an always-on default; 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. Its `node:test` suite (`scripts/verify-exact-tree.mjs`'s own tooling test, alongside `scripts/dependency-state.mjs`'s) runs serially (`--test-concurrency=1`, since these fixtures interleave git/pnpm/worktree operations that must not overlap on this hardware) via `pnpm run test:node`, wired into CI's quality job separately from Vitest (whose include glob is `.ts`/`.tsx`-only).

**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 --test-concurrency=1 tests/unit/tooling/dependency-state.test.mjs tests/unit/tooling/verify-exact-tree.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
1 change: 1 addition & 0 deletions scripts/dependency-state.d.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// QNBS-v3: diagnostic-only dimension, independent of resolvePushEvidence's canonical evidence validity.
export type DependencyState = 'MATCHES' | 'DIVERGED' | 'NOT_APPLICABLE' | 'UNKNOWN';

export function listTreeFiles(sha: string, cwd?: string): string[] | null;
export function dependencyFiles(root?: string): string[];
export function calculateDependencyFingerprint(root?: string): string;
export function fingerprintPath(root?: string): string;
Expand Down
6 changes: 3 additions & 3 deletions scripts/dependency-state.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ export function calculateDependencyFingerprint(root = projectRoot) {
return hashManifests(entries);
}

// QNBS-v3: --full-tree ignores cwd-subdirectory scoping; -z disables git's default path C-quoting.
function defaultListTreeFiles(sha, cwd) {
// QNBS-v3: -z avoids path C-quoting; exported so verify-exact-tree.mjs reuses this, not a second parser.
export function listTreeFiles(sha, cwd) {
const result = spawnSync('git', ['ls-tree', '-r', '--full-tree', '--name-only', '-z', sha], {
cwd,
encoding: 'utf8',
Expand All @@ -89,7 +89,7 @@ function defaultListTreeFiles(sha, cwd) {

// QNBS-v3: diagnostic-only; mirrors dependencyFiles' inclusion rules against a commit, not disk.
export function dependencyFilesFromRef(sha, root = projectRoot, dependencies = {}) {
const listTree = dependencies.listTree ?? ((ref) => defaultListTreeFiles(ref, root));
const listTree = dependencies.listTree ?? ((ref) => listTreeFiles(ref, root));
const allPaths = listTree(sha);
if (allPaths === null) return null;
const rootFiles = new Set(['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']);
Expand Down
75 changes: 75 additions & 0 deletions scripts/verify-exact-tree.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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>;
// QNBS-v3: a separate temp dir authority from mkdtempFn -- distinct lifecycle (hooks dir vs. worktree dir).
mkdtempHooksFn?: () => Promise<string>;
rmFn?: (path: string) => Promise<void>;
installTimeoutMs?: number;
tsgoArgs?: string[];
tsgoTimeoutMs?: number;
repoRoot?: string;
// QNBS-v3: reuses dependency-state.mjs's git-tree enumeration authority -- not a second parser.
listTreeFiles?: (sha: string, cwd: string) => string[] | null;
}

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>;
Loading
Loading