implements PLN-0134
This commit is contained in:
parent
badc9b8874
commit
6593581954
@ -180,10 +180,58 @@ mod tests {
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::primitives::Rect;
|
||||
use prometeu_hal::syscalls::{Syscall, caps};
|
||||
use prometeu_system::CrashReport;
|
||||
use prometeu_system::process::ProcessState;
|
||||
use prometeu_system::task::{TaskId, TaskState};
|
||||
use prometeu_system::windows::WindowOwner;
|
||||
use prometeu_system::{CrashReport, discover_games_root};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
static TEST_DIR_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct TestGameRoot {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TestGameRoot {
|
||||
fn new() -> Self {
|
||||
let unique = TEST_DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"prometeu-firmware-game-root-test-{}-{}",
|
||||
std::process::id(),
|
||||
unique
|
||||
));
|
||||
fs::create_dir_all(&path).expect("test game root should be created");
|
||||
Self { path }
|
||||
}
|
||||
|
||||
fn add_game(&self, name: &str, with_program: bool) -> PathBuf {
|
||||
let cart = self.path.join(name);
|
||||
fs::create_dir_all(&cart).expect("test cartridge should be created");
|
||||
let manifest = r#"{
|
||||
"magic": "PMTU",
|
||||
"cartridge_version": 1,
|
||||
"app_id": 42,
|
||||
"title": "Stress",
|
||||
"app_version": "1.0.0",
|
||||
"app_mode": "Game"
|
||||
}"#;
|
||||
fs::write(cart.join("manifest.json"), manifest)
|
||||
.expect("test manifest should be written");
|
||||
if with_program {
|
||||
fs::write(cart.join("program.pbx"), halting_program())
|
||||
.expect("test program should be written");
|
||||
}
|
||||
cart
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestGameRoot {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
fn halting_program() -> Vec<u8> {
|
||||
let code = assemble("HALT").expect("assemble");
|
||||
@ -419,7 +467,7 @@ mod tests {
|
||||
#[test]
|
||||
fn hub_home_does_not_launch_shell_before_home_input_is_armed() {
|
||||
let (mut firmware, mut platform) = hub_home_firmware();
|
||||
let signals = InputSignals { f_signal: true, x_pos: 112, y_pos: 108, ..Default::default() };
|
||||
let signals = InputSignals { f_signal: true, x_pos: 112, y_pos: 172, ..Default::default() };
|
||||
|
||||
firmware.tick(&signals, &mut platform);
|
||||
|
||||
@ -432,7 +480,7 @@ mod tests {
|
||||
let (mut firmware, mut platform) = hub_home_firmware();
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let signals = InputSignals { f_signal: true, x_pos: 112, y_pos: 108, ..Default::default() };
|
||||
let signals = InputSignals { f_signal: true, x_pos: 112, y_pos: 172, ..Default::default() };
|
||||
firmware.tick(&signals, &mut platform);
|
||||
|
||||
let task_id = match &firmware.state {
|
||||
@ -455,7 +503,7 @@ mod tests {
|
||||
let (mut firmware, mut platform) = hub_home_firmware();
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let signals = InputSignals { f_signal: true, x_pos: 256, y_pos: 108, ..Default::default() };
|
||||
let signals = InputSignals { f_signal: true, x_pos: 256, y_pos: 172, ..Default::default() };
|
||||
firmware.tick(&signals, &mut platform);
|
||||
|
||||
let task_id = match &firmware.state {
|
||||
@ -473,6 +521,50 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hub_home_click_launches_game_library_entry_through_load_cartridge() {
|
||||
let root = TestGameRoot::new();
|
||||
root.add_game("stress", true);
|
||||
|
||||
let (mut firmware, mut platform) = hub_home_firmware();
|
||||
firmware.hub.set_game_library(discover_games_root(&root.path));
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let signals = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() };
|
||||
firmware.tick(&signals, &mut platform);
|
||||
|
||||
match &firmware.state {
|
||||
FirmwareState::LoadCartridge(step) => {
|
||||
assert_eq!(step.cartridge.title, "Stress");
|
||||
assert_eq!(step.cartridge.app_id, 42);
|
||||
}
|
||||
other => panic!("expected LoadCartridge state, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hub_home_failed_game_launch_remains_home_and_logs_error() {
|
||||
let root = TestGameRoot::new();
|
||||
let cart = root.add_game("stress", true);
|
||||
|
||||
let (mut firmware, mut platform) = hub_home_firmware();
|
||||
firmware.hub.set_game_library(discover_games_root(&root.path));
|
||||
fs::remove_file(cart.join("program.pbx")).expect("program should be removable");
|
||||
firmware.tick(&InputSignals::default(), &mut platform);
|
||||
|
||||
let signals = InputSignals { f_signal: true, x_pos: 72, y_pos: 90, ..Default::default() };
|
||||
firmware.tick(&signals, &mut platform);
|
||||
|
||||
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
|
||||
assert!(
|
||||
firmware
|
||||
.os
|
||||
.recent_logs(16)
|
||||
.iter()
|
||||
.any(|entry| entry.msg.contains("Failed to launch Home game"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_running_start_close_uses_lifecycle_and_returns_hub_home() {
|
||||
let (mut firmware, mut platform, _signals, task_id) = load_shell_running_firmware();
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, ShellRunningStep};
|
||||
use crate::firmware::firmware_state::{
|
||||
AppCrashesStep, FirmwareState, LoadCartridgeStep, ShellRunningStep,
|
||||
};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::primitives::Rect;
|
||||
use prometeu_system::SystemProfileAction;
|
||||
@ -21,16 +24,32 @@ impl HubHomeStep {
|
||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
||||
}
|
||||
|
||||
if let Some(SystemProfileAction::LaunchNativeShell(app)) = outcome.action {
|
||||
let task_id = ctx.os.sessions().create_native_shell_task(app.app_id(), app.title());
|
||||
let id = ctx.os.windows().add_window(
|
||||
app.title().to_string(),
|
||||
WindowOwner::Task(task_id),
|
||||
Rect { x: 40, y: 20, w: 400, h: 220 },
|
||||
app.color(),
|
||||
);
|
||||
ctx.os.windows().set_focus(id);
|
||||
return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id)));
|
||||
match outcome.action {
|
||||
Some(SystemProfileAction::LaunchNativeShell(app)) => {
|
||||
let task_id = ctx.os.sessions().create_native_shell_task(app.app_id(), app.title());
|
||||
let id = ctx.os.windows().add_window(
|
||||
app.title().to_string(),
|
||||
WindowOwner::Task(task_id),
|
||||
Rect { x: 40, y: 20, w: 400, h: 220 },
|
||||
app.color(),
|
||||
);
|
||||
ctx.os.windows().set_focus(id);
|
||||
return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id)));
|
||||
}
|
||||
Some(SystemProfileAction::LaunchGame { path }) => match CartridgeLoader::load(&path) {
|
||||
Ok(cartridge) => {
|
||||
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge)));
|
||||
}
|
||||
Err(error) => {
|
||||
ctx.os.log(
|
||||
LogLevel::Error,
|
||||
LogSource::Hub,
|
||||
0,
|
||||
format!("Failed to launch Home game {}: {:?}", path.display(), error),
|
||||
);
|
||||
}
|
||||
},
|
||||
Some(SystemProfileAction::CloseShell) | None => {}
|
||||
}
|
||||
|
||||
None
|
||||
|
||||
@ -6,13 +6,20 @@ use prometeu_hal::{
|
||||
ShellUiFramePacket,
|
||||
};
|
||||
use prometeu_vm::VirtualMachine;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 108, w: 112, h: 48 };
|
||||
const SHELL_B_BUTTON: Rect = Rect { x: 256, y: 108, w: 112, h: 48 };
|
||||
const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 172, w: 112, h: 32 };
|
||||
const SHELL_B_BUTTON: Rect = Rect { x: 256, y: 172, w: 112, h: 32 };
|
||||
const CLOSE_BUTTON: Rect = Rect { x: 420, y: 24, w: 16, h: 16 };
|
||||
const VIEWPORT: Rect = Rect { x: 0, y: 0, w: 480, h: 270 };
|
||||
const HOME_PANEL: Rect = Rect { x: 48, y: 58, w: 384, h: 154 };
|
||||
const TITLE_PANEL: Rect = Rect { x: 72, y: 34, w: 336, h: 28 };
|
||||
const GAME_ROW_X: i32 = 72;
|
||||
const GAME_ROW_Y: i32 = 90;
|
||||
const GAME_ROW_W: i32 = 336;
|
||||
const GAME_ROW_H: i32 = 22;
|
||||
const GAME_ROW_GAP: i32 = 6;
|
||||
const MAX_HOME_GAME_ROWS: usize = 3;
|
||||
#[cfg(test)]
|
||||
const SHELL_FRAME: Rect = Rect { x: 40, y: 20, w: 400, h: 220 };
|
||||
|
||||
@ -73,9 +80,10 @@ impl NativeShellApp {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SystemProfileAction {
|
||||
LaunchNativeShell(NativeShellApp),
|
||||
LaunchGame { path: PathBuf },
|
||||
CloseShell,
|
||||
}
|
||||
|
||||
@ -129,6 +137,7 @@ impl PrometeuHub {
|
||||
input.touch().x(),
|
||||
input.touch().y(),
|
||||
input.touch().f().pressed,
|
||||
self.game_library.entries(),
|
||||
) {
|
||||
return Some(action);
|
||||
}
|
||||
@ -141,7 +150,7 @@ impl PrometeuHub {
|
||||
let pointer_y = platform.input().touch().y();
|
||||
|
||||
let packet = if os.windows().window_count() == 0 {
|
||||
render_home_packet(pointer_x, pointer_y)
|
||||
render_home_packet(pointer_x, pointer_y, self.game_library.entries())
|
||||
} else {
|
||||
let mut commands = Vec::new();
|
||||
for window in os.windows().windows() {
|
||||
@ -186,7 +195,13 @@ impl PrometeuHub {
|
||||
}
|
||||
}
|
||||
|
||||
fn action_for_click(in_shell: bool, x: i32, y: i32, pressed: bool) -> Option<SystemProfileAction> {
|
||||
fn action_for_click(
|
||||
in_shell: bool,
|
||||
x: i32,
|
||||
y: i32,
|
||||
pressed: bool,
|
||||
game_entries: &[crate::GameLibraryEntry],
|
||||
) -> Option<SystemProfileAction> {
|
||||
if !pressed {
|
||||
return None;
|
||||
}
|
||||
@ -195,12 +210,31 @@ fn action_for_click(in_shell: bool, x: i32, y: i32, pressed: bool) -> Option<Sys
|
||||
return rect_contains(CLOSE_BUTTON, x, y).then_some(SystemProfileAction::CloseShell);
|
||||
}
|
||||
|
||||
if let Some(entry) = game_entries
|
||||
.iter()
|
||||
.take(MAX_HOME_GAME_ROWS)
|
||||
.enumerate()
|
||||
.find(|(index, _entry)| rect_contains(game_row_rect(*index), x, y))
|
||||
.map(|(_index, entry)| entry)
|
||||
{
|
||||
return Some(SystemProfileAction::LaunchGame { path: entry.path.clone() });
|
||||
}
|
||||
|
||||
HOME_BUTTONS
|
||||
.iter()
|
||||
.find(|button| rect_contains(button.rect, x, y))
|
||||
.map(|button| SystemProfileAction::LaunchNativeShell(button.app))
|
||||
}
|
||||
|
||||
fn game_row_rect(index: usize) -> Rect {
|
||||
Rect {
|
||||
x: GAME_ROW_X,
|
||||
y: GAME_ROW_Y + (GAME_ROW_H + GAME_ROW_GAP) * index as i32,
|
||||
w: GAME_ROW_W,
|
||||
h: GAME_ROW_H,
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_contains(rect: Rect, x: i32, y: i32) -> bool {
|
||||
x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h
|
||||
}
|
||||
@ -212,7 +246,11 @@ fn activation_input_down(input: &dyn InputPlatform) -> bool {
|
||||
|| input.pad().start().down
|
||||
}
|
||||
|
||||
fn render_home_packet(pointer_x: i32, pointer_y: i32) -> ShellUiFramePacket {
|
||||
fn render_home_packet(
|
||||
pointer_x: i32,
|
||||
pointer_y: i32,
|
||||
game_entries: &[crate::GameLibraryEntry],
|
||||
) -> ShellUiFramePacket {
|
||||
let mut commands = Vec::new();
|
||||
commands.push(GfxUiCommand::Clear { color: COLOR_BG });
|
||||
fill_rect(&mut commands, VIEWPORT, COLOR_BG);
|
||||
@ -226,21 +264,26 @@ fn render_home_packet(pointer_x: i32, pointer_y: i32) -> ShellUiFramePacket {
|
||||
);
|
||||
|
||||
draw_panel(&mut commands, HOME_PANEL, COLOR_PANEL, COLOR_BORDER);
|
||||
draw_buffer_text(
|
||||
&mut commands,
|
||||
HOME_PANEL.x + 26,
|
||||
HOME_PANEL.y + 22,
|
||||
"Native Shell Apps",
|
||||
COLOR_TEXT,
|
||||
);
|
||||
draw_buffer_text(&mut commands, HOME_PANEL.x + 26, HOME_PANEL.y + 22, "Games", COLOR_TEXT);
|
||||
draw_buffer_text(
|
||||
&mut commands,
|
||||
HOME_PANEL.x + 26,
|
||||
HOME_PANEL.y + 40,
|
||||
"first retro ui slice",
|
||||
"local library",
|
||||
COLOR_MUTED,
|
||||
);
|
||||
|
||||
if game_entries.is_empty() {
|
||||
draw_buffer_text(&mut commands, GAME_ROW_X, GAME_ROW_Y + 6, "No games found", COLOR_MUTED);
|
||||
} else {
|
||||
for (index, entry) in game_entries.iter().take(MAX_HOME_GAME_ROWS).enumerate() {
|
||||
let rect = game_row_rect(index);
|
||||
let hovered = rect_contains(rect, pointer_x, pointer_y);
|
||||
let text = format!("{} #{} {}", entry.title, entry.app_id, entry.app_version);
|
||||
draw_game_row(&mut commands, rect, &text, hovered);
|
||||
}
|
||||
}
|
||||
|
||||
for button in HOME_BUTTONS {
|
||||
let hovered = rect_contains(button.rect, pointer_x, pointer_y);
|
||||
draw_button(&mut commands, button.rect, button.label, hovered);
|
||||
@ -335,9 +378,17 @@ fn draw_button(commands: &mut Vec<GfxUiCommand>, rect: Rect, label: &str, hovere
|
||||
Rect { x: rect.x + 3, y: rect.y + 3, w: rect.w - 6, h: rect.h - 6 },
|
||||
COLOR_MUTED,
|
||||
);
|
||||
fill_rect(commands, Rect { x: rect.x + 12, y: rect.y + 12, w: 16, h: 24 }, border);
|
||||
draw_rect(commands, Rect { x: rect.x + 34, y: rect.y + 12, w: 62, h: 24 }, COLOR_PANEL_DARK);
|
||||
draw_buffer_text(commands, rect.x + 40, rect.y + 21, label, COLOR_TEXT);
|
||||
fill_rect(commands, Rect { x: rect.x + 12, y: rect.y + 8, w: 14, h: 16 }, border);
|
||||
draw_rect(commands, Rect { x: rect.x + 34, y: rect.y + 8, w: 62, h: 16 }, COLOR_PANEL_DARK);
|
||||
draw_buffer_text(commands, rect.x + 40, rect.y + 13, label, COLOR_TEXT);
|
||||
}
|
||||
|
||||
fn draw_game_row(commands: &mut Vec<GfxUiCommand>, rect: Rect, label: &str, hovered: bool) {
|
||||
let fill = if hovered { COLOR_BUTTON_ACTIVE } else { COLOR_PANEL_DARK };
|
||||
let border = if hovered { COLOR_HILITE } else { COLOR_MUTED };
|
||||
fill_rect(commands, rect, fill);
|
||||
draw_rect(commands, rect, border);
|
||||
draw_buffer_text(commands, rect.x + 8, rect.y + 7, label, COLOR_TEXT);
|
||||
}
|
||||
|
||||
fn draw_buffer_text(commands: &mut Vec<GfxUiCommand>, x: i32, y: i32, text: &str, color: Color) {
|
||||
@ -351,7 +402,7 @@ mod tests {
|
||||
#[test]
|
||||
fn shell_a_button_click_emits_launch_action() {
|
||||
assert_eq!(
|
||||
action_for_click(false, HOME_BUTTONS[0].rect.x, HOME_BUTTONS[0].rect.y, true),
|
||||
action_for_click(false, HOME_BUTTONS[0].rect.x, HOME_BUTTONS[0].rect.y, true, &[]),
|
||||
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA))
|
||||
);
|
||||
}
|
||||
@ -363,7 +414,8 @@ mod tests {
|
||||
false,
|
||||
SHELL_B_BUTTON.x + SHELL_B_BUTTON.w - 1,
|
||||
SHELL_B_BUTTON.y,
|
||||
true
|
||||
true,
|
||||
&[]
|
||||
),
|
||||
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB))
|
||||
);
|
||||
@ -372,18 +424,30 @@ mod tests {
|
||||
#[test]
|
||||
fn button_hit_test_excludes_right_and_bottom_edges() {
|
||||
assert_eq!(
|
||||
action_for_click(false, SHELL_A_BUTTON.x + SHELL_A_BUTTON.w, SHELL_A_BUTTON.y, true),
|
||||
action_for_click(
|
||||
false,
|
||||
SHELL_A_BUTTON.x + SHELL_A_BUTTON.w,
|
||||
SHELL_A_BUTTON.y,
|
||||
true,
|
||||
&[]
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y + SHELL_A_BUTTON.h, true),
|
||||
action_for_click(
|
||||
false,
|
||||
SHELL_A_BUTTON.x,
|
||||
SHELL_A_BUTTON.y + SHELL_A_BUTTON.h,
|
||||
true,
|
||||
&[]
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn held_pointer_does_not_emit_click_action() {
|
||||
assert_eq!(action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false), None);
|
||||
assert_eq!(action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false, &[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -392,13 +456,38 @@ mod tests {
|
||||
assert!(!hub.home_input_armed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_row_click_emits_launch_game_action() {
|
||||
let entry = crate::GameLibraryEntry {
|
||||
manifest: prometeu_hal::cartridge::CartridgeManifest {
|
||||
magic: "PMTU".to_string(),
|
||||
cartridge_version: 1,
|
||||
app_id: 42,
|
||||
title: "Stress".to_string(),
|
||||
app_version: "1.0.0".to_string(),
|
||||
app_mode: prometeu_hal::app_mode::AppMode::Game,
|
||||
capabilities: vec![],
|
||||
},
|
||||
title: "Stress".to_string(),
|
||||
app_id: 42,
|
||||
app_version: "1.0.0".to_string(),
|
||||
path: PathBuf::from("test-cartridges/stress-console"),
|
||||
discovered_at: std::time::SystemTime::UNIX_EPOCH,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
action_for_click(false, GAME_ROW_X, GAME_ROW_Y, true, std::slice::from_ref(&entry)),
|
||||
Some(SystemProfileAction::LaunchGame { path: entry.path.clone() })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_button_click_emits_close_only_in_shell() {
|
||||
assert_eq!(
|
||||
action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true),
|
||||
action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[]),
|
||||
Some(SystemProfileAction::CloseShell)
|
||||
);
|
||||
assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true), None);
|
||||
assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true, &[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -412,7 +501,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn home_render_produces_shell_ui_packet_commands() {
|
||||
let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y);
|
||||
let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, &[]);
|
||||
|
||||
assert!(matches!(packet.commands.first(), Some(GfxUiCommand::Clear { .. })));
|
||||
assert!(packet.commands.iter().any(|command| {
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"discussion/lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||
{"type":"discussion","id":"DSC-0002","status":"done","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-04-21","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0037","file":"discussion/lessons/DSC-0002-runtime-edge-test-plan/LSN-0037-domain-gates-must-be-owned-by-the-repository.md","status":"done","created_at":"2026-04-21","updated_at":"2026-04-21"}]}
|
||||
{"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0004","status":"in_progress","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-07-03","tags":[],"agendas":[{"id":"AGD-0003","file":"AGD-0003-system-run-cart.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-07-03"},{"id":"AGD-0045","file":"AGD-0045-systemos-library-root-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0035","file":"DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0003"},{"id":"DEC-0036","file":"DEC-0036-systemos-games-root-and-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0045"}],"plans":[{"id":"PLN-0129","file":"PLN-0129-remove-userland-run-cart-abi-surface.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0130","file":"PLN-0130-remove-runtime-run-cart-stub-dispatch.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0131","file":"PLN-0131-preserve-direct-cartridge-boot-coverage.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0132","file":"PLN-0132-specify-games-root-home-launch-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0133","file":"PLN-0133-implement-games-root-library-discovery.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0134","file":"PLN-0134-wire-home-game-list-launch-flow.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0135","file":"PLN-0135-validate-games-root-launch-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0004","status":"in_progress","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-07-03","tags":[],"agendas":[{"id":"AGD-0003","file":"AGD-0003-system-run-cart.md","status":"accepted","created_at":"2026-03-27","updated_at":"2026-07-03"},{"id":"AGD-0045","file":"AGD-0045-systemos-library-root-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03"}],"decisions":[{"id":"DEC-0035","file":"DEC-0035-remove-userland-system-run-cart-and-preserve-direct-boot.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0003"},{"id":"DEC-0036","file":"DEC-0036-systemos-games-root-and-home-game-launch.md","status":"accepted","created_at":"2026-07-03","updated_at":"2026-07-03","ref_agenda":"AGD-0045"}],"plans":[{"id":"PLN-0129","file":"PLN-0129-remove-userland-run-cart-abi-surface.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0130","file":"PLN-0130-remove-runtime-run-cart-stub-dispatch.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0131","file":"PLN-0131-preserve-direct-cartridge-boot-coverage.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0035"]},{"id":"PLN-0132","file":"PLN-0132-specify-games-root-home-launch-contract.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0133","file":"PLN-0133-implement-games-root-library-discovery.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0134","file":"PLN-0134-wire-home-game-list-launch-flow.md","status":"done","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]},{"id":"PLN-0135","file":"PLN-0135-validate-games-root-launch-end-to-end.md","status":"open","created_at":"2026-07-03","updated_at":"2026-07-03","ref_decisions":["DEC-0036"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
id: PLN-0134
|
||||
ticket: system-run-cart
|
||||
title: Wire Home Game List Launch Flow
|
||||
status: open
|
||||
status: done
|
||||
created: 2026-07-03
|
||||
ref_decisions: [DEC-0036]
|
||||
tags: []
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user