Skip to content

Commit 2e4dba4

Browse files
AndrewAltimitAI Agent BotclaudeAI Review AgentAI Pipeline Agent
authored
refactor: structured errors, SDL module split, browser tests, link checker CI (#52)
* refactor: structured errors, SDL module split, browser spec tests, doc index Replace string-based OasisError variants with structured sub-error enums (SdiError, BackendError, ConfigError, VfsError, CommandError, WmError, PluginError, PlatformError). Each sub-error has typed variants for common failure modes plus an Other(String) fallback with From<String> impls for backward compat. Callers can now pattern-match on specific error kinds (e.g., VfsError::NotFound, BackendError::NotConnected) instead of parsing error strings. Split oasis-backend-sdl/src/lib.rs (1867 lines) into shapes.rs, blitting.rs, and input.rs modules. Inherent pub(crate) methods delegate from the single SdiBackend trait impl. Add 26 browser engine spec compliance tests: HTML tokenizer (malformed entities, uppercase tags, script string literals), CSS cascade (universal selectors, compound classes, specificity edge cases, nth-child), layout (nested percentages, min/max-width, display:none), tree builder (implicit head/body, mismatched formatting, p auto-close). Add Document Index sections to CLAUDE.md and AGENTS.md linking to all major docs (ADRs, plans, guides, operations) so agents can navigate without loading everything into context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: add markdown link checker to PR validation and main CI Add md-link-checker step (internal-only mode) after doc build in both pr-validation.yml and main-ci.yml. Checks top-level *.md and docs/ directory, avoiding vendored target/ and node_modules/ by scoping the scan targets explicitly. Fix 3 pre-existing broken links: - AGENTS.md: README anchor #security-advisory → #security-notice - docs/design.md: TOC anchors missing [PLANNED] suffix Update CI pipeline order in CLAUDE.md to reflect current step sequence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address AI review feedback (iteration 1) Automated fix by Claude in response to Gemini/Codex review. Iteration: 1/5 Co-Authored-By: AI Review Agent <noreply@anthropic.com> * fix: resolve CI pipeline failures Automated fix by Claude in response to pipeline failures. Failures addressed: - format - lint - test-suite Actions taken: - Ran autoformat (ruff format, cargo fmt) - Fixed remaining lint issues Iteration: 1/5 Co-Authored-By: AI Pipeline Agent <noreply@anthropic.com> --------- Co-authored-by: AI Agent Bot <ai-agent@localhost> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: AI Review Agent <ai-review-agent@localhost> Co-authored-by: AI Pipeline Agent <ai-pipeline-agent@localhost>
1 parent b6e64a1 commit 2e4dba4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+2779
-1335
lines changed

.github/workflows/main-ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ jobs:
7474
-e RUSTDOCFLAGS="-D warnings"
7575
rust-ci cargo doc --workspace --no-deps
7676
77+
# -- Markdown Link Check ------------------------------------------------
78+
- name: Markdown link check
79+
run: |
80+
FAIL=0
81+
for target in *.md docs/ scripts/psp-scenarios.md; do
82+
[ -e "$target" ] || continue
83+
md-link-checker "$target" --internal-only || FAIL=1
84+
done
85+
exit $FAIL
86+
7787
# -- Tests -------------------------------------------------------------
7888
- name: Test
7989
run: |

.github/workflows/pr-validation.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,24 @@ jobs:
7979
-e RUSTDOCFLAGS="-D warnings"
8080
rust-ci cargo doc --workspace --no-deps
8181
82+
# -- Markdown Link Check ------------------------------------------------
83+
- name: Markdown link check
84+
run: |
85+
set -o pipefail
86+
FAIL=0
87+
# Check top-level markdown and docs/ (skip vendored target/node_modules)
88+
for target in *.md docs/ scripts/psp-scenarios.md; do
89+
[ -e "$target" ] || continue
90+
md-link-checker "$target" --internal-only 2>&1 \
91+
| tee -a link_check_output.txt || FAIL=1
92+
done
93+
if [ $FAIL -ne 0 ]; then
94+
echo "### Markdown Link Check" >> $GITHUB_STEP_SUMMARY
95+
grep -E '(broken link|->.*not found)' link_check_output.txt \
96+
>> $GITHUB_STEP_SUMMARY 2>/dev/null || true
97+
fi
98+
exit $FAIL
99+
82100
# -- Tests -------------------------------------------------------------
83101
- name: Test
84102
run: |

AGENTS.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ Configured in `.mcp.json`, all run as Docker containers via `docker compose --pr
186186

187187
**OpenAI Codex has been disabled across all pipelines effective immediately.** OpenAI has entered partnerships with government agencies that enable mass surveillance and autonomous weapons decision-making. The mass surveillance risk alone -- where code, prompts, and context sent through OpenAI APIs may be accessible to nation-state actors -- is unacceptable for this project and its users.
188188

189-
We strongly recommend **Anthropic (Claude)** models for all AI-assisted workflows. See the [README security advisory](README.md#security-advisory-openai--codex--gpt-phase-out) for full details.
189+
We strongly recommend **Anthropic (Claude)** models for all AI-assisted workflows. See the [README security notice](README.md#security-notice) for full details.
190190

191191
To re-enable at your own risk: set `CODEX_ENABLED=true` in your environment.
192192

@@ -229,3 +229,39 @@ To re-enable at your own risk: set `CODEX_ENABLED=true` in your environment.
229229
- `rustfmt.toml` -- Formatting rules
230230
- `deny.toml` -- License and advisory policy
231231
- `.pre-commit-config.yaml` -- Pre-commit hooks (trailing whitespace, yaml check, large files, actionlint, shellcheck, containerized rustfmt + clippy)
232+
233+
## Document Index
234+
235+
Key documentation for deeper context on specific topics. Read as needed rather than loading everything.
236+
237+
### Architecture & Design
238+
- [`docs/design.md`](docs/design.md) -- Technical design document v2.4 (~1300 lines, comprehensive architecture)
239+
- [`docs/adr/001-arena-based-dom.md`](docs/adr/001-arena-based-dom.md) -- ADR: Arena-based DOM allocation
240+
- [`docs/adr/002-vfs-abstraction.md`](docs/adr/002-vfs-abstraction.md) -- ADR: Virtual file system design
241+
- [`docs/adr/003-backend-trait-design.md`](docs/adr/003-backend-trait-design.md) -- ADR: Backend trait hierarchy
242+
- [`docs/adr/004-psp-two-binary-architecture.md`](docs/adr/004-psp-two-binary-architecture.md) -- ADR: PSP EBOOT + PRX split
243+
- [`docs/adr/005-toml-skin-system.md`](docs/adr/005-toml-skin-system.md) -- ADR: TOML skin engine
244+
245+
### Plans & Roadmaps
246+
- [`docs/psp-modernization-plan.md`](docs/psp-modernization-plan.md) -- PSP backend modernization (9 phases, 40 steps)
247+
- [`docs/comprehensive-improvements-plan-v2.md`](docs/comprehensive-improvements-plan-v2.md) -- Cross-crate improvement plan
248+
- [`docs/browser-improvement-plan-r3.md`](docs/browser-improvement-plan-r3.md) -- Browser engine improvement plan
249+
- [`docs/app-extraction-plan.md`](docs/app-extraction-plan.md) -- App crate extraction from oasis-core
250+
- [`docs/testing-gap-analysis.md`](docs/testing-gap-analysis.md) -- Test coverage gap analysis
251+
- [`docs/prd-oasis-video-integration.md`](docs/prd-oasis-video-integration.md) -- Video decode integration PRD
252+
- [`docs/internet-archive-tv-guide-plan.md`](docs/internet-archive-tv-guide-plan.md) -- TV Guide streaming plan
253+
254+
### Guides
255+
- [`docs/getting-started.md`](docs/getting-started.md) -- Getting started guide
256+
- [`docs/adding-commands.md`](docs/adding-commands.md) -- How to add terminal commands
257+
- [`docs/skin-authoring.md`](docs/skin-authoring.md) -- Skin creation with full TOML reference
258+
- [`docs/plugin-development.md`](docs/plugin-development.md) -- Plugin development guide
259+
- [`docs/ffi-integration.md`](docs/ffi-integration.md) -- UE5 / C-ABI integration guide
260+
- [`docs/psp-plugin.md`](docs/psp-plugin.md) -- PSP kernel plugin (PRX) documentation
261+
262+
### Operations
263+
- [`docs/troubleshooting.md`](docs/troubleshooting.md) -- Troubleshooting common issues
264+
- [`docs/security.md`](docs/security.md) -- Security policy and advisories
265+
- [`CLAUDE.md`](CLAUDE.md) -- Claude Code agent instructions and build commands
266+
- [`CONTRIBUTING.md`](CONTRIBUTING.md) -- Contribution policy (AI-authored only)
267+
- [`scripts/psp-scenarios.md`](scripts/psp-scenarios.md) -- PSP test scenario documentation

CLAUDE.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ cargo run -p oasis-app --bin oasis-screenshot
6060

6161
## CI Pipeline Order
6262

63-
format check -> clippy -> test -> release build -> cargo-deny -> PSP EBOOT build -> PPSSPP headless test -> screenshot regression -> benchmarks -> code coverage -> GitHub Pages deploy (WASM)
63+
format check -> clippy -> doc build -> markdown link check -> test -> release build -> screenshot regression -> cargo-deny -> benchmarks -> PSP EBOOT build -> PPSSPP headless test -> code coverage -> GitHub Pages deploy (WASM)
6464

6565
All steps run via `docker compose --profile ci run --rm rust-ci`.
6666

@@ -181,3 +181,39 @@ Exports C-ABI functions: `oasis_create`, `oasis_destroy`, `oasis_tick`, `oasis_s
181181
- `ci` -- rust-ci container (rust:1.93-slim + SDL2 dev libs + nightly + cargo-deny)
182182
- `psp` -- PPSSPP emulator (multi-stage build, NVIDIA GPU passthrough)
183183
- `services` -- MCP server containers (code-quality, content-creation, gemini, etc.)
184+
185+
## Document Index
186+
187+
Key documentation files for agents and contributors. Read these for deeper context on specific topics rather than loading everything into every conversation.
188+
189+
### Architecture & Design
190+
- [`docs/design.md`](docs/design.md) -- Technical design document v2.4 (~1300 lines, comprehensive architecture)
191+
- [`docs/adr/001-arena-based-dom.md`](docs/adr/001-arena-based-dom.md) -- ADR: Arena-based DOM allocation
192+
- [`docs/adr/002-vfs-abstraction.md`](docs/adr/002-vfs-abstraction.md) -- ADR: Virtual file system design
193+
- [`docs/adr/003-backend-trait-design.md`](docs/adr/003-backend-trait-design.md) -- ADR: Backend trait hierarchy
194+
- [`docs/adr/004-psp-two-binary-architecture.md`](docs/adr/004-psp-two-binary-architecture.md) -- ADR: PSP EBOOT + PRX split
195+
- [`docs/adr/005-toml-skin-system.md`](docs/adr/005-toml-skin-system.md) -- ADR: TOML skin engine
196+
197+
### Plans & Roadmaps
198+
- [`docs/psp-modernization-plan.md`](docs/psp-modernization-plan.md) -- PSP backend modernization (9 phases, 40 steps)
199+
- [`docs/comprehensive-improvements-plan-v2.md`](docs/comprehensive-improvements-plan-v2.md) -- Cross-crate improvement plan
200+
- [`docs/browser-improvement-plan-r3.md`](docs/browser-improvement-plan-r3.md) -- Browser engine improvement plan
201+
- [`docs/app-extraction-plan.md`](docs/app-extraction-plan.md) -- App crate extraction from oasis-core
202+
- [`docs/testing-gap-analysis.md`](docs/testing-gap-analysis.md) -- Test coverage gap analysis
203+
- [`docs/prd-oasis-video-integration.md`](docs/prd-oasis-video-integration.md) -- Video decode integration PRD
204+
- [`docs/internet-archive-tv-guide-plan.md`](docs/internet-archive-tv-guide-plan.md) -- TV Guide streaming plan
205+
206+
### Guides
207+
- [`docs/getting-started.md`](docs/getting-started.md) -- Getting started guide
208+
- [`docs/adding-commands.md`](docs/adding-commands.md) -- How to add terminal commands
209+
- [`docs/skin-authoring.md`](docs/skin-authoring.md) -- Skin creation with full TOML reference
210+
- [`docs/plugin-development.md`](docs/plugin-development.md) -- Plugin development guide
211+
- [`docs/ffi-integration.md`](docs/ffi-integration.md) -- UE5 / C-ABI integration guide
212+
- [`docs/psp-plugin.md`](docs/psp-plugin.md) -- PSP kernel plugin (PRX) documentation
213+
214+
### Operations
215+
- [`docs/troubleshooting.md`](docs/troubleshooting.md) -- Troubleshooting common issues
216+
- [`docs/security.md`](docs/security.md) -- Security policy and advisories
217+
- [`AGENTS.md`](AGENTS.md) -- Multi-agent system configuration and CI workflow
218+
- [`CONTRIBUTING.md`](CONTRIBUTING.md) -- Contribution policy (AI-authored only)
219+
- [`scripts/psp-scenarios.md`](scripts/psp-scenarios.md) -- PSP test scenario documentation

crates/oasis-audio/src/manager.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ impl AudioManager {
5959
backend: &mut dyn AudioBackend,
6060
) -> Result<()> {
6161
if !vfs.exists(path) {
62-
return Err(OasisError::Vfs(format!("file not found: {path}")));
62+
return Err(OasisError::Vfs(format!("file not found: {path}").into()));
6363
}
6464
let data = vfs.read(path)?;
6565
let _track_id = backend.load_track(&data)?;
@@ -72,7 +72,7 @@ impl AudioManager {
7272
/// Play the current track (or start from the first track if none selected).
7373
pub fn play(&mut self, backend: &mut dyn AudioBackend) -> Result<()> {
7474
if self.playlist.is_empty() {
75-
return Err(OasisError::Command("playlist is empty".to_string()));
75+
return Err(OasisError::Command("playlist is empty".into()));
7676
}
7777
if self.playlist.current_track().is_none() {
7878
self.playlist.set_current(0);
@@ -89,7 +89,7 @@ impl AudioManager {
8989
/// Pause playback.
9090
pub fn pause(&mut self, backend: &mut dyn AudioBackend) -> Result<()> {
9191
if self.state != PlaybackState::Playing {
92-
return Err(OasisError::Command("not playing".to_string()));
92+
return Err(OasisError::Command("not playing".into()));
9393
}
9494
backend.pause()?;
9595
self.state = PlaybackState::Paused;
@@ -99,7 +99,7 @@ impl AudioManager {
9999
/// Resume paused playback.
100100
pub fn resume(&mut self, backend: &mut dyn AudioBackend) -> Result<()> {
101101
if self.state != PlaybackState::Paused {
102-
return Err(OasisError::Command("not paused".to_string()));
102+
return Err(OasisError::Command("not paused".into()));
103103
}
104104
backend.resume()?;
105105
self.state = PlaybackState::Playing;
@@ -241,18 +241,18 @@ impl AudioManager {
241241
},
242242
"vol" => {
243243
let vol_str = parts.get(1).unwrap_or(&"");
244-
let vol: u8 = vol_str
245-
.parse()
246-
.map_err(|_| OasisError::Command(format!("invalid volume: {vol_str}")))?;
244+
let vol: u8 = vol_str.parse().map_err(|_| {
245+
OasisError::Command(format!("invalid volume: {vol_str}").into())
246+
})?;
247247
self.set_volume(vol, backend)?;
248248
Ok(format!("volume: {}%", self.volume))
249249
},
250250
"repeat" => {
251251
let mode_str = parts.get(1).unwrap_or(&"");
252252
let mode = RepeatMode::parse(mode_str).ok_or_else(|| {
253-
OasisError::Command(format!(
254-
"invalid repeat mode: {mode_str} (use off/all/one)"
255-
))
253+
OasisError::Command(
254+
format!("invalid repeat mode: {mode_str} (use off/all/one)").into(),
255+
)
256256
})?;
257257
self.set_repeat(mode);
258258
Ok(format!("repeat: {mode}"))
@@ -262,7 +262,9 @@ impl AudioManager {
262262
let state = if self.playlist.shuffle { "on" } else { "off" };
263263
Ok(format!("shuffle: {state}"))
264264
},
265-
_ => Err(OasisError::Command(format!("unknown audio command: {cmd}"))),
265+
_ => Err(OasisError::Command(
266+
format!("unknown audio command: {cmd}").into(),
267+
)),
266268
}
267269
}
268270
}

crates/oasis-audio/src/null_backend.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ impl AudioBackend for NullAudioBackend {
5858

5959
fn play(&mut self, track: AudioTrackId) -> Result<()> {
6060
if track.0 >= self.next_id {
61-
return Err(OasisError::Backend(format!("track {} not loaded", track.0)));
61+
return Err(OasisError::Backend(
62+
format!("track {} not loaded", track.0).into(),
63+
));
6264
}
6365
self.current_track = Some(track.0);
6466
self.playing = true;

crates/oasis-audio/src/radio/mod.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -362,24 +362,26 @@ impl RadioManager {
362362
},
363363
"vol" => {
364364
let vol_str = parts.get(1).unwrap_or(&"");
365-
let vol: u8 = vol_str
366-
.parse()
367-
.map_err(|_| OasisError::Command(format!("invalid volume: {vol_str}")))?;
365+
let vol: u8 = vol_str.parse().map_err(|_| {
366+
OasisError::Command(format!("invalid volume: {vol_str}").into())
367+
})?;
368368
self.set_volume(vol, backend)?;
369369
Ok(format!("volume: {}%", self.volume))
370370
},
371371
"fav" => {
372372
let idx_str = parts.get(1).unwrap_or(&"");
373373
let idx: usize = idx_str
374374
.parse()
375-
.map_err(|_| OasisError::Command(format!("invalid index: {idx_str}")))?;
375+
.map_err(|_| OasisError::Command(format!("invalid index: {idx_str}").into()))?;
376376
if self.registry.toggle_favorite(idx) {
377377
let fav = self.registry.stations[idx].favorite;
378378
let name = &self.registry.stations[idx].name;
379379
let star = if fav { "added" } else { "removed" };
380380
Ok(format!("{name}: favorite {star}"))
381381
} else {
382-
Err(OasisError::Command(format!("station {idx} not found")))
382+
Err(OasisError::Command(
383+
format!("station {idx} not found").into(),
384+
))
383385
}
384386
},
385387
"genre" => {
@@ -393,7 +395,9 @@ impl RadioManager {
393395
}
394396
},
395397
// "tune" is handled by the main loop (needs NetworkBackend).
396-
_ => Err(OasisError::Command(format!("unknown radio command: {cmd}"))),
398+
_ => Err(OasisError::Command(
399+
format!("unknown radio command: {cmd}").into(),
400+
)),
397401
}
398402
}
399403

@@ -460,7 +464,7 @@ impl RadioManager {
460464
self.registry = reg;
461465
Ok(())
462466
},
463-
Err(e) => Err(OasisError::Backend(e)),
467+
Err(e) => Err(OasisError::Backend(e.into())),
464468
}
465469
}
466470

crates/oasis-audio/src/radio/source.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ impl RadioSource for IcecastSource {
220220
return Ok(None);
221221
}
222222
self.state = SourceState::Error;
223-
return Err(OasisError::Backend(format!("stream read: {e}")));
223+
return Err(OasisError::Backend(format!("stream read: {e}").into()));
224224
},
225225
};
226226

@@ -430,7 +430,7 @@ impl RadioSource for ArchiveSource {
430430
return Ok(None);
431431
}
432432
self.state = SourceState::Error;
433-
return Err(OasisError::Backend(format!("stream read: {e}")));
433+
return Err(OasisError::Backend(format!("stream read: {e}").into()));
434434
},
435435
};
436436

@@ -444,15 +444,15 @@ impl RadioSource for ArchiveSource {
444444
let url = url.clone();
445445
self.state = SourceState::Error;
446446
let _ = self.stream.close();
447-
return Err(OasisError::Backend(format!("redirect:{url}")));
447+
return Err(OasisError::Backend(format!("redirect:{url}").into()));
448448
}
449449
// Check for HTTP error status.
450450
if let Some(code) = self.status_code
451451
&& code >= 400
452452
{
453453
self.state = SourceState::Error;
454454
let _ = self.stream.close();
455-
return Err(OasisError::Backend(format!("HTTP {code}")));
455+
return Err(OasisError::Backend(format!("HTTP {code}").into()));
456456
}
457457

458458
let body = self.header_buf[body_offset..].to_vec();
@@ -984,7 +984,7 @@ mod tests {
984984
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
985985
self.call_count += 1;
986986
if self.call_count % 2 == 1 {
987-
return Err(OasisError::Backend("WouldBlock".to_string()));
987+
return Err(OasisError::Backend("WouldBlock".into()));
988988
}
989989
self.inner.read(buf)
990990
}

crates/oasis-backend-psp/src/audio.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -960,7 +960,7 @@ impl AudioBackend for PspAudioBackend {
960960
.tracks
961961
.get_mut(idx)
962962
.and_then(|slot| slot.take())
963-
.ok_or_else(|| OasisError::Backend(format!("track {} not loaded", track.0)))?;
963+
.ok_or_else(|| OasisError::Backend(format!("track {} not loaded", track.0).into()))?;
964964
send_audio_cmd(AudioCmd::LoadAndPlayData(data));
965965
self.current_track = Some(track.0);
966966
Ok(())

crates/oasis-backend-psp/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ impl SdiCore for PspBackend {
458458
rgba_data: &[u8],
459459
) -> OasisResult<TextureId> {
460460
self.load_texture_inner(width, height, rgba_data)
461-
.ok_or_else(|| OasisError::Backend("PSP texture allocation failed".into()))
461+
.ok_or_else(|| OasisError::Backend("PSP texture allocation failed".to_string().into()))
462462
}
463463

464464
fn destroy_texture(&mut self, tex: TextureId) -> OasisResult<()> {
@@ -482,7 +482,7 @@ impl SdiCore for PspBackend {
482482

483483
fn read_pixels(&self, _x: i32, _y: i32, _w: u32, _h: u32) -> OasisResult<Vec<u8>> {
484484
Err(OasisError::Backend(
485-
"read_pixels not supported on PSP".into(),
485+
"read_pixels not supported on PSP".to_string().into(),
486486
))
487487
}
488488

0 commit comments

Comments
 (0)