Skip to content
Merged
10 changes: 5 additions & 5 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-2942_keys-0EA5E9" alt="i18n 19 locales — 2942 keys">
<img src="https://img.shields.io/badge/Tests-7644%2B_%2F_604_files-22C55E" alt="7644+ tests / 604 files">
<img src="https://img.shields.io/badge/Tests-7649%2B_%2F_604_files-22C55E" alt="7649+ tests / 604 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`) | 2942 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 (7644+ tests / 604 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Testing** | Vitest 4.x (7649+ tests / 604 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 (7644+ tests, 604 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (7649+ tests, 604 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 @@ -713,8 +713,8 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
<!-- bundle-budget:source-of-truth -->
Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**.

**Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):**
- **7644+ unit tests** across **604 test files** — CI is authoritative for pass/fail
**Current test metrics (2026-09-10, source-synchronized; CI remains authoritative for pass/fail):**
- **7649+ unit tests** across **604 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: **2942 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
40 changes: 28 additions & 12 deletions public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ const PRECACHE_URLS = [
..._WB_MANIFEST.map((entry) => (typeof entry === 'string' ? entry : entry.url)),
];

// QNBS-v3: marker written into CACHE_STATIC only once precache fully succeeds; activate checks it before pruning an older generation.
const PRECACHE_ADMISSION_URL = `${BASE}__sw-precache-complete__`;

// ── Max age / entry limits ───────────────────────────────────
const MAX_AGE_DYNAMIC = 7 * 24 * 60 * 60 * 1000; // 7 days
const MAX_AGE_IMAGES = 30 * 24 * 60 * 60 * 1000; // 30 days
Expand Down Expand Up @@ -112,16 +115,22 @@ async function offlineFallback(request) {
// INSTALL — Precache shell
// ════════════════════════════════════════════════════════════
self.addEventListener('install', (event) => {
// QNBS-v3: activates immediately, no waiting — register-sw.ts owns the bounded pre-reload flush mitigation, residual risk tracked in #518.
self.skipWaiting();
// QNBS-v3: Never precache inside Tauri — the desktop app serves its shell from the bundle.
if (IS_TAURI) return;
if (IS_TAURI) {
self.skipWaiting();
return;
}
event.waitUntil(
caches.open(CACHE_STATIC)
.then((cache) => cache.addAll(PRECACHE_URLS))
.then((cache) => cache.addAll(PRECACHE_URLS).then(() => cache.put(PRECACHE_ADMISSION_URL, new Response('ok'))))
Comment thread
qnbs marked this conversation as resolved.
.then(() => {
// QNBS-v3: only claim the update once precache fully succeeded; register-sw.ts owns the pre-reload flush mitigation for the resulting reload.
self.skipWaiting();
})
.catch((err) => {
// Some precache entries (e.g. offline.html) may not exist yet; continue anyway
swLogger.warn('Precache partial failure (non-fatal):', err);
// QNBS-v3: rethrow so install() itself fails — a worker that never reaches "installed" can never be waited-on, activated, or SKIP_WAITING'd.
swLogger.warn('Precache failed — this installation will not complete:', err);
throw err;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
})
Comment thread
qnbs marked this conversation as resolved.
Comment thread
qnbs marked this conversation as resolved.
);
});
Expand Down Expand Up @@ -154,19 +163,26 @@ self.addEventListener('activate', (event) => {
return;
}
event.waitUntil(
caches.keys()
.then((cacheNames) =>
Promise.all(
(async () => {
// QNBS-v3: admission gate — only prune older caches once this generation's own precache is proven complete; otherwise leave every cache untouched.
const staticCache = await caches.open(CACHE_STATIC);
const precacheComplete = Boolean(await staticCache.match(PRECACHE_ADMISSION_URL));
if (precacheComplete) {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames
// QNBS-v3: prune only owned-and-stale — never delete a cache we don't positively own.
.filter((name) => isWorldScriptOwnedCache(name) && !ALL_CACHES.includes(name))
.map((name) => {
swLogger.log('Pruning old cache:', name);
return caches.delete(name);
})
)
)
.then(() => self.clients.claim())
);
} else {
swLogger.warn('Skipping cache-generation cutover: this generation\'s precache never completed');
}
await self.clients.claim();
Comment thread
qnbs marked this conversation as resolved.
})()
);
});

Expand Down
Loading
Loading