Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,9 @@ jobs:
- name: Native-readiness roadmap policy gate
run: pnpm run native-readiness:check

- name: Tauri plugin Rust/npm version-parity gate
run: pnpm run tauri-plugins:check
Comment thread
qnbs marked this conversation as resolved.

- name: Feature parity audit
run: pnpm run parity:check

Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **Tauri plugin Rust/npm version parity restored:** `tauri-plugin-http` and
`tauri-plugin-notification` had drifted ahead of their npm counterparts
(`@tauri-apps/plugin-http`, `@tauri-apps/plugin-notification`) after #661 bumped only the Rust
side, failing every platform's Tauri release build. Bumped the npm packages to match; added
`check-tauri-plugin-versions.mjs`, a cheap CI guard catching this class of mismatch before the
next release tag instead of at tag-triggered release time.

<!-- release-candidate: v1.28.5 -->
## [1.28.5] — 2026-09-09

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-2942_keys-0EA5E9" alt="i18n 19 locales — 2942 keys">
<img src="https://img.shields.io/badge/Tests-7635%2B_%2F_603_files-22C55E" alt="7635+ tests / 603 files">
<img src="https://img.shields.io/badge/Tests-7641%2B_%2F_604_files-22C55E" alt="7641+ 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 @@ -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`) | 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 (7635+ tests / 603 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Testing** | Vitest 4.x (7641+ 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 @@ -550,7 +550,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
│ ├── unit/ # Vitest unit tests (7635+ tests, 603 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (7641+ 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 @@ -715,7 +715,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
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):**
- **7635+ unit tests** across **603 test files** — CI is authoritative for pass/fail
- **7641+ 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
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +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",
"ci:local:full": "pnpm run lint && pnpm run typecheck && pnpm run i18n:check && pnpm run guardrail:desktop-imports && pnpm run native-readiness:check",
"ci:local:full": "pnpm run lint && pnpm run typecheck && pnpm run i18n:check && pnpm run guardrail:desktop-imports && pnpm run native-readiness:check && pnpm run tauri-plugins: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",
"dev:turbo": "turbo run dev --parallel",
Expand All @@ -46,6 +46,7 @@
"csp:check": "node scripts/check-csp-policy.mjs",
"csp:verify": "node scripts/sync-csp.mjs && git diff --exit-code -- index.html nginx.conf public/_headers vercel.json src-tauri/tauri.conf.json && node scripts/check-csp-policy.mjs",
"native-readiness:check": "node scripts/check-native-readiness.mjs",
"tauri-plugins:check": "node scripts/check-tauri-plugin-versions.mjs",
"workflow-policy:check": "node scripts/workflow-policy-check.mjs",
"pr-size:check": "node scripts/check-pr-size.mjs",
"suppressions:check": "node scripts/check-suppressions.mjs",
Expand Down Expand Up @@ -142,8 +143,8 @@
"@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-fs": "^2.5.1",
"@tauri-apps/plugin-http": "^2.5.9",
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-http": "^2.6.0",
"@tauri-apps/plugin-notification": "^2.4.0",
Comment thread
qnbs marked this conversation as resolved.
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tauri-apps/plugin-updater": "^2.11.0",
Expand Down
21 changes: 14 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions scripts/check-tauri-plugin-versions.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function findTauriPluginVersionMismatches(
cargoLock: string,
pkg: { dependencies?: Record<string, string> },
): string[];
71 changes: 71 additions & 0 deletions scripts/check-tauri-plugin-versions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env node
/**
* Verify each Tauri plugin's Rust crate (src-tauri/Cargo.lock) and coupled npm package
* (package.json) share the same major.minor line.
* Run via ci:prepush or manually:
* node scripts/check-tauri-plugin-versions.mjs
*/
// QNBS-v3: tauri build rejects this mismatch too, but only inside the slow, tag-triggered, cross-platform release workflow — mirrors that check cheaply for regular CI (found live: v1.28.5's Rust-only plugin bump via #661 broke every platform's release build).
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.join(__dirname, '..');

const cargoLockPath = path.join(root, 'src-tauri', 'Cargo.lock');
const pkgPath = path.join(root, 'package.json');
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated

// QNBS-v3: only Tauri plugins with a corresponding @tauri-apps/plugin-* npm package are coupled — tauri-plugin-log/window-state/deep-link/single-instance have no JS-side counterpart to drift against.
Comment thread
qnbs marked this conversation as resolved.
Outdated
const PLUGIN_PAIRS = [
['tauri-plugin-dialog', '@tauri-apps/plugin-dialog'],
['tauri-plugin-fs', '@tauri-apps/plugin-fs'],
['tauri-plugin-http', '@tauri-apps/plugin-http'],
['tauri-plugin-notification', '@tauri-apps/plugin-notification'],
['tauri-plugin-process', '@tauri-apps/plugin-process'],
['tauri-plugin-shell', '@tauri-apps/plugin-shell'],
['tauri-plugin-updater', '@tauri-apps/plugin-updater'],
];

function resolvedCargoVersion(cargoLock, crateName) {
const match = cargoLock.match(new RegExp(`name = "${crateName}"\\nversion = "([^"]+)"`));
return match ? match[1] : null;
Comment thread
qnbs marked this conversation as resolved.
Outdated
}

