Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
62 changes: 56 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,14 @@ jobs:
timeout-minutes: 5
outputs:
tauri: ${{ steps.filter.outputs.tauri }}
crates: ${{ steps.filter.outputs.crates }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- name: Detect src-tauri changes
- name: Detect src-tauri / crates changes
id: filter
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
Expand All @@ -112,15 +113,22 @@ jobs:
BASE="${{ github.event.before }}"
fi
if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ] || ! git cat-file -e "$BASE" 2>/dev/null; then
echo "::notice::No usable base SHA to diff against — defaulting to tauri=true (fail open)"
echo "::notice::No usable base SHA to diff against — defaulting to tauri=true, crates=true (fail open)"
echo "tauri=true" >> "$GITHUB_OUTPUT"
echo "crates=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git diff --name-only "$BASE" "${{ github.sha }}" | grep -qE '^(src-tauri/|\.github/workflows/ci\.yml$)'; then
CHANGED=$(git diff --name-only "$BASE" "${{ github.sha }}")
if grep -qE '^(src-tauri/|\.github/workflows/ci\.yml$)' <<< "$CHANGED"; then
echo "tauri=true" >> "$GITHUB_OUTPUT"
else
echo "tauri=false" >> "$GITHUB_OUTPUT"
fi
if grep -qE '^(crates/|\.github/workflows/ci\.yml$)' <<< "$CHANGED"; then
Comment thread
qnbs marked this conversation as resolved.
Outdated
echo "crates=true" >> "$GITHUB_OUTPUT"
else
echo "crates=false" >> "$GITHUB_OUTPUT"
fi

# ----------------------------------------------------------
# 1. QUALITY GATE: Lint + Typecheck + Tests (parallel matrix)
Expand Down Expand Up @@ -267,6 +275,42 @@ jobs:
working-directory: src-tauri
run: cargo test --locked

# ----------------------------------------------------------
# 1c. CORE-RUST: worldscript-project crate Gate (fmt/check/clippy/test), path-scoped via `changes`
# QNBS-v3: independent workspace from src-tauri (crates/Cargo.toml) — zero GUI/Tauri deps, so no
# GTK/WebKit apt-get install steps are needed here, unlike rust-tauri above.
Comment thread
qnbs marked this conversation as resolved.
Outdated
# ----------------------------------------------------------
core-rust:
Comment thread
qnbs marked this conversation as resolved.
name: 🧩 Core Rust Gate
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [security, changes]
# QNBS-v3: skips for PRs that don't touch crates/** — ci-success treats 'skipped' as pass for this job only
if: needs.changes.outputs.crates == 'true'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable
components: rustfmt, clippy
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: crates -> target
- name: Rust format check
working-directory: crates
run: cargo fmt --check
- name: Rust compile check
working-directory: crates
run: cargo check --locked
- name: Rust clippy
working-directory: crates
run: cargo clippy --locked --all-targets -- -D warnings
- name: Rust tests
working-directory: crates
run: cargo test --locked

# ----------------------------------------------------------
# 2. BUILD: Production build + Pages artifact upload
# QNBS-v3: Stryker mutation removed from the PR/CI pipeline (2026-06-02) — it added noise as a
Expand Down Expand Up @@ -368,16 +412,18 @@ jobs:
path: ./dist

# ----------------------------------------------------------
# 3. CI SUCCESS: single required-status aggregator (security + quality + changes + rust-tauri + build + e2e + vrt)
# 3. CI SUCCESS: single required-status aggregator
# (security + quality + changes + rust-tauri + core-rust + build + e2e + vrt)
# ----------------------------------------------------------
ci-success:
name: ✅ CI Success
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [security, quality, changes, rust-tauri, build, e2e, vrt]
needs: [security, quality, changes, rust-tauri, core-rust, build, e2e, vrt]
if: always()
steps:
# QNBS-v3: rust-tauri may legitimately be 'skipped' (changes.outputs.tauri == 'false') — that's a pass, not a failure
# QNBS-v3: rust-tauri/core-rust may legitimately be 'skipped' (their changes.outputs.* is
# 'false') — that's a pass, not a failure
- name: Verify all required jobs succeeded
run: |
FAIL=0
Expand All @@ -387,6 +433,9 @@ jobs:
if [ "${{ needs.rust-tauri.result }}" != "success" ] && [ "${{ needs.rust-tauri.result }}" != "skipped" ]; then
FAIL=1
fi
if [ "${{ needs.core-rust.result }}" != "success" ] && [ "${{ needs.core-rust.result }}" != "skipped" ]; then
FAIL=1
fi
[ "${{ needs.build.result }}" = "success" ] || FAIL=1
[ "${{ needs.e2e.result }}" = "success" ] || FAIL=1
[ "${{ needs.vrt.result }}" = "success" ] || FAIL=1
Expand All @@ -396,6 +445,7 @@ jobs:
echo " quality: ${{ needs.quality.result }}"
echo " changes: ${{ needs.changes.result }}"
echo " rust-tauri: ${{ needs.rust-tauri.result }} (skipped = OK, src-tauri untouched)"
echo " core-rust: ${{ needs.core-rust.result }} (skipped = OK, crates/ untouched)"
echo " build: ${{ needs.build.result }}"
echo " e2e: ${{ needs.e2e.result }}"
echo " vrt: ${{ needs.vrt.result }}"
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ tests/bench/baseline/

