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 @@ -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-6988%2B_%2F_578_files-22C55E" alt="6988+ tests / 578 files">
<img src="https://img.shields.io/badge/Tests-6998%2B_%2F_578_files-22C55E" alt="6998+ tests / 578 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 (6988+ tests / 578 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Testing** | Vitest 4.x (6998+ tests / 578 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 (6988+ tests, 578 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (6998+ tests, 578 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):**
- **6988+ unit tests** across **578 test files** — CI is authoritative for pass/fail
- **6998+ unit tests** across **578 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
13 changes: 13 additions & 0 deletions scripts/ci-prepush-lowend.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ async function main() {
console.log(
'[local-admission] change evidence incomplete or unresolved; using conservative full admission',
);
// QNBS-v3: informational only — required CI remains authoritative regardless of this signal.
if (manualEvidence.workingTreeState === 'DIVERGED')
report(
'Working tree vs. push',
'DIVERGED',
'local results do not verify the exact pushed commit(s); required CI remains authoritative',
);
else if (manualEvidence.workingTreeState === 'UNKNOWN')
report(
'Working tree vs. push',
'UNKNOWN',
'could not determine whether local results reflect the exact pushed commit(s); required CI remains authoritative',
);

if (!ensureDependencyState()) {
report('Dependency state', 'FAIL');
Expand Down
3 changes: 3 additions & 0 deletions scripts/ci-prepush-range-resolver.d.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { WorkingTreeState } from './signing/signing-core.d.mts';

export interface ManualChangeEvidence {
readonly files: readonly string[];
readonly rangeResolved: boolean;
readonly workingTreeState: WorkingTreeState;
}

export interface ManualRangeDependencies {
Expand Down
21 changes: 16 additions & 5 deletions scripts/ci-prepush-range-resolver.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,20 @@ export function changedFilesFromManualRange(dependencies = {}) {
const diffNames = dependencies.diffNames ?? defaultDiffNames;
const workingTreeFiles = dependencies.workingTreeFiles ?? defaultWorkingTreeFiles;

// QNBS-v3: no push event or localSha exists in this mode, so nothing is ever compared.
const upstream = resolveUpstream();
if (!upstream) return { files: [], rangeResolved: false };
if (!upstream) return { files: [], rangeResolved: false, workingTreeState: 'NOT_APPLICABLE' };
const diffFiles = diffNames(`${upstream}..HEAD`);
if (diffFiles === null) return { files: [], rangeResolved: false };
if (diffFiles === null)
return { files: [], rangeResolved: false, workingTreeState: 'NOT_APPLICABLE' };
const workingFiles = workingTreeFiles();
if (workingFiles === null) return { files: [], rangeResolved: false };
return { files: diffFiles.concat(workingFiles), rangeResolved: true };
if (workingFiles === null)
return { files: [], rangeResolved: false, workingTreeState: 'NOT_APPLICABLE' };
return {
files: diffFiles.concat(workingFiles),
rangeResolved: true,
workingTreeState: 'NOT_APPLICABLE',
};
}

export function resolveManualEvidence(evidenceFile, dependencies = {}) {
Expand All @@ -63,5 +70,9 @@ export function resolveManualEvidence(evidenceFile, dependencies = {}) {
const evidence = resolveEvidence(readEvidenceFile(evidenceFile), process.cwd());
if (evidence.evidenceState !== 'RESOLVED') throw new Error(evidence.reason ?? 'invalid evidence');
// QNBS-v3: pathEvidenceState, not evidenceState, tells us whether changedFiles is trustworthy.
return { files: evidence.changedFiles, rangeResolved: evidence.pathEvidenceState === 'COMPLETE' };
return {
files: evidence.changedFiles,
rangeResolved: evidence.pathEvidenceState === 'COMPLETE',
workingTreeState: evidence.workingTreeState,
};
}
10 changes: 10 additions & 0 deletions scripts/signing/signing-core.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -79,24 +79,34 @@ export function writePrePushEvidenceFile(
file: string,
input: string | string[] | RefUpdate[],
): void;
// QNBS-v3: diagnostic-only dimension, independent of evidenceState/pathEvidenceState validity.
export type WorkingTreeState = 'MATCHES' | 'DIVERGED' | 'NOT_APPLICABLE' | 'UNKNOWN';
export interface PushEvidenceUpdate extends RefUpdate {
base?: string;
disposition: 'DELETED' | 'TAG' | 'NEW_BRANCH' | 'UPDATED';
workingTreeState: WorkingTreeState;
}
export interface PushEvidence {
updates: PushEvidenceUpdate[];
changedFiles: string[];
evidenceState: 'RESOLVED' | 'INVALID';
pathEvidenceState: 'COMPLETE' | 'PARTIAL';
workingTreeState: WorkingTreeState;
reason?: string;
}
export function computeWorkingTreeState(
sha: string,
cwd?: string,
dependencies?: { runGit?: (args: string[], options?: GitOptions) => GitResult },
): WorkingTreeState;
export function resolvePushEvidence(
input: string | string[] | RefUpdate[],
cwd?: string,
dependencies?: {
commitExists?: (sha: string) => boolean;
objectExists?: (sha: string) => boolean;
changedFilesBetween?: (base: string, head: string) => string[];
worktreeMatchesCommit?: (sha: string) => WorkingTreeState;
},
): PushEvidence;
export function selectIntroducedCommits(commits: string[], reachableFromBase: string[]): string[];
Expand Down
37 changes: 35 additions & 2 deletions scripts/signing/signing-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,30 @@ function changedFilesBetween(base, head, cwd) {
return result.stdout.split('\0').filter((path) => path.length > 0);
}

// QNBS-v3: diagnostic-only signal; never throws, so it cannot corrupt canonical evidence validity.
export function computeWorkingTreeState(sha, cwd, dependencies = {}) {
const runGitFn = dependencies.runGit ?? runGit;
let result;
try {
// QNBS-v3: intentionally tracked-content only -- untracked files never make MATCHES a proof.
result = runGitFn(['diff', '--quiet', sha, '--'], { cwd });
Comment thread
qnbs marked this conversation as resolved.
} catch {
return 'UNKNOWN';
}
if (result.error) return 'UNKNOWN';
if (result.status === 0) return 'MATCHES';
if (result.status === 1) return 'DIVERGED';
return 'UNKNOWN';
}

function aggregateWorkingTreeState(evidenceUpdates) {
const relevant = evidenceUpdates.filter((update) => update.workingTreeState !== 'NOT_APPLICABLE');
if (relevant.length === 0) return 'NOT_APPLICABLE';
if (relevant.some((update) => update.workingTreeState === 'DIVERGED')) return 'DIVERGED';
if (relevant.some((update) => update.workingTreeState === 'UNKNOWN')) return 'UNKNOWN';
return 'MATCHES';
}

export function resolvePushEvidence(input, cwd = process.cwd(), dependencies = {}) {
try {
const updates = validatedPrePushUpdates(input);
Expand All @@ -341,17 +365,23 @@ export function resolvePushEvidence(input, cwd = process.cwd(), dependencies = {
((sha) => isSha(sha) && runGit(['cat-file', '-e', `${sha}^{object}`], { cwd }).status === 0);
const resolveFiles =
dependencies.changedFilesBetween ?? ((base, head) => changedFilesBetween(base, head, cwd));
const matchesWorktree =
dependencies.worktreeMatchesCommit ?? ((sha) => computeWorkingTreeState(sha, cwd));
const changedFiles = new Set();
const evidenceUpdates = [];
for (const update of updates) {
if (isZeroSha(update.localSha)) {
evidenceUpdates.push({ ...update, disposition: 'DELETED' });
evidenceUpdates.push({ ...update, disposition: 'DELETED', workingTreeState: 'NOT_APPLICABLE' });
continue;
}
if (update.remoteRef.startsWith('refs/tags/')) {
if (!objectExists(update.localSha))
throw new Error(`local outgoing object is unavailable for ${update.localRef}`);
evidenceUpdates.push({ ...update, disposition: 'TAG' });
evidenceUpdates.push({
...update,
disposition: 'TAG',
workingTreeState: matchesWorktree(update.localSha),
});
continue;
}
if (!commitExists(update.localSha))
Expand All @@ -364,6 +394,7 @@ export function resolvePushEvidence(input, cwd = process.cwd(), dependencies = {
...update,
base,
disposition: isZeroSha(update.remoteSha) ? 'NEW_BRANCH' : 'UPDATED',
workingTreeState: matchesWorktree(update.localSha),
});
}
// QNBS-v3: tag updates prove object validity but not a complete changed-path set.
Expand All @@ -375,13 +406,15 @@ export function resolvePushEvidence(input, cwd = process.cwd(), dependencies = {
changedFiles: [...changedFiles],
evidenceState: 'RESOLVED',
pathEvidenceState,
workingTreeState: aggregateWorkingTreeState(evidenceUpdates),
};
} catch (error) {
return {
updates: [],
changedFiles: [],
evidenceState: 'INVALID',
pathEvidenceState: 'PARTIAL',
workingTreeState: 'NOT_APPLICABLE',
reason: error instanceof Error ? error.message : 'invalid push evidence',
};
}
Expand Down
Loading
Loading