function majorMinor(version) {
const match = version.match(/^(\d+)\.(\d+)/);
return match ? `${match[1]}.${match[2]}` : null;
}

export function findTauriPluginVersionMismatches(cargoLock, pkg) {
const findings = [];
for (const [crateName, npmName] of PLUGIN_PAIRS) {
const rustVersion = resolvedCargoVersion(cargoLock, crateName);
const npmRange = pkg.dependencies?.[npmName];
Comment thread
qnbs marked this conversation as resolved.
Outdated
if (!rustVersion || !npmRange) continue;
const npmVersion = npmRange.replace(/^[\^~]/, '');
Comment thread
qnbs marked this conversation as resolved.
Outdated
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
Comment thread
qnbs marked this conversation as resolved.
Outdated
if (majorMinor(rustVersion) !== majorMinor(npmVersion)) {
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
findings.push(
`${crateName} (Rust ${rustVersion}) vs ${npmName} (npm ${npmVersion}) — major/minor mismatch, "pnpm exec tauri build" rejects this`,
);
}
}
return findings;
}

function main() {
const cargoLock = fs.readFileSync(cargoLockPath, 'utf8');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
const findings = findTauriPluginVersionMismatches(cargoLock, pkg);
if (findings.length > 0) {
process.stderr.write(
`[tauri-plugin-versions] MISMATCH — ${findings.length} finding(s):\n${findings.map((f) => ` - ${f}`).join('\n')}\n`,
);
process.exit(1);
}
process.stdout.write(
`[tauri-plugin-versions] OK — ${PLUGIN_PAIRS.length} plugin pairs checked, all major.minor-aligned.\n`,
);
}

if (path.resolve(process.argv[1] ?? '') === fileURLToPath(import.meta.url)) main();
3 changes: 3 additions & 0 deletions scripts/ci-prepush-lowend.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ async function main() {
runNodeScript('scripts/check-tauri-import-boundary.mjs'),
);
await runCheck('Native readiness', () => runNodeScript('scripts/check-native-readiness.mjs'));
await runCheck('Tauri plugin version parity', () =>
runNodeScript('scripts/check-tauri-plugin-versions.mjs'),
);

if (shouldRunAdmissionCheck('i18n', classification.files) || full) {
await runCheck('i18n key parity', () => runNodeScript('scripts/check-i18n-keys.mjs'));
Expand Down
58 changes: 58 additions & 0 deletions tests/unit/checkTauriPluginVersions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// @vitest-environment node
import { describe, expect, it } from 'vitest';
import { findTauriPluginVersionMismatches } from '../../scripts/check-tauri-plugin-versions.mjs';

function cargoLockEntry(crateName: string, version: string): string {
return `[[package]]\nname = "${crateName}"\nversion = "${version}"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\n`;
}

describe('findTauriPluginVersionMismatches', () => {
it('reports nothing when every plugin pair shares the same major.minor', () => {
const cargoLock = cargoLockEntry('tauri-plugin-http', '2.6.0');
const pkg = { dependencies: { '@tauri-apps/plugin-http': '^2.6.3' } };
expect(findTauriPluginVersionMismatches(cargoLock, pkg)).toEqual([]);
});

// QNBS-v3: reproduces the live v1.28.5 release-build failure — Rust bumped via a Dependabot PR without a matching npm bump.
it('flags a Rust-ahead-of-npm minor-version mismatch, reproducing the v1.28.5 release failure', () => {
const cargoLock =
cargoLockEntry('tauri-plugin-http', '2.6.0') +
cargoLockEntry('tauri-plugin-notification', '2.4.0');
const pkg = {
dependencies: {
'@tauri-apps/plugin-http': '^2.5.9',
'@tauri-apps/plugin-notification': '^2.3.3',
},
};
const findings = findTauriPluginVersionMismatches(cargoLock, pkg);
expect(findings).toHaveLength(2);
expect(findings[0]).toContain('tauri-plugin-http');
expect(findings[1]).toContain('tauri-plugin-notification');
});

it('flags an npm-ahead-of-Rust minor-version mismatch too', () => {
const cargoLock = cargoLockEntry('tauri-plugin-updater', '2.9.0');
const pkg = { dependencies: { '@tauri-apps/plugin-updater': '^2.11.0' } };
const findings = findTauriPluginVersionMismatches(cargoLock, pkg);
expect(findings).toHaveLength(1);
expect(findings[0]).toContain('tauri-plugin-updater');
});

it('ignores a plugin with no npm counterpart declared', () => {
const cargoLock = cargoLockEntry('tauri-plugin-log', '2.7.0');
const pkg = { dependencies: {} };
expect(findTauriPluginVersionMismatches(cargoLock, pkg)).toEqual([]);
});

it('ignores an npm package with no resolved Rust crate in Cargo.lock', () => {
const cargoLock = '';
const pkg = { dependencies: { '@tauri-apps/plugin-http': '^2.6.0' } };
expect(findTauriPluginVersionMismatches(cargoLock, pkg)).toEqual([]);
});

it('tolerates a patch-version difference within the same major.minor', () => {
const cargoLock = cargoLockEntry('tauri-plugin-dialog', '2.7.1');
const pkg = { dependencies: { '@tauri-apps/plugin-dialog': '^2.7.0' } };
expect(findTauriPluginVersionMismatches(cargoLock, pkg)).toEqual([]);
});
});
Loading