Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ jobs:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
Expand Down Expand Up @@ -195,6 +195,9 @@ jobs:
- name: Native-readiness roadmap policy gate
run: pnpm run native-readiness:check

- name: Workflow-policy structural gate (permissions, needs graph, action pins)
run: pnpm run workflow-policy:check
Comment thread
qnbs marked this conversation as resolved.
Outdated

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

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-7005%2B_%2F_578_files-22C55E" alt="7005+ tests / 578 files">
<img src="https://img.shields.io/badge/Tests-7064%2B_%2F_579_files-22C55E" alt="7064+ tests / 579 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 (7005+ tests / 578 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Testing** | Vitest 4.x (7064+ tests / 579 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 (7005+ tests, 578 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (7064+ tests, 579 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):**
- **7005+ unit tests** across **578 test files** — CI is authoritative for pass/fail
- **7064+ unit tests** across **579 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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"workflow-policy:check": "node scripts/workflow-policy-check.mjs",
"suppressions:check": "node scripts/check-suppressions.mjs",
"token:audit": "node scripts/audit-tokens.mjs",
"guardrail:desktop-imports": "node scripts/check-tauri-import-boundary.mjs",
Expand Down Expand Up @@ -202,7 +203,8 @@
"vitest": "^4.1.10",
"wait-on": "^9.1.0",
"wrangler": "^4.120.1",
"y-protocols": "^1.0.7"
"y-protocols": "^1.0.7",
"yaml": "^2.9.0"
},
"simple-git-hooks": {
"pre-commit": "node scripts/hooks/pre-commit.mjs",
Expand Down
6 changes: 4 additions & 2 deletions pnpm-lock.yaml

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

13 changes: 13 additions & 0 deletions scripts/ci-prepush-check-registry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ const admissionCheckRegistry = Object.freeze([
'scripts/ci-prepush-range-resolver.mjs',
]),
},
{
name: 'workflowPolicy',
// QNBS-v3: composite actions carry the same uses:-pin risk as workflows themselves.
matches: (file) =>
file.startsWith('.github/workflows/') || file.startsWith('.github/actions/'),
implementationFiles: new Set([
routingAuthority,
runnerAuthority,
'scripts/ci-prepush-classifier.mjs',
'scripts/ci-prepush-range-resolver.mjs',
'scripts/workflow-policy-check.mjs',
]),
},
]);

export function shouldRunAdmissionCheck(name, files) {
Expand Down
3 changes: 3 additions & 0 deletions scripts/ci-prepush-lowend.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ async function main() {
if (shouldRunAdmissionCheck('contentGuard', classification.files) || full)
await runCheck('Content guard', () => runNodeScript('scripts/content-guard.mjs'));

if (shouldRunAdmissionCheck('workflowPolicy', classification.files) || full)
await runCheck('Workflow policy', () => runNodeScript('scripts/workflow-policy-check.mjs'));

if (typecheckRequired) {
await runCheck('TypeScript (single checker)', () =>
// QNBS-v3: one checker bounds memory use on constrained developer machines.
Expand Down
121 changes: 121 additions & 0 deletions scripts/workflow-policy-check.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { Document, LineCounter } from 'yaml';

export interface WorkflowPolicyFailure {
file: string;
message: string;
}

export interface ListWorkflowFilesDependencies {
readdirSync?: (dir: string) => string[];
}

export function listWorkflowFiles(
root?: string,
dependencies?: ListWorkflowFilesDependencies,
): string[];

export interface DirEntryLike {
name: string;
isDirectory(): boolean;
}

export interface ListActionFilesDependencies {
// QNBS-v3: recursive discovery needs Dirent-shaped entries, not listWorkflowFiles' flat string[].
readdirSync?: (dir: string, options: { withFileTypes: true }) => DirEntryLike[];
}

export function listActionFiles(
root?: string,
dependencies?: ListActionFilesDependencies,
): string[];

export interface ParseWorkflowFileDependencies {
readFileSync?: (filePath: string, encoding: 'utf8') => string;
}

export interface ParsedWorkflowFile {
filePath: string;
content: string;
doc: Document;
lineCounter: LineCounter;
}

export function parseWorkflowFile(
filePath: string,
dependencies?: ParseWorkflowFileDependencies,
): ParsedWorkflowFile;

export function checkTopLevelPermissions(
fileName: string,
doc: Document,
failures: WorkflowPolicyFailure[],
): void;

export function checkJobWriteScopeAllowlist(
fileName: string,
doc: Document,
failures: WorkflowPolicyFailure[],
): void;

export function checkNeedsGraph(
fileName: string,
doc: Document,
failures: WorkflowPolicyFailure[],
): void;

export interface CheckActionPinsOptions {
fileKind?: 'workflow' | 'action';
lineCounter?: LineCounter;
}

export function checkActionPins(
fileName: string,
doc: Document,
failures: WorkflowPolicyFailure[],
options?: CheckActionPinsOptions,
): void;

export function checkAggregatorNeeds(
fileName: string,
doc: Document,
failures: WorkflowPolicyFailure[],
): void;

export function checkPublishingBoundary(
fileName: string,
doc: Document,
failures: WorkflowPolicyFailure[],
): void;

export interface WorkflowTriggers {
workflowDispatch: boolean;
tagPush: boolean;
}

export function getTriggers(doc: Document): WorkflowTriggers;

export type CheckWorkflowFileDependencies = ParseWorkflowFileDependencies;

export function checkWorkflowFile(
filePath: string,
dependencies?: CheckWorkflowFileDependencies,
): WorkflowPolicyFailure[];

export function checkActionFile(
filePath: string,
dependencies?: CheckWorkflowFileDependencies,
): WorkflowPolicyFailure[];

export type CheckAllWorkflowsDependencies = CheckWorkflowFileDependencies &
ListWorkflowFilesDependencies &
ListActionFilesDependencies & {
listWorkflowFiles?: (root: string) => string[];
listActionFiles?: (root: string) => string[];
};

export function checkAllWorkflows(
root?: string,
dependencies?: CheckAllWorkflowsDependencies,
): WorkflowPolicyFailure[];

export function main(): void;
Loading
Loading