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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,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-2937_keys-0EA5E9" alt="i18n 19 locales — 2937 keys">
<img src="https://img.shields.io/badge/Tests-7369%2B_%2F_595_files-22C55E" alt="7369+ tests / 595 files">
<img src="https://img.shields.io/badge/Tests-7370%2B_%2F_595_files-22C55E" alt="7370+ tests / 595 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 @@ -511,7 +511,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`) | 2937 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 (7369+ tests / 595 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Testing** | Vitest 4.x (7370+ tests / 595 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 @@ -549,7 +549,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
│ ├── unit/ # Vitest unit tests (7369+ tests, 595 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (7370+ tests, 595 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 @@ -711,7 +711,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-30, source-synchronized; CI remains authoritative for pass/fail):**
- **7369+ unit tests** across **595 test files** — CI is authoritative for pass/fail
- **7370+ unit tests** across **595 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: **2937 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
98 changes: 98 additions & 0 deletions config/pr-size-exceptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,104 @@
"docs/native/UI-DOMAIN-STATE-CLASSIFICATION.md"
],
"reason": "One-time S5 R-15 normative secure-storage contract whose size grew through iterative security, durability, migration, recovery, and data-integrity review convergence. The exception is restricted to the single contract document plus its two minimal ledger/classification integrations; no executable or production source changes are admitted."
},
{
"id": "pr-583-532-e2e-startup-determinism",
"repository": "qnbs/WorldScript-Studio",
"prNumber": 583,
"baseRef": "main",
"headRef": "fix/532-e2e-startup-determinism",
"maxFiles": 65,
"maxCommits": 14,
"maxNonExemptMeaningfulLines": 1611,
"supplementalLineAllowances": [],
"allowedPaths": [
"README.md",
"app/listenerMiddleware.ts",
"components/settings/FactoryResetDangerZone.tsx",
"hooks/useFactoryReset.ts",
"hooks/useSettingsView.ts",
"locales/ar/settings.json",
"locales/ar/sidebar.json",
"locales/de/settings.json",
"locales/de/sidebar.json",
"locales/el/settings.json",
"locales/en/settings.json",
"locales/es/settings.json",
"locales/es/sidebar.json",
"locales/eu/settings.json",
"locales/eu/sidebar.json",
"locales/fa/settings.json",
"locales/fa/sidebar.json",
"locales/fi/settings.json",
"locales/fi/sidebar.json",
"locales/fr/settings.json",
"locales/fr/sidebar.json",
"locales/he/settings.json",
"locales/he/sidebar.json",
"locales/hu/settings.json",
"locales/hu/sidebar.json",
"locales/is/settings.json",
"locales/is/sidebar.json",
"locales/it/settings.json",
"locales/it/sidebar.json",
"locales/ja/settings.json",
"locales/ja/sidebar.json",
"locales/ko/settings.json",
"locales/ko/sidebar.json",
"locales/pt/settings.json",
"locales/pt/sidebar.json",
"locales/ru/settings.json",
"locales/ru/sidebar.json",
"locales/sv/settings.json",
"locales/sv/sidebar.json",
"locales/zh/settings.json",
"locales/zh/sidebar.json",
"packages/worker-bus/src/deadLetterQueue.ts",
"public/locales/ar/bundle.json",
Comment thread
qnbs marked this conversation as resolved.
"public/locales/de/bundle.json",
"public/locales/el/bundle.json",
"public/locales/en/bundle.json",
"public/locales/es/bundle.json",
"public/locales/eu/bundle.json",
"public/locales/fa/bundle.json",
"public/locales/fi/bundle.json",
"public/locales/fr/bundle.json",
"public/locales/he/bundle.json",
"public/locales/hu/bundle.json",
"public/locales/is/bundle.json",
"public/locales/it/bundle.json",
"public/locales/ja/bundle.json",
"public/locales/ko/bundle.json",
"public/locales/pt/bundle.json",
"public/locales/ru/bundle.json",
"public/locales/sv/bundle.json",
"public/locales/zh/bundle.json",
"services/ai/aiInferenceCacheService.ts",
"services/crossProjectIndexService.ts",
"services/diagnostics/logSinks.ts",
"services/factoryResetService.ts",
"services/localFirst/docPersistence.ts",
"services/loraAdapterService.ts",
"services/proForge/proForgeHistoryStore.ts",
"services/proForge/proForgeMemoryBank.ts",
"services/sceneRevisionService.ts",
"services/storage/idbCore.ts",
"services/storage/idbResetGate.ts",
"tests/e2e/onboarding-entry-precondition.spec.ts",
"tests/unit/aiInferenceCacheService.test.ts",
"tests/unit/factoryResetService.test.ts",
"tests/unit/hooks/useSettingsView.test.ts",
"tests/unit/listenerMiddleware.test.ts",
"tests/unit/localFirst/docPersistence.test.ts",
"tests/unit/loraAdapterService.test.ts",
"tests/unit/services/ai/aiInferenceCacheServiceResetRetry.test.ts",
"tests/unit/settings/EncryptionRecoveryModal.test.tsx",
"tests/unit/settings/IdbUnlockModal.test.tsx",
"tests/unit/settings/SettingsModals.test.tsx",
"tests/unit/storage/idbResetGate.test.ts"
],
"reason": "PR #583 (issue #532 E2E startup/navigation nondeterminism) grew through iterative review convergence into a full async, generation/epoch-based IDB reset-quiescence contract covering every long-lived connection in the app (9 service modules plus the shared gate itself). The gate fails closed — beginIdbReset() rejects if any registered closer fails, after every closer has still had its chance to run, so wipeAllAppData() aborts before any database deletion on an unproven teardown — and a closer registered while a reset is draining joins that same awaited barrier instead of racing ahead of it as fire-and-forget. A centralized beginIdbOpenAdmission()/isIdbOpenStillValid() pair closes a further gap: an open that STARTS after a reset already bumped the generation would otherwise still match at completion, so every opener now also refuses to start a fresh open while a reset is in progress. Every reset closer also invalidates its own module's pending open flight (not just the cached database), so the first post-reset caller starts a genuinely fresh flight instead of reusing one already doomed to a generation-mismatch rejection. deleteAllIndexedDBDatabases() uses Promise.allSettled so a fast-rejecting deletion cannot release the gate while another deletion is still outstanding, and only targets database names it can prove it owns (exact KNOWN_DB_NAMES plus the worldscript-localfirst- prefix), never assuming ownership just because native enumeration returned a name. Also closes several stale-open-completion single-flight races via an identity token (proForgeHistoryStore, loraAdapterService, sceneRevisionService, logSinks, crossProjectIndexService, proForgeMemoryBank, DeadLetterQueue), a synchronous indexedDB.open() throw that could permanently memoize a rejected open promise (DeadLetterQueue), and a test-reset helper that swapped the fake IndexedDB factory without first releasing its own cached connection (loraAdapterService). persistProjectDoc() returns a distinct-identity transient NOOP when a reset is in progress (never the shared NOOP_PERSISTENCE singleton), so a local-first handle cached during an active reset is not mistaken for an intentional NOOP and reused indefinitely after the reset ends. This entry was fully recomputed (not carried forward) after #583 was actually rebased onto current main, which by then already carried PR #592's independent factory-reset persistence-admission/drain-coordinator fix (issues #591/#593) — the two PRs modify overlapping files (app/listenerMiddleware.ts, services/factoryResetService.ts, services/crossProjectIndexService.ts, tests/unit/factoryResetService.test.ts, tests/unit/listenerMiddleware.test.ts, tests/e2e/onboarding-entry-precondition.spec.ts) and were reconciled by layering both mechanisms: #592's isFactoryResetInProgress()/coordinator-draining gate (blocks new Redux-listener-level writes, drains in-flight ones) runs first inside wipeAllAppData(), then this PR's beginIdbReset() force-closes every other long-lived IDB connection the coordinators do not track. The rebase also revealed that PR #590 (merged earlier, unrelated) had already independently shipped the same locale-independent Settings/mobile-\"More\"-button navigation fix this PR originally introduced via components/SettingsView.tsx, components/settings/SettingsModals.tsx, components/settings/DataSection.tsx, components/Sidebar.tsx, and tests/e2e/helpers.ts — parallel convergent evolution left this PR's own changes to those five files fully superseded (zero net diff against current main), so they are correctly absent from allowedPaths below; every remaining path was independently re-verified as a genuine, currently non-empty diff. Final measured diff against current main: 65 governed files (84 incl. generated locale bundles), 1611 meaningful lines, 14 commits — exact measured ceilings freshly computed via check-pr-size.mjs itself, no speculative headroom. This recompute also found and fixed a latent bug in check-pr-size.mjs itself, exposed for the first time by an exception whose own ceiling (65/1611/14) legitimately exceeds TIERS.absolute (30/3000/15): when exceptionWithinLimits was true, evaluatePrSize() fell through to selectSeverity() against the fixed absolute tier instead of treating the exception's own ceiling as authoritative, so a fully-satisfied exception with a wide-enough scope would still report blocking:true — neither #539 (maxFiles:30, at the absolute tier's own boundary) nor #564 (maxFiles:3, well under it) had ever exercised this path. Fixed to short-circuit on exception.entry directly; a regression test covers a wide exception ceiling exceeding the fixed tier. Review-thread state was not re-verified as part of this recompute and must be checked live against the current PR before merge, independent of this file-scope/line-ceiling exception. allowedPaths lists all 84 changed paths, not just the 65 governed ones: check-pr-size.mjs's path-scope match (pathScopeMatch) requires every path in the raw, unfiltered diff to be present in allowedPaths for the exception to apply at all, even though the separate governed-file-count and meaningful-line-count checks (maxFiles, maxNonExemptMeaningfulLines) already exclude generated locale bundles via isGovernanceExcluded() — omitting the 19 public/locales/*/bundle.json paths here would make the exception silently fail to apply. The 17 sidebar.json files show only a trailing-newline diff: they lacked a final newline on main already, and Biome's format-on-commit hook re-adds one the moment any of them is staged for any reason in this PR — content is otherwise byte-identical to main, and this could not be avoided without skipping the pre-commit hook. No file outside this exact list is admitted; the ceiling is a one-time allowance for this PR's actual, fully-reviewed, freshly-rebased diff, not a new target."
}
]
}
24 changes: 12 additions & 12 deletions scripts/check-pr-size.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -460,18 +460,18 @@ export function evaluatePrSize(base, head, dependencies = {}) {
);
return count <= allowance.maxMeaningfulLines;
}));
const severity =
exception.entry && !exceptionWithinLimits
? {
tier: 'absolute',
blocking: true,
limits: {
files: exception.entry.maxFiles,
lines: exception.entry.maxNonExemptMeaningfulLines,
commits: exception.entry.maxCommits,
},
}
: selectSeverity({ fileCount, lineCount: nonExemptLineCount, commitCount, allDocs });
// QNBS-v3: an exception's own ceiling can legitimately exceed TIERS.absolute (that is the whole point of granting one) -- falling through to selectSeverity() here would re-check the raw counts against the fixed 30/3000/15 tier and block anyway, even though the PR-specific ceiling was satisfied.
const severity = exception.entry
? {
tier: exceptionWithinLimits ? 'exception' : 'absolute',
blocking: !exceptionWithinLimits,
limits: {
files: exception.entry.maxFiles,
lines: exception.entry.maxNonExemptMeaningfulLines,
commits: exception.entry.maxCommits,
},
}
: selectSeverity({ fileCount, lineCount: nonExemptLineCount, commitCount, allDocs });
return {
ok: true,
fileCount,
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/tooling/checkPrSize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,35 @@ describe('evaluatePrSize', () => {
);
expect(tooManyCommits.severity?.blocking).toBe(true);
});

// QNBS-v3: an exception's whole purpose is authorizing a PR past TIERS.absolute (30/3000/15) -- a ceiling wide enough to matter must not then get re-checked against that same fixed tier and blocked anyway.
it('does not block a PR within a wide exception ceiling that exceeds the fixed absolute tier', () => {
const rows: NumstatRow[] = Array.from({ length: 40 }, (_, i) => ({
path: `scripts/tool-${i}.mjs`,
added: 100,
removed: 0,
}));
const wide = {
...exception,
maxFiles: 65,
maxCommits: 20,
maxNonExemptMeaningfulLines: 4000,
supplementalLineAllowances: [],
allowedPaths: rows.map((row) => row.path),
};
const result = evaluatePrSize(
'base',
'head',
exceptionDependencies({
rows,
changedPaths: rows.map((row) => row.path),
commitCount: 18,
registry: { schemaVersion: 1, exceptions: [wide] },
}),
);
expect(result.severity?.blocking).toBe(false);
expect(result.severity?.tier).toBe('exception');
});
});
});

Expand Down
Loading