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
30 changes: 23 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,17 @@ The app supports a multi-provider AI stack (Gemini, OpenAI, Claude, Grok, OpenRo
```bash
pnpm run ci:prepush
```
This gate is mandatory before every push and after every local correction before re-pushing; it runs sequentially with a single-checker project typecheck, i18n parity/quality and bundle checks, release/doc truth, and lightweight native guardrails. The pre-commit hook separately runs staged-file Biome checks. Full repository lint, coverage, E2E, Storybook, Lighthouse, and mutation checks belong to cloud CI. If branch switching or a lockfile/package-manifest change makes pnpm report dependency verification errors, run `node scripts/dependency-state.mjs reconcile` and rerun the complete pre-push gate.
This gate is mandatory before every push and after every local correction before re-pushing: it
always resolves a change-aware classification from the outgoing evidence first, then runs
release/doc truth and lightweight native guardrails sequentially and unconditionally. The
single-checker project typecheck and the i18n parity/quality/bundle checks run only when that
classification requires them (e.g. deferred for docs-, workflow-, tooling-, or non-TypeScript
test-only changes); incomplete or unresolved path evidence fails closed into running every
conditional check. The pre-commit hook separately runs staged-file Biome checks. Full repository
lint, coverage, E2E, Storybook, Lighthouse, and mutation checks belong to cloud CI, which also
re-runs the complete typecheck and i18n checks regardless of what the local gate deferred. If
branch switching or a lockfile/package-manifest change makes pnpm report dependency verification
errors, run `node scripts/dependency-state.mjs reconcile` and rerun the complete pre-push gate.
Optional targeted smoke test: `pnpm exec vitest run <path>` **without** `--coverage`.
**Hard rule:** Never invoke `pnpm test`, `npm run test`, or a bare Vitest wrapper; always use an explicit `pnpm exec vitest run <path>` command to avoid watch-mode hangs on constrained hardware. Never start multiple heavyweight processes concurrently.
4. **Audit cloud CI logs, fix locally, then re-push** – If the cloud CI run fails, inspect the logs via GitHub web UI or `gh run watch`, reproduce the specific failing test or lint error in isolation, fix it locally (quick tier to verify), commit, and push again for another cloud CI run.
Expand Down Expand Up @@ -295,12 +305,18 @@ procedure.
### Philosophy

- **Cloud CI-first:** The canonical quality gate is GitHub Actions. Low-end local machines should run only the "Quick" tier.
- **Quick tier (local, before every push):** `pnpm run ci:prepush` runs the project typecheck with
one checker, i18n parity/quality/bundle/content checks, release/doc truth, and lightweight desktop guardrails sequentially;
the pre-commit hook separately runs staged-file Biome checks. Run the gate again after every
correction before re-pushing; do not
push based only on a targeted test or a changed-file lint run. Optionally:
`pnpm exec vitest run <path>` **without** `--coverage`.
- **Quick tier (local, before every push):** `pnpm run ci:prepush` always resolves a change-aware
classification from the outgoing evidence, then runs release/doc truth and lightweight desktop
guardrails sequentially unconditionally. The one-checker project typecheck and i18n
parity/quality/bundle/content checks run only when the classification requires them —
`DOCS_ONLY`/`WORKFLOW_ONLY`/`NON_CODE_ONLY`/`RUST_TAURI`/`TOOLING`/non-TypeScript `TEST_ONLY`
changes report typecheck as deferred to required CI instead, and i18n/content-guard checks run
Comment thread
qnbs marked this conversation as resolved.
only for their own governed paths; incomplete or unresolved path evidence fails closed into
running everything. Required GitHub CI remains the unconditional authority for the full
typecheck and i18n validation. The pre-commit hook separately runs staged-file Biome checks. Run
the gate again after every correction before re-pushing; do not push based only on a targeted
test or a changed-file lint run. Optionally: `pnpm exec vitest run <path>` **without**
`--coverage`.
- **Dependency state:** `pnpm run deps:verify` compares a content fingerprint of dependency
manifests, workspace package manifests, and patches. After a dependency-related branch switch,
run `node scripts/dependency-state.mjs reconcile` (or `pnpm run deps:reconcile` when pnpm can
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ pnpm run token:audit # audit-tokens.mjs — design-token usage gate (CI b

**Vitest watch-mode hard rule:** Never invoke `pnpm test`, `npm run test`, or a bare Vitest wrapper. Always use an explicit targeted `pnpm exec vitest run <path>` command; watch mode hangs the constrained development hardware.

**Mandatory pre-push gate:** Run `pnpm run ci:prepush` before every push and again after every local correction before re-pushing. It runs the full repository lint, then the exact CI typecheck and i18n checks sequentially. A targeted test or changed-file lint run alone is insufficient. If pnpm reports dependency verification after a branch or lockfile change, run `pnpm install --frozen-lockfile` first. The pre-commit hook does not replace this gate.
**Mandatory pre-push gate:** Run `pnpm run ci:prepush` before every push and again after every local correction before re-pushing. It always resolves a change-aware classification (`scripts/ci-prepush-classifier.mjs`) from the outgoing evidence first, then runs docs/release-truth, CSP, desktop-import-boundary, native-readiness, and dependency-state checks unconditionally — it does **not** run Biome lint; that stays the pre-commit hook's job on staged files only (`lint-staged`), and full-repository lint is CI-owned. The single-checker (`--checkers 1`) local typecheck and the i18n/content-guard checks run only when the classification requires them — `DOCS_ONLY`, `WORKFLOW_ONLY`, `NON_CODE_ONLY`, `RUST_TAURI`, `TOOLING`, and non-TypeScript `TEST_ONLY` changes report typecheck as `DEFERRED_TO_REQUIRED_CI` instead of running it locally, and i18n/content-guard checks run only for changes matching their own governed paths or implementation files (see `scripts/ci-prepush-check-registry.mjs`). It is the same `tsgo --noEmit` check as CI, not literally identical to it — CI uses `--checkers 4`. Whenever outgoing path evidence is incomplete, unresolved, or the manual committed-range diff fails, the gate fails closed into full local admission (every conditional check runs) rather than deferring anything. A targeted test or changed-file lint run alone is insufficient. If pnpm reports a dependency verification failure after a branch or lockfile change, run `node scripts/dependency-state.mjs reconcile` (or `pnpm run deps:reconcile`) first, then rerun the gate. The pre-commit hook does not replace this gate. Required GitHub CI remains the unconditional authority for the complete lint, TypeScript, and i18n validation regardless of what the local gate deferred.

**Quality gate (local pre-push subset):** `pnpm run ci:prepush` runs the full repository lint followed by the exact CI typecheck and i18n checks; CI additionally runs full-suite coverage and heavy jobs. 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`).
**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`).

**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).

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<img src="https://img.shields.io/badge/Storage-IndexedDB_v8-F59E0B" alt="IndexedDB v8">
<img src="https://img.shields.io/badge/PWA-v3.0-5BB974?logo=pwa" alt="PWA v3.0">
<img src="https://img.shields.io/badge/i18n-19_locales-2925_keys-0EA5E9" alt="i18n 19 locales — 2925 keys">
<img src="https://img.shields.io/badge/Tests-6963%2B_%2F_575_files-22C55E" alt="6963+ tests / 575 files">
<img src="https://img.shields.io/badge/Tests-6984%2B_%2F_577_files-22C55E" alt="6984+ tests / 577 files">
<img src="https://img.shields.io/codecov/c/github/qnbs/WorldScript-Studio?logo=codecov&label=Coverage" alt="Codecov Coverage">
<img src="https://img.shields.io/badge/License-MIT-22C55E" alt="License MIT">
<img src="https://img.shields.io/github/actions/workflow/status/qnbs/WorldScript-Studio/.github/workflows/ci.yml?branch=main&logo=github" alt="CI Status">
Expand Down Expand Up @@ -512,7 +512,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and
| **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) |
| **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2925 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence |
| **Testing** | Vitest 4.x (6963+ tests / 575 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Testing** | Vitest 4.x (6984+ tests / 577 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy |
| **Visualization** | Force-directed graph | Interactive character relationship network |
| **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` |
Expand Down Expand Up @@ -550,7 +550,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
│ ├── unit/ # Vitest unit tests (6963+ tests, 575 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (6984+ tests, 577 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths
│ │ └── settings/ # WebLlmPanel, AiSections
│ └── e2e/ # Playwright specs + helpers.ts
Expand Down Expand Up @@ -712,7 +712,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
| `scorecard` | weekly + `main` push | OpenSSF Scorecard — SARIF uploaded to GitHub Code Scanning |

**Current test metrics (2026-08-21, source-synchronized; CI remains authoritative for pass/fail):**
- **6963+ unit tests** across **575 test files** — CI is authoritative for pass/fail
- **6984+ unit tests** across **577 test files** — CI is authoritative for pass/fail
- Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics)
- i18n: **2925 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta)

Expand Down
25 changes: 24 additions & 1 deletion docs/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ For historical optimization notes (targets may predate the live workflow), see [

| Tier | Where | Commands / scope |
|------|--------|------------------|
| **Quick (local)** | Developer laptop | `pnpm run ci:prepush` (single-checker typecheck, i18n quality, release/doc truth, and lightweight guardrails); the pre-commit hook runs staged Biome checks; optional targeted `pnpm exec vitest run <path>` for a fast smoke |
| **Quick (local)** | Developer laptop | `pnpm run ci:prepush` (change-aware: single-checker typecheck and i18n/content-guard checks run only when the outgoing change classification requires them, see [`ci:prepush` change-aware routing](#ciprepush-change-aware-routing); release/doc truth and lightweight guardrails run unconditionally); the pre-commit hook runs staged Biome checks; optional targeted `pnpm exec vitest run <path>` for a fast smoke |
| **Heavy (CI)** | `ci.yml` | Vitest **with** `--coverage` and thresholds, Playwright E2E (`CI=true`) including **mobile emulation** (Pixel 5 / Chromium), Lighthouse CI, Storybook static build, bundle budget + analyze. Mutation testing (Stryker) is **not** part of this pipeline — see [Mutation testing status](#mutation-testing-status). |

**Merge readiness:** A green workflow run on the PR/branch matters more than reproducing every E2E or LHCI step locally. Use CI **artifacts** (Playwright HTML report, coverage, Lighthouse output) to debug failures.
Expand Down Expand Up @@ -306,6 +306,29 @@ pnpm exec vitest run <path> # optional targeted smoke, no coverage
Playwright E2E, Lighthouse, Storybook, and full-suite coverage are intentionally omitted from
the local block above; GitHub Actions owns those heavy checks on this hardware.

### `ci:prepush` change-aware routing

`pnpm run ci:prepush` always resolves a change classification from the outgoing evidence first
(`scripts/ci-prepush-classifier.mjs`), then runs docs/release-truth, CSP, desktop-import boundary,
native-readiness, and dependency-state checks unconditionally on every invocation. It does **not**
run Biome lint — full-repository lint stays CI-owned (`quality` job); only staged files are linted
locally, by the separate pre-commit hook (`lint-staged`). Two check groups are conditional on the
change classification instead of always running:

- **TypeScript (single-checker)** — skipped, reporting `DEFERRED_TO_REQUIRED_CI`, when the
classification is `DOCS_ONLY`, `WORKFLOW_ONLY`, `NON_CODE_ONLY`, `RUST_TAURI`, `TOOLING`, or
non-TypeScript `TEST_ONLY`. Runs for every other classification, including `AMBIGUOUS`/`MIXED`.
- **i18n (key parity, bundle rebuild, translation quality) and content-guard** — run only when the
changed files match their own governed paths or implementation files
(`scripts/ci-prepush-check-registry.mjs`), independent of the TypeScript decision above.

**Fail-closed by design:** whenever outgoing path evidence is incomplete — the manual committed
range can't be resolved, a Git diff command fails, or pre-push-hook evidence reports partial path
completeness (e.g. a tag-only push) — the gate does not defer anything; it falls back to running
every check, matching a `--full` invocation. Deferring a check locally never changes what required
GitHub CI validates: the complete lint, TypeScript, and i18n checks always run in CI regardless of
what the local gate ran or deferred, and CI remains the merge authority.

On standard hardware, or when debugging a build-affecting change, run the build-specific checks
separately; CI remains authoritative for the complete build and artifact checks:

Expand Down
1 change: 1 addition & 0 deletions scripts/ci-prepush-check-registry.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export function shouldRunAdmissionCheck(name: string, files: readonly string[]): boolean;
Comment thread
qnbs marked this conversation as resolved.
44 changes: 44 additions & 0 deletions scripts/ci-prepush-check-registry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const routingAuthority = 'scripts/ci-prepush-check-registry.mjs';
const runnerAuthority = 'scripts/ci-prepush-lowend.mjs';
const i18nPolicyFiles = new Set([
'scripts/check-i18n-keys.mjs',
'scripts/i18n-locales.mjs',
'scripts/build-i18n.mjs',
'scripts/i18n-quality-report.mjs',
]);

// QNBS-v3: not exported — shouldRunAdmissionCheck is the public API, nothing else consumes this.
const admissionCheckRegistry = Object.freeze([
{
name: 'i18n',
matches: (file) =>
file.startsWith('locales/') ||
file.startsWith('public/locales/') ||
i18nPolicyFiles.has(file),
implementationFiles: new Set([
routingAuthority,
runnerAuthority,
'scripts/ci-prepush-classifier.mjs',
'scripts/ci-prepush-range-resolver.mjs',
]),
Comment thread
qnbs marked this conversation as resolved.
},
{
name: 'contentGuard',
matches: (file) =>
file === 'scripts/content-guard.mjs' ||
file.startsWith('community-templates/') ||
file.startsWith('public/community-templates/'),
implementationFiles: new Set([
routingAuthority,
runnerAuthority,
'scripts/ci-prepush-classifier.mjs',
'scripts/ci-prepush-range-resolver.mjs',
]),
Comment thread
qnbs marked this conversation as resolved.
},
]);

export function shouldRunAdmissionCheck(name, files) {
const entry = admissionCheckRegistry.find((candidate) => candidate.name === name);
if (!entry) throw new Error(`unknown local admission check: ${name}`);
return files.some((file) => entry.matches(file) || entry.implementationFiles.has(file));
}
27 changes: 27 additions & 0 deletions scripts/ci-prepush-classifier.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export type ChangeKind =
| 'NO_CHANGES'
| 'DOCS_ONLY'
| 'WORKFLOW_ONLY'
| 'NON_CODE_ONLY'
| 'RUST_TAURI'
| 'TOOLING'
| 'TEST_ONLY'
| 'TYPESCRIPT_APPLICATION'
| 'DEPENDENCY_TOOLCHAIN'
| 'BUILD_CONFIGURATION'
| 'AMBIGUOUS'
| 'MIXED';

export interface ChangeClassification {
readonly kind: ChangeKind;
readonly categories: readonly string[];
readonly files: readonly string[];
}

export function classifyFile(file: string): string;
export function classifyChangedFiles(files: readonly string[]): ChangeClassification;
export function requiresTypecheck(
classification: ChangeClassification,
options?: { readonly full?: boolean },
): boolean;
export function manualAdmissionNeedsFullValidation(rangeResolved: boolean): boolean;
Loading
Loading