# Tauri build output
src-tauri/target/

# crates/ (Wave 2 Rust Core workspace) build output
crates/target/
.env

test-results/
Expand Down
2 changes: 2 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"!!**/.storybook",
"!!**/backend",
"!!**/src-tauri",
"!!**/crates",
"!!**/tests/fixtures/project-golden-masters",
"!!**/public/sw.js",
"!!**/coverage",
"!!**/playwright-report",
Expand Down
107 changes: 107 additions & 0 deletions crates/Cargo.lock

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

3 changes: 3 additions & 0 deletions crates/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[workspace]
Comment thread
qnbs marked this conversation as resolved.
resolver = "2"
members = ["worldscript-project"]
18 changes: 18 additions & 0 deletions crates/worldscript-project/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "worldscript-project"
version = "0.1.0"
edition = "2021"
rust-version = "1.77.2"
description = "Renderer-neutral WorldScript Studio project schema, validation, migration, and I/O (Wave 2)"
publish = false

[lib]
name = "worldscript_project"

[[bin]]
name = "wsproj"
path = "src/bin/wsproj.rs"

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
75 changes: 75 additions & 0 deletions crates/worldscript-project/src/bin/wsproj.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! Headless CLI proving the Wave 2 lifecycle scenario runs with no GUI/Tauri runtime present.
//! `cargo run --bin wsproj -- demo-lifecycle`

use std::env;
use std::process::ExitCode;
use worldscript_project::schema::{Character, StorySection};
use worldscript_project::{io, migrate_to_latest, validate, ProjectEnvelope, StoryProject};

fn demo_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
let mut project = StoryProject::new("Demo Project", "A logline for the demo.");
project.characters.push(Character {
id: "char-1".to_string(),
name: "Ada".to_string(),
backstory: String::new(),
motivation: String::new(),
appearance: String::new(),
personality_traits: String::new(),
flaws: String::new(),
notes: String::new(),
has_avatar: None,
character_arc: String::new(),
relationships: String::new(),
});
project.manuscript.push(StorySection {
id: "sec-1".to_string(),
title: "Chapter One".to_string(),
content: "It was a dark and stormy night.".to_string(),
summary: None,
notes: None,
word_count: None,
status: None,
});

validate(&project)?;
println!(
"validated: {} character(s), {} section(s)",
project.characters.len(),
project.manuscript.len()
);

let envelope = ProjectEnvelope::current(project);
let mut path = env::temp_dir();
path.push(format!("wsproj-demo-{}.json", std::process::id()));
io::save_project(&path, &envelope)?;
Comment thread
qnbs marked this conversation as resolved.
Outdated
println!("saved to {}", path.display());

let reloaded = io::load_project(&path)?;
assert_eq!(reloaded, envelope, "reload must round-trip exactly");
let migrated = migrate_to_latest(reloaded)?;
Comment thread
qnbs marked this conversation as resolved.
Outdated
validate(&migrated.project)?;
println!(
"reloaded, migrated to schema v{}, re-validated OK",
migrated.schema_version
Comment thread
qnbs marked this conversation as resolved.
Outdated
);

std::fs::remove_file(&path)?;
Ok(())
}

fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
match args.get(1).map(String::as_str) {
Some("demo-lifecycle") => match demo_lifecycle() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("demo-lifecycle failed: {e}");
ExitCode::FAILURE
}
},
_ => {
eprintln!("usage: wsproj demo-lifecycle");
ExitCode::FAILURE
}
}
}
Loading
Loading