Implement and archive Phase 5 integration acceptance

This commit is contained in:
2026-09-07 10:38:58 +02:00
parent cc82830889
commit 86c3ebeb6d
18 changed files with 1224 additions and 39 deletions

2
Cargo.lock generated
View File

@@ -607,6 +607,8 @@ name = "tb-cli"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"crossterm",
"ratatui",
"tb-frontend", "tb-frontend",
"tb-ide", "tb-ide",
"tb-runtime", "tb-runtime",

27
PLAN.md
View File

@@ -446,15 +446,15 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
virtuelle Ereignisfolge reproduzierbar ein. virtuelle Ereignisfolge reproduzierbar ein.
## Phase 5 — IDE (`tb-ide`) ## Phase 5 — IDE (`tb-ide`)
- [ ] IDE-Rahmen: Menüleiste, MDI-artige Fensterverwaltung, Statuszeile, - [x] IDE-Rahmen: Menüleiste, MDI-artige Fensterverwaltung, Statuszeile,
klassisches Farbschema klassisches Farbschema
- [ ] Editor: Syntaxprüfung/-normalisierung pro Zeile (Keywords groß, - [x] Editor: Syntaxprüfung/-normalisierung pro Zeile (Keywords groß,
Leerzeichen), Suchen/Ersetzen, Hilfe-Verweise Leerzeichen), Suchen/Ersetzen, Hilfe-Verweise
- [ ] Projektverwaltung (`.MAK`): mehrere Module/Formulare - [x] Projektverwaltung (`.MAK`): mehrere Module/Formulare
- [ ] Formular-Designer: Steuerelemente platzieren/verschieben/skalieren, - [x] Formular-Designer: Steuerelemente platzieren/verschieben/skalieren,
Eigenschaftenfenster Properties Bar
- [ ] Ausführen aus der IDE: Start/Unterbrechen/Fortsetzen/Neustart - [x] Ausführen aus der IDE: Start/Unterbrechen/Fortsetzen/Neustart
- [ ] **Export-Bedienung der IDE:** Run → Make EXE File und Make Library - [x] **Export-Bedienung der IDE:** Run → Make EXE File und Make Library
mit vollständigen Dialogen, Zielsystem/-architektur, Ausgabepfad, mit vollständigen Dialogen, Zielsystem/-architektur, Ausgabepfad,
Eingabeprüfung, Status-/Fehleranzeige und Übergabe an den Exportpfad Eingabeprüfung, Status-/Fehleranzeige und Übergabe an den Exportpfad
vorbereiten. Die UI gehört vollständig zu Phase 5. Solange der vorbereiten. Die UI gehört vollständig zu Phase 5. Solange der
@@ -469,16 +469,23 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
- [x] **`RUN`** — Programm neu starten bzw. ein anderes laden und starten. - [x] **`RUN`** — Programm neu starten bzw. ein anderes laden und starten.
Nutzt denselben Zurücksetzpfad der VM wie „Neustart" oben. Nutzt denselben Zurücksetzpfad der VM wie „Neustart" oben.
**1 Inventareintrag** **1 Inventareintrag**
- [ ] Debugger: Breakpoints, Einzelschritt/Prozedurschritt, Direktfenster, - [x] Debugger: Breakpoints, Einzelschritt/Prozedurschritt, Direktfenster,
Überwachungsausdrücke Überwachungsausdrücke
- [ ] Hilfe-System: rendert die Markdown-Dokumentation aus `docs/` - [x] Hilfe-System: rendert die Markdown-Dokumentation aus `docs/`
(Sprach- und Bibliotheksreferenz) im Help-Fenster — Konsolen- (Sprach- und Bibliotheksreferenz) im Help-Fenster — Konsolen-
Markdown-Rendering mit Umbruch nach aktueller Fenstergröße, Markdown-Rendering mit Umbruch nach aktueller Fenstergröße,
Hyperlink-Navigation wie im Vorbild (Tab/Enter/Alt+F1), F1 = Hyperlink-Navigation wie im Vorbild (Tab/Enter/Alt+F1), F1 =
kontextsensitiver Sprung zum Thema unter dem Cursor kontextsensitiver Sprung zum Thema unter dem Cursor
- [ ] Meilenstein: Programm komplett in der IDE schreiben, gestalten, - [x] Meilenstein: Programm komplett in der IDE schreiben, gestalten,
debuggen und ausführen debuggen und ausführen
Abgenommen am 2026-09-07 durch Change `phase-5-08-integration-und-phasenabnahme`.
Die zugehörigen `verification.md` und `befehlsmatrix.md` im OpenSpec-Change
(bei Abschluss unter `openspec/changes/archive/`) ordnen alle elf Planpunkte
und 45 Referenzbereiche den bestandenen Tests zu. Workspace-Regressionen,
headless IDE/CLI/TBC-Parität, lokaler Unix-PTY-Test und Release-Compile-Budget
sind bestanden. Die vollständige Terminal-/OS-Matrix bleibt Phase 6.
## Phase 6 — Kompatibilität, Politur, Distribution ## Phase 6 — Kompatibilität, Politur, Distribution
- [ ] Kompatibilitäts-Testsuite ausbauen (Snapshot-Tests der Bildschirmausgabe) - [ ] Kompatibilitäts-Testsuite ausbauen (Snapshot-Tests der Bildschirmausgabe)
- [ ] Plattformtests: Windows Terminal, Linux (mind. 2 Emulatoren), macOS — - [ ] Plattformtests: Windows Terminal, Linux (mind. 2 Emulatoren), macOS —

View File

@@ -19,3 +19,5 @@ anyhow.workspace = true
[dev-dependencies] [dev-dependencies]
tb-ide = { path = "../tb-ide" } tb-ide = { path = "../tb-ide" }
crossterm.workspace = true
ratatui.workspace = true

View File

@@ -0,0 +1,804 @@
//! Phase-5 acceptance drives App through native input; CLI comparisons use run_chain itself.
use super::*;
use crossterm::event::{
Event, KeyCode as K, KeyEvent, KeyModifiers as M, MouseButton, MouseEvent, MouseEventKind,
};
use ratatui::{backend::TestBackend, Terminal};
use std::{
fs,
sync::atomic::{AtomicUsize, Ordering},
};
use tb_ide::{
app::{App, DialogKind, Execution, Hit, Mode},
commands::Command,
export::{ExportStatus, ProjectStamp},
};
use tb_runtime::{snapshot, value::Value};
struct Temp(PathBuf);
impl Temp {
fn new() -> Self {
static N: AtomicUsize = AtomicUsize::new(0);
let p = std::env::temp_dir().join(format!(
"tb-accept-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(&p).unwrap();
Self(p.canonicalize().unwrap())
}
}
impl Drop for Temp {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn key(a: &mut App, k: K, m: M) {
a.handle(Event::Key(KeyEvent::new(k, m)));
}
fn plain(a: &mut App, k: K) {
key(a, k, M::NONE);
}
fn text(a: &mut App, s: &str) {
for c in s.chars() {
plain(a, K::Char(c));
}
}
fn field(a: &mut App, n: usize, s: &str) {
for _ in 0..30 {
if a.dialog.as_ref().unwrap().focus == n {
break;
}
plain(a, K::Tab);
}
assert_eq!(a.dialog.as_ref().unwrap().focus, n);
key(a, K::Char('a'), M::CONTROL);
plain(a, K::Delete);
text(a, s);
}
fn choice(a: &mut App, n: usize, s: &str) {
for _ in 0..30 {
if a.dialog.as_ref().unwrap().focus == n {
break;
}
plain(a, K::Tab);
}
for _ in 0..200 {
if a.dialog.as_ref().unwrap().fields[n]
.string()
.eq_ignore_ascii_case(s)
{
return;
}
plain(a, K::Right);
}
panic!("choice missing {s}: {:?}", a.dialog);
}
fn submit(a: &mut App) {
plain(a, K::Enter);
assert!(a.dialog.is_none(), "{:?} / {}", a.dialog, a.message);
}
fn menu(a: &mut App, c: Command) {
let menus = a.menus();
let (m, i) = menus
.iter()
.enumerate()
.find_map(|(m, menu)| {
menu.items
.iter()
.position(|i| i.command == Some(c))
.map(|i| (m, i))
})
.unwrap_or_else(|| panic!("menu missing {c:?}"));
key(a, K::Char(menus[m].mnemonic), M::ALT);
for _ in 0..i {
plain(a, K::Down);
}
plain(a, K::Enter);
assert_eq!(a.last_command, Some(c), "{}", a.message);
}
fn draw(a: &mut App) -> String {
let mut t = Terminal::new(TestBackend::new(a.size.0, a.size.1)).unwrap();
t.draw(|f| a.render(f)).unwrap();
t.backend()
.buffer()
.content
.chunks(a.size.0 as usize)
.map(|r| r.iter().map(|c| c.symbol()).collect::<String>())
.collect::<Vec<_>>()
.join("\n")
}
fn click_hit(a: &mut App, predicate: impl Fn(&Hit) -> bool) {
draw(a);
let r = a
.hits
.iter()
.rev()
.find(|(_, h)| predicate(h))
.unwrap_or_else(|| panic!("hit missing"))
.0;
a.handle(Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: r.x,
row: r.y,
modifiers: M::NONE,
}));
}
fn replace_code(a: &mut App, s: &str) {
key(a, K::Char('a'), M::CONTROL);
a.handle(Event::Paste(s.into()));
}
fn code(a: &App) -> String {
a.project
.document(a.active_document().unwrap())
.unwrap()
.code()
.into()
}
fn cursor_line(a: &mut App, line: usize) {
key(a, K::Home, M::CONTROL);
for _ in 1..line {
plain(a, K::Down);
}
plain(a, K::Home);
}
fn save_as(a: &mut App, path: &Path) {
menu(a, Command::SaveAs);
field(a, 0, &path.display().to_string());
submit(a);
}
fn property(a: &mut App, name: &str, value: &str) {
if !a.properties {
plain(a, K::F(10));
}
click_hit(a, |h| matches!(h, Hit::DesignAction("property")));
choice(a, 0, name);
submit(a);
plain(a, K::F(2));
key(a, K::Char('a'), M::CONTROL);
text(a, value);
plain(a, K::Enter);
assert!(a.dialog.is_none(), "{:?}", a.dialog);
assert!(!a.value_focus);
}
fn tick(a: &mut App, now: u64) {
for _ in 0..40 {
a.tick_execution(now);
if !matches!(a.execution, Execution::Running) {
break;
}
}
}
fn open_code(a: &mut App, name: &str) {
menu(a, Command::Code);
if a.dialog.is_none() {
menu(a, Command::Code);
}
choice(a, 0, name);
submit(a);
assert_eq!(a.mode, Mode::Environment);
}
fn create(a: &mut App, t: &Temp) -> PathBuf {
assert_eq!(a.project.members().len(), 1);
assert!(code(a).is_empty());
assert!(draw(a).contains("Untitled"));
menu(a, Command::NewProject);
if a.dialog.is_some() {
choice(a, 0, "Verwerfen");
submit(a);
}
assert!(code(a).is_empty());
menu(a, Command::SyntaxChecking);
let include=format!("total=1\nCALL Bump(total)\nForm1.Hide\nCLS\nLINE INPUT input$\nPRINT total; input$; COMMAND$\nOPEN \"{}\" FOR OUTPUT AS #1\nPRINT #1, total\nCLOSE #1\nIF COMMAND$=\"error\" THEN ERROR 5\nEND\n",t.0.join("result.txt").display());
replace_code(a, &include);
menu(a, Command::SaveText);
field(a, 0, &t.0.join("work.bi").display().to_string());
submit(a);
replace_code(a, "' Main module\n");
save_as(a, &t.0.join("main.bas"));
menu(a, Command::NewModule);
field(a, 0, "Helper");
submit(a);
replace_code(a, "SUB Bump(n AS INTEGER)\nn=n+1\nEND SUB\n");
save_as(a, &t.0.join("helper.bas"));
menu(a, Command::NewForm);
assert_eq!(a.mode, Mode::Designer);
property(a, "CAPTION", "Abnahme");
let designer_focus = a.active;
plain(a, K::F(1));
assert_eq!(a.help.page().path, "docs/forms-referenz.md");
plain(a, K::Tab);
plain(a, K::Enter);
key(a, K::F(1), M::ALT);
plain(a, K::Esc);
assert_eq!(a.active, designer_focus);
plain(a, K::F(12));
plain(a, K::Enter);
choice(a, 0, "Load");
submit(a);
let handler = code(a);
assert!(handler.to_uppercase().contains("FORM_LOAD"), "{handler}");
cursor_line(a, 1);
key(a, K::End, M::CONTROL); // Replace body through the editor, retaining the generated event signature.
replace_code(
a,
&format!(
"DECLARE SUB Bump(n AS INTEGER)\nDIM SHARED total AS INTEGER\n{}",
handler.replace("END SUB", "SHARED total\n'$INCLUDE: 'work.bi'\nEND SUB")
),
);
key(a, K::F(12), M::SHIFT);
assert_eq!(a.mode, Mode::Designer);
// Toolbox Enter is the documented keyboard equivalent of double-click placement.
menu(a, Command::Toolbox);
for _ in 0..2 {
plain(a, K::Down);
}
plain(a, K::Enter);
assert!(a.designer.selections.values().any(|s| !s.is_empty()));
property(a, "INDEX", "0");
property(a, "CAPTION", "Start");
menu(a, Command::Toolbox);
plain(a, K::Down);
plain(a, K::Down);
plain(a, K::Enter);
property(a, "INDEX", "1");
property(a, "CTLNAME", "CommandButton1");
menu(a, Command::MenuDesign);
plain(a, K::Insert);
plain(a, K::Enter);
field(a, 1, "&Datei");
submit(a);
// Save all members and the chosen startup through actual dialog fields.
open_code(a, "main.bas");
menu(a, Command::Startup);
choice(a, 0, "Form1.frm");
submit(a);
let path = t.0.join("project.mak");
menu(a, Command::SaveProject);
field(a, 0, &path.display().to_string());
submit(a);
assert_eq!(a.project.members().len(), 3);
assert!(!a.project.is_dirty());
let form = fs::read_to_string(t.0.join("Form1.frm")).unwrap();
let parsed = tb_ui::frm::read_text("Form1.frm", &form).unwrap();
let array: Vec<_> = parsed
.root
.children
.iter()
.filter(|n| n.class == tb_frontend::forms::ObjectClass::CommandButton)
.map(tb_ide::designer::node_key)
.collect();
assert_eq!(
array,
vec![
("COMMANDBUTTON1".into(), Some(0)),
("COMMANDBUTTON1".into(), Some(1))
]
);
assert!(form.to_uppercase().contains("BEGIN MENU"));
assert!(form.to_uppercase().contains("FORM_LOAD"));
path
}
fn exports(a: &mut App, t: &Temp) {
let stamp = ProjectStamp::capture(&a.project);
for c in [Command::MakeExe, Command::MakeLibrary] {
menu(a, c);
assert!(draw(a).contains("Erzeugen: Phase 6"));
choice(a, 1, "linux");
choice(a, 2, "x86_64");
field(a, 3, "");
plain(a, K::Enter);
assert!(a.dialog.as_ref().unwrap().error.contains("Ausgabepfad"));
field(a, 3, &t.0.join("bad.tbc").display().to_string());
plain(a, K::Enter);
assert!(a.dialog.as_ref().unwrap().error.contains("TBC"));
let existing = t.0.join("existing");
fs::write(&existing, "original").unwrap();
field(a, 3, &existing.display().to_string());
plain(a, K::Enter);
assert!(a
.dialog
.as_ref()
.unwrap()
.error
.contains("Überschreibentscheidung"));
plain(a, K::Tab);
plain(a, K::Char(' '));
plain(a, K::Enter);
assert_eq!(fs::read_to_string(existing).unwrap(), "original");
let out = t.0.join("native-result");
field(a, 3, &out.display().to_string());
plain(a, K::Enter);
let request = a.dialog.as_ref().unwrap().request.clone().unwrap();
assert_eq!(request.project, stamp);
for status in [
ExportStatus::Running,
ExportStatus::Failed("Kontrollierter Fehler".into()),
ExportStatus::Cancelled,
ExportStatus::Success,
] {
a.export_result(&request, status.clone()).unwrap();
assert!(draw(a).contains(&status.text()));
assert!(!out.exists());
}
plain(a, K::Esc);
assert_eq!(ProjectStamp::capture(&a.project), stamp);
}
}
#[test]
fn create_save_debug_help_exports_and_reopen() {
let t = Temp::new();
let mut a = App::new(&t.0, t.0.join("options"), (80, 25)).unwrap();
let path = create(&mut a, &t);
let saved: Vec<_> = [
"main.bas",
"helper.bas",
"Form1.frm",
"work.bi",
"project.mak",
]
.map(|p| fs::read(t.0.join(p)).unwrap())
.into();
exports(&mut a, &t);
open_code(&mut a, "Form1.frm");
let line = code(&a)
.lines()
.position(|l| l.contains("$INCLUDE"))
.unwrap()
+ 1;
cursor_line(&mut a, line);
menu(&mut a, Command::IncludedFile);
assert!(a
.project
.document(a.active_document().unwrap())
.unwrap()
.source_path()
.ends_with("work.bi"));
cursor_line(&mut a, 2);
plain(&mut a, K::F(9));
let code_focus = a.active;
cursor_line(&mut a, 6);
plain(&mut a, K::F(1));
assert!(a.help.page().title.contains("Index") || a.help.page().path.starts_with("docs/"));
plain(&mut a, K::Tab);
plain(&mut a, K::Enter);
key(&mut a, K::F(1), M::ALT);
plain(&mut a, K::Esc);
assert_eq!(a.active, code_focus);
menu(&mut a, Command::CommandLine);
field(&mut a, 0, "!");
submit(&mut a);
key(&mut a, K::F(5), M::SHIFT);
tick(&mut a, 0);
assert_eq!(a.execution, Execution::Paused, "{}", a.message);
assert!(a
.session
.vm
.as_ref()
.unwrap()
.current_file()
.ends_with("work.bi"));
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 2);
plain(&mut a, K::F(10));
tick(&mut a, 0);
assert_eq!(a.execution, Execution::Paused);
assert_eq!(a.session.vm.as_ref().unwrap().current_line(), 3);
menu(&mut a, Command::AddWatch);
field(&mut a, 0, "total");
submit(&mut a);
assert!(matches!(a.debugger.watches[0].value, Ok(Value::Int(2))));
menu(&mut a, Command::Immediate);
text(&mut a, "total=9");
plain(&mut a, K::Enter);
tick(&mut a, 0);
assert_eq!(a.execution, Execution::Paused);
plain(&mut a, K::F(5));
tick(&mut a, 0);
assert_eq!(a.execution, Execution::Waiting, "{}", a.message);
menu(&mut a, Command::Output);
a.handle(Event::Paste("abc\n".into()));
tick(&mut a, 100);
assert_eq!(a.execution, Execution::Ended, "{}", a.message);
assert_eq!(snapshot::text(a.session.screen()), "? abc\n 9 abc!\n");
assert!(fs::read_to_string(t.0.join("result.txt"))
.unwrap()
.contains('9'));
for (p, bytes) in [
"main.bas",
"helper.bas",
"Form1.frm",
"work.bi",
"project.mak",
]
.iter()
.zip(saved.iter())
{
assert_eq!(&fs::read(t.0.join(p)).unwrap(), bytes);
}
menu(&mut a, Command::NewProject);
if a.dialog.is_some() {
choice(&mut a, 0, "Verwerfen");
submit(&mut a);
}
menu(&mut a, Command::OpenProject);
field(&mut a, 0, &path.display().to_string());
plain(&mut a, K::Enter);
choice(&mut a, 0, "Verwerfen");
submit(&mut a);
assert_eq!(a.project.members().len(), 3);
assert!(a
.project
.document(a.project.startup().unwrap())
.unwrap()
.source_path()
.ends_with("Form1.frm"));
assert!(!a.project.is_dirty());
for id in a.project.members() {
let doc = a.project.document(id).unwrap();
match doc.content() {
tb_vm::project_io::Content::Form(form) => assert_eq!(
form.as_ref(),
&tb_ui::frm::read_text(
&doc.source_path().display().to_string(),
&fs::read_to_string(doc.source_path()).unwrap()
)
.unwrap()
),
_ => assert_eq!(doc.code(), fs::read_to_string(doc.source_path()).unwrap()),
}
}
menu(&mut a, Command::CommandLine);
field(&mut a, 0, "!");
submit(&mut a);
menu(&mut a, Command::Start);
tick(&mut a, 0);
assert_eq!(a.execution, Execution::Waiting);
menu(&mut a, Command::Output);
a.handle(Event::Paste("abc\n".into()));
tick(&mut a, 100);
assert_eq!(a.execution, Execution::Ended);
assert_eq!(snapshot::text(a.session.screen()), "? abc\n 2 abc!\n");
}
struct ScriptHost {
now: u64,
sent: bool,
events: std::collections::VecDeque<Ereignis>,
}
impl Host for ScriptHost {
fn present(&mut self, _: &tb_runtime::screen::TextScreen) {}
fn jetzt_ms(&mut self) -> u64 {
self.now
}
fn next_event(&mut self, _: bool) -> Option<Ereignis> {
self.events.pop_front()
}
fn warten(&mut self, _: Option<u64>) -> Option<Ereignis> {
assert!(!self.sent, "Unexpected extra wait");
self.sent = true;
self.now = 100;
self.events
.extend("abc\r".chars().map(|c| Ereignis::Taste(c.to_string(), 0)));
self.events.pop_front()
}
}
#[test]
fn saved_project_ide_source_cli_and_tbc_have_identical_sessions() {
let t = Temp::new();
let mut a = App::new(&t.0, t.0.join("options"), (80, 25)).unwrap();
let path = create(&mut a, &t);
assert_eq!(
super::cmd_build(&[path.display().to_string()]),
ExitCode::SUCCESS
);
for command in ["!", "error"] {
menu(&mut a, Command::CommandLine);
field(&mut a, 0, command);
submit(&mut a);
fs::write(t.0.join("result.txt"), "initial").unwrap();
menu(&mut a, Command::Start);
let ide = a.session.vm.as_ref().unwrap();
assert_eq!(ide.rt.command, command);
assert_eq!((ide.rt.screen.cols(), ide.rt.screen.rows()), (80, 25));
assert!(ide.forms.has_visible_forms());
assert!(!ide.rt.dateien.ist_offen(1));
assert!(a.session.host.events.is_empty());
tick(&mut a, 0);
assert_eq!(a.execution, Execution::Waiting, "{}", a.message);
// Same input and time boundary as ScriptHost::warten, through native App input.
a.handle(Event::Paste("abc\n".into()));
tick(&mut a, 100);
assert_eq!(
a.execution,
if command == "!" {
Execution::Ended
} else {
Execution::Error
},
"{}",
a.message
);
if command == "error" {
assert!(a.message.contains('5'));
}
let output = snapshot::text(a.session.screen());
let file = fs::read(t.0.join("result.txt")).unwrap();
assert_eq!(output, format!("? abc\n 2 abc{command}\n"));
assert!(String::from_utf8_lossy(&file).contains('2'));
for target in [path.clone(), path.with_extension("tbc")] {
fs::write(t.0.join("result.txt"), "initial").unwrap();
let input = super::compile(Some(&target.display().to_string()))
.unwrap()
.1;
let initial = new_execution(input, command, Some((80, 25)), None).unwrap();
assert!(initial.forms.has_visible_forms());
assert_eq!(initial.rt.command, command);
assert!(!initial.rt.dateien.ist_offen(1));
let mut host = ScriptHost {
now: 0,
sent: false,
events: Default::default(),
};
let (event, vm) = super::run_chain(
&[target.display().to_string(), command.into()],
&mut host,
Some((80, 25)),
)
.unwrap();
if command == "!" {
assert_eq!(event, RunEvent::Ended);
} else {
assert!(
matches!(event, RunEvent::Error { code: 5, .. }),
"{event:?}"
);
}
assert_eq!(snapshot::text(&vm.rt.screen), output);
assert_eq!(fs::read(t.0.join("result.txt")).unwrap(), file);
assert!(!vm.rt.dateien.ist_offen(1));
assert_eq!(host.now, 100);
}
menu(&mut a, Command::Restart);
assert!(a.session.host.events.is_empty());
assert!(!a.session.vm.as_ref().unwrap().rt.dateien.ist_offen(1));
tick(&mut a, 0);
assert_eq!(a.execution, Execution::Waiting);
a.handle(Event::Paste("abc\n".into()));
tick(&mut a, 100);
assert_eq!(snapshot::text(a.session.screen()), output);
}
}
#[test]
fn every_menu_command_has_native_keyboard_and_mouse_routing() {
let t = Temp::new();
for designer in [false, true] {
let menus = tb_ide::commands::menus(designer);
for (m, group) in menus.iter().enumerate() {
for (i, item) in group.items.iter().enumerate() {
let Some(command) = item.command else {
continue;
};
for mouse in [false, true] {
let mut a = App::new(&t.0, t.0.join("options"), (120, 45)).unwrap();
if designer {
menu(&mut a, Command::NewForm);
}
if mouse {
if designer {
plain(&mut a, K::F(11));
plain(&mut a, K::Esc);
}
click_hit(&mut a, |h| matches!(h,Hit::Menu(n) if *n==m));
click_hit(&mut a, |h| matches!(h,Hit::MenuItem(n) if *n==i));
assert_eq!(a.last_command, Some(command), "{command:?}");
} else {
menu(&mut a, command);
}
assert!(a.basic_events.is_empty(), "{command:?}");
assert!(!a.message.contains("folgt in Phase-5"));
}
}
}
}
}
#[test]
fn function_keys_conflict_contexts_and_errors_keep_a_single_input_owner() {
let t = Temp::new();
let mut a = App::new(&t.0, t.0.join("options"), (100, 30)).unwrap();
menu(&mut a, Command::SyntaxChecking);
replace_code(&mut a, "PRINT (\n");
menu(&mut a, Command::Start);
assert_eq!(a.execution, Execution::Error);
assert!(a.session.vm.is_none());
let invalid = code(&a);
assert!(invalid.contains("PRINT ("));
replace_code(&mut a, "LINE INPUT s$\nPRINT s$\nEND\n");
plain(&mut a, K::F(5));
tick(&mut a, 0);
assert_eq!(a.execution, Execution::Waiting);
plain(&mut a, K::F(4));
assert!(a.session.fullscreen);
plain(&mut a, K::F(4));
assert!(!a.session.fullscreen);
plain(&mut a, K::F(11));
assert!(a.menu.is_some());
plain(&mut a, K::Esc);
plain(&mut a, K::F(2));
assert!(matches!(
a.dialog.as_ref().unwrap().kind,
DialogKind::ChooseCode
));
plain(&mut a, K::Esc);
plain(&mut a, K::F(12));
assert!(a.basic_events.is_empty());
if a.dialog.is_some() {
plain(&mut a, K::Esc);
}
key(&mut a, K::Pause, M::CONTROL);
assert_eq!(a.execution, Execution::Paused);
assert!(a.basic_events.is_empty());
a.handle(Event::Resize(40, 10));
assert!(draw(&mut a).contains("80×25"));
a.handle(Event::Resize(100, 30));
assert!(draw(&mut a).contains("Untitled"));
plain(&mut a, K::F(5));
menu(&mut a, Command::Output);
text(&mut a, "one");
plain(&mut a, K::Enter);
tick(&mut a, 100);
assert_eq!(snapshot::text(a.session.screen()), "? one\none\n");
open_code(&mut a, "Untitled.bas");
replace_code(&mut a, "RUN \"missing\"\n");
menu(&mut a, Command::Start);
tick(&mut a, 0);
assert_eq!(a.execution, Execution::Error);
assert!(a.message.contains("nicht gefunden"));
open_code(&mut a, "Untitled.bas");
assert!(code(&a).contains("missing"));
replace_code(&mut a, "PRINT SHELL(\"exit 7\")\nEND\n");
menu(&mut a, Command::Start);
tick(&mut a, 0);
assert_eq!(
a.session.host.shell_request.take().as_deref(),
Some("exit 7")
);
a.session.host.shell_result = Some(Ok(7));
tick(&mut a, 0);
assert_eq!(snapshot::text(a.session.screen()), " 7 \n");
menu(&mut a, Command::NewForm);
plain(&mut a, K::F(2));
assert!(a.value_focus);
plain(&mut a, K::Esc);
plain(&mut a, K::F(10));
assert!(!a.properties && a.menu.is_some());
plain(&mut a, K::F(10));
assert!(a.properties && a.menu.is_none());
plain(&mut a, K::F(11));
assert!(a.menu.is_some());
plain(&mut a, K::Esc);
plain(&mut a, K::F(12));
assert!(matches!(
a.dialog.as_ref().unwrap().kind,
DialogKind::DesignObject(_)
));
plain(&mut a, K::Esc);
assert!(a.basic_events.is_empty());
}
#[test]
fn partial_save_and_external_edit_recover_without_losing_documents() {
let t = Temp::new();
let mut a = App::new(&t.0, t.0.join("options"), (100, 30)).unwrap();
replace_code(&mut a, "PRINT 1\n");
menu(&mut a, Command::NewModule);
field(&mut a, 0, "Second");
submit(&mut a);
replace_code(&mut a, "PRINT 2\n");
let ids = a.project.members();
menu(&mut a, Command::SaveProject);
field(&mut a, 0, &t.0.join("p.mak").display().to_string());
field(&mut a, 2, &t.0.join("first.bas").display().to_string());
field(
&mut a,
4,
&t.0.join("absent/second.bas").display().to_string(),
);
plain(&mut a, K::Enter);
assert!(!a.dialog.as_ref().unwrap().error.is_empty());
assert!(!a.project.document(ids[0]).unwrap().is_dirty());
assert!(a.project.document(ids[1]).unwrap().is_dirty());
assert!(a
.project
.document(ids[1])
.unwrap()
.code()
.contains("PRINT 2"));
assert!(!t.0.join("p.mak").exists());
field(&mut a, 4, &t.0.join("second.bas").display().to_string());
submit(&mut a);
open_code(&mut a, "first.bas");
replace_code(&mut a, "PRINT 3\n");
fs::write(t.0.join("first.bas"), "' external\n").unwrap();
menu(&mut a, Command::SaveFile);
plain(&mut a, K::Enter);
assert!(a
.dialog
.as_ref()
.unwrap()
.error
.contains("Überschreibentscheidung"));
assert_eq!(
fs::read_to_string(t.0.join("first.bas")).unwrap(),
"' external\n"
);
assert!(code(&a).contains("PRINT 3"));
plain(&mut a, K::Tab);
plain(&mut a, K::Char(' '));
submit(&mut a);
assert!(!a.project.is_dirty());
menu(&mut a, Command::Start);
tick(&mut a, 0);
assert_eq!(a.execution, Execution::Ended);
assert_eq!(snapshot::text(a.session.screen()), " 3 \n 2 \n");
}
#[test]
fn reference_matrix_is_complete_and_points_to_runnable_tests() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let changes = root.join("openspec/changes");
let name = "phase-5-08-integration-und-phasenabnahme";
let active = changes.join(name);
let change = if active.exists() {
active
} else {
fs::read_dir(changes.join("archive"))
.unwrap()
.map(|p| p.unwrap().path())
.find(|p| p.file_name().unwrap().to_string_lossy().ends_with(name))
.unwrap()
};
let overview = fs::read_to_string(
changes
.join("archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md"),
)
.unwrap();
let overview = overview
.split("## Vollständige Zuordnung der Referenzbedienung")
.nth(1)
.unwrap()
.split("## Explizite")
.next()
.unwrap();
let matrix = fs::read_to_string(change.join("befehlsmatrix.md")).unwrap();
let rows = |s: &str| {
s.lines()
.filter(|l| l.starts_with('|'))
.skip(2)
.map(|l| {
l.split('|')
.skip(1)
.map(|c| c.trim().to_owned())
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
};
let expected = rows(overview);
let actual = rows(&matrix);
assert_eq!(expected.len(), 45);
assert_eq!(actual.len(), expected.len());
for (expected, actual) in expected.iter().zip(actual) {
assert_eq!(&actual[..3], &expected[..3]);
let tests: Vec<_> = actual[3].split('`').skip(1).step_by(2).collect();
assert!(!tests.is_empty());
for test in tests {
let (file, function) = test.split_once("::").unwrap();
let source = fs::read_to_string(root.join(file)).unwrap();
assert!(
source.contains(&format!("#[test]\nfn {function}(")),
"{test}"
);
}
}
}

View File

@@ -336,3 +336,6 @@ impl Host for PipeHost {
self.start.elapsed().as_millis() as u64 self.start.elapsed().as_millis() as u64
} }
} }
#[cfg(test)]
mod ide_acceptance;

View File

@@ -720,14 +720,14 @@ impl App {
for f in &input.forms { for f in &input.forms {
catalog.append(&f.catalog()); catalog.append(&f.catalog());
} }
ensure!(
!catalog
.objects
.iter()
.any(|o| o.name.eq_ignore_ascii_case(new)),
"Name {new} bereits vorhanden"
);
let form_name = self.design_form()?.root.name.clone(); let form_name = self.design_form()?.root.name.clone();
ensure!(
catalog.objects.iter().filter(|o| o.name.eq_ignore_ascii_case(new)).all(|o| {
node_key(&selected).1.is_some() && o.array && o.class == selected.class
&& o.parent_form.as_deref().is_some_and(|name| name.eq_ignore_ascii_case(&form_name))
}),
"Name {new} bereits vorhanden; nur indizierte Controls derselben Klasse und Form können ein Array bilden"
);
let target = if selected.class == Class::Form { let target = if selected.class == Class::Form {
catalog.find(&old) catalog.find(&old)
} else { } else {
@@ -887,27 +887,52 @@ impl App {
.join("; ") .join("; ")
) )
})?; })?;
let mut candidate_catalog = forms::FormCatalog::default();
for form in &candidate.forms {
candidate_catalog.append(&form.catalog());
}
let bindings = |references: &[(tb_frontend::SourcePos, String, u16)], let bindings = |references: &[(tb_frontend::SourcePos, String, u16)],
files: &[tb_frontend::source::SourceFile], files: &[tb_frontend::source::SourceFile],
objects: &forms::FormCatalog,
rename_old: bool| { rename_old: bool| {
let mut result = Vec::new(); let mut result = Vec::new();
for (p, n, o) in references { for (p, n, o) in references {
let mut n = n.clone(); let mut n = n.clone();
let object = &objects.objects[*o as usize];
let mut name = object.name.clone();
let mut owner = object.parent_form.clone();
if rename_old && *o == target { if rename_old && *o == target {
name = new.to_uppercase();
if n == old { if n == old {
n = new.to_uppercase(); n = new.to_uppercase();
} else if let Some(r) = handlers.get(&n) { } else if let Some(r) = handlers.get(&n) {
n = r.to_uppercase(); n = r.to_uppercase();
} }
} }
result.push((files[p.source as usize].path.clone(), n, *o)); if rename_old && selected.class == Class::Form && owner.as_deref() == Some(&old) {
owner = Some(new.to_uppercase());
}
// Merging arrays changes catalog indices. Compare semantic object identities.
result.push((
files[p.source as usize].path.clone(),
n,
name,
owner,
object.class.name(),
object.array,
));
} }
result.sort(); result.sort();
result result
}; };
ensure!( ensure!(
bindings(&bound.objects, &source_files, true) bindings(&bound.objects, &source_files, &catalog, true)
== bindings(&candidate_bound.objects, &candidate_compiled.sources, false), == bindings(
&candidate_bound.objects,
&candidate_compiled.sources,
&candidate_catalog,
false
),
"Unklare oder verdeckte Objektverweise; nichts geändert" "Unklare oder verdeckte Objektverweise; nichts geändert"
); );
self.project self.project

View File

@@ -924,3 +924,63 @@ fn events_with_the_same_name_are_local_to_their_form() {
.code() .code()
.contains("SUB Form_LOAD")); .contains("SUB Form_LOAD"));
} }
#[test]
fn indexed_controls_merge_atomically_with_bindings_and_undo() {
let t = Temp::new();
let mut a = t.app();
let root = root(&mut a);
let first = a
.design_place(C::CommandButton, 0, root, Rect::new(1, 1, 8, 2))
.unwrap();
property(&mut a, "INDEX", "0");
let target = node(&mut a, first).name;
let second = a
.design_place(C::CommandButton, 0, root, Rect::new(1, 4, 8, 2))
.unwrap();
let old = node(&mut a, second).name;
// Scalar and duplicate-index merges must leave every document untouched.
let before = a.design_form().unwrap().clone();
assert!(a.design_rename(&target).is_err());
assert_eq!(*a.design_form().unwrap(), before);
property(&mut a, "INDEX", "0");
let before = a.design_form().unwrap().clone();
assert!(a.design_rename(&target).is_err());
assert_eq!(*a.design_form().unwrap(), before);
property(&mut a, "INDEX", "1");
let unrelated = a
.design_place(C::TextBox, 0, root, Rect::new(20, 1, 8, 2))
.unwrap();
let unrelated_name = node(&mut a, unrelated).name;
property(&mut a, "INDEX", "2");
let before = a.design_form().unwrap().clone();
assert!(a.design_rename(&target).is_err());
assert_eq!(*a.design_form().unwrap(), before);
a.design_select(second, false).unwrap();
let doc = a.design_document().unwrap();
let source = format!("SUB {old}_Click(Index AS INTEGER)\n{old}(1).Caption = {target}(0).Caption\n{unrelated_name}(2).Text = \"kept\"\nEND SUB\n");
a.project.replace_text(doc, 0..0, &source).unwrap();
let before = a.design_form().unwrap().clone();
a.design_rename(&target).unwrap();
assert_eq!(node(&mut a, second).name, target);
let code = &a.design_form().unwrap().code;
assert!(code
.to_uppercase()
.contains(&format!("SUB {}_CLICK", target.to_uppercase())));
assert!(code.contains(&format!("{target}(1).Caption")));
assert!(code.contains(&format!("{unrelated_name}(2).Text")));
a.execute(Command::Undo);
assert_eq!(*a.design_form().unwrap(), before);
// Two handlers for the merged event are ambiguous and cannot be committed.
a.project
.replace_text(
doc,
0..0,
&format!("SUB {target}_Click(Index AS INTEGER)\nEND SUB\n"),
)
.unwrap();
let before = a.design_form().unwrap().clone();
a.design_select(second, false).unwrap();
assert!(a.design_rename(&target).is_err());
assert_eq!(*a.design_form().unwrap(), before);
}

View File

@@ -336,7 +336,10 @@ Position und Größe auch bei langen Namen/Werten. Boolean-Werte sind
`true`/`false` beziehungsweise `-1`/`0`. Typen, Metadatenbereiche und `true`/`false` beziehungsweise `-1`/`0`. Typen, Metadatenbereiche und
Laufzeitregeln werden geprüft; schreibgeschützte Properties bleiben erhalten. Laufzeitregeln werden geprüft; schreibgeschützte Properties bleiben erhalten.
Spin.Style wählt 0 (vertikal, 1×2) oder 1 (horizontal, 2×1); Width/Height Spin.Style wählt 0 (vertikal, 1×2) oder 1 (horizontal, 2×1); Width/Height
sind bei Spin schreibgeschützt. Explizites Index=0 erzeugt ein Control-Array. sind bei Spin schreibgeschützt. Explizites Index=0 erzeugt ein Control-Array. Weitere Controls derselben
Klasse und Form erhalten zuerst einen anderen eindeutigen Index und dann
denselben CtlName. Indexkollisionen oder doppelte Ereignisprozeduren werden
ohne Teiländerung abgewiesen; Undo stellt Namen und Codebindungen wieder her.
Cut/Copy/Paste/Clear und Undo wirken im Designer auf Controls; die Cut/Copy/Paste/Clear und Undo wirken im Designer auf Controls; die
Textzwischenablage des Editors bleibt getrennt. Container nehmen ihre Textzwischenablage des Editors bleibt getrennt. Container nehmen ihre

View File

@@ -12,15 +12,22 @@ und mögliche Überschreibbestätigungen stehen im Speicherdialog.
Tools → Command Button wählt das Werkzeug. Ziehe auf der Form eine Fläche Tools → Command Button wählt das Werkzeug. Ziehe auf der Form eine Fläche
auf; alternativ platziert ein Doppelklick auf das Toolbox-Werkzeug ein Control. auf; alternativ platziert ein Doppelklick auf das Toolbox-Werkzeug ein Control.
Wähle das Control und mit Page Up/Down die Eigenschaft Caption. F2 aktiviert Wähle das Control. Falls die Menüleiste statt der Properties Bar sichtbar ist,
wechsle mit F10 zur Properties Bar. Page Up/Down wählt die Eigenschaft Caption. F2 aktiviert
das Wertfeld, tippe `Hallo` und bestätige mit Enter. Pfeile verschieben, das Wertfeld, tippe `Hallo` und bestätige mit Enter. Pfeile verschieben,
Shift+Pfeile verändern die Größe. Die tatsächlichen Klassennamen und Shift+Pfeile verändern die Größe. Die tatsächlichen Klassennamen und
Eigenschaften stehen in der [Forms-Referenz](forms-referenz.md). Eigenschaften stehen in der [Forms-Referenz](forms-referenz.md).
Für ein Control-Array setze beim ersten Control Index auf `0`. Platziere
ein zweites Control derselben Klasse, setze dessen Index auf `1` und
CtlName auf den Namen des ersten Controls. Beide gehören nun zum selben
Array. Jeder Index muss eindeutig sein; Undo nimmt die Umbenennung
einschließlich gebundener Codeverweise zurück.
## Ereigniscode schreiben ## Ereigniscode schreiben
F12 öffnet die Ereignisauswahl des gewählten Controls. Wähle Click und F12 öffnet die Objektauswahl. Bestätige das gewählte Control mit Enter,
bestätige. Die IDE öffnet beziehungsweise erzeugt die passende Prozedur. wähle anschließend Click und bestätige erneut. Die IDE öffnet beziehungsweise erzeugt die passende Prozedur.
Schreibe zwischen SUB und END SUB: Schreibe zwischen SUB und END SUB:
```basic ```basic

View File

@@ -35,7 +35,7 @@ Besondere Risiken sind unverändert ausgeführte alte Kompilate nach einer Text
| 05 | [Formular-Designer](../2026-09-06-phase-5-05-formular-designer/proposal.md) | 01, 02, 03 | Visuelles Gestalten und konsistenter Ereigniscode | | 05 | [Formular-Designer](../2026-09-06-phase-5-05-formular-designer/proposal.md) | 01, 02, 03 | Visuelles Gestalten und konsistenter Ereigniscode |
| 06 | [Debugger und Direktfenster](../2026-09-06-phase-5-06-debugger-und-direktfenster/proposal.md) | 03, 04 | Quellgenaues Debuggen, Watches, Immediate, History und Fehlerhalte | | 06 | [Debugger und Direktfenster](../2026-09-06-phase-5-06-debugger-und-direktfenster/proposal.md) | 03, 04 | Quellgenaues Debuggen, Watches, Immediate, History und Fehlerhalte |
| 07 | [Hilfesystem](../2026-09-07-phase-5-07-hilfesystem/proposal.md) | 02, 03 | Offline-Markdown-Hilfe und Kontextnavigation | | 07 | [Hilfesystem](../2026-09-07-phase-5-07-hilfesystem/proposal.md) | 02, 03 | Offline-Markdown-Hilfe und Kontextnavigation |
| 08 | [Integration und Phasenabnahme](../../phase-5-08-integration-und-phasenabnahme/proposal.md) | 0107 | Durchgängiger Arbeitsablauf, Befehlsabdeckung und nachgewiesener PLAN-Status | | 08 | [Integration und Phasenabnahme](../2026-09-07-phase-5-08-integration-und-phasenabnahme/proposal.md) | 0107 | Durchgängiger Arbeitsablauf, Befehlsabdeckung und nachgewiesener PLAN-Status |
```text ```text
01 --> 02 --> 03 --> 04 --> 06 --+ 01 --> 02 --> 03 --> 04 --> 06 --+

View File

@@ -0,0 +1,65 @@
# Phase 5: ausführbare Befehlsmatrix
Die ersten drei Spalten übernehmen alle 45 Zeilen der ursprünglichen Phase-5-Übersicht.
Zuständigkeiten 0107 bezeichnen die archivierten Featurechanges, 08 die Integration.
Alle hier genannten Rust-Tests laufen mit `cargo test --workspace`.
`reference_matrix_is_complete_and_points_to_runnable_tests` prüft Vollständigkeit,
Zuordnung und vorhandene Testfunktionen auch nach Archivierung dieses Changes.
Der gemeinsame App-Nachweis `every_menu_command_has_native_keyboard_and_mouse_routing`
speist für jeden Menüeintrag in Environment und Designer sowohl Alt/Mnemonic/Pfeile/Enter
als auch gerenderte Maus-Hitbereiche ein. Verfügbarkeitsdiagnosen sind dort erlaubt;
den tatsächlichen Facheffekt im zulässigen Kontext belegen die folgenden Fachtests.
`function_keys_conflict_contexts_and_errors_keep_a_single_input_owner` und der Hauptablauf
prüfen zusätzlich die kontextabhängigen Funktionstasten ohne doppelte BASIC-Zustellung.
Kein interner Befehlsaufruf allein wird als Nachweis des nativen Eingaberoutings gewertet.
File-Shell und BASIC-Shell werden zusätzlich durch `python3 tests/support/ide-execution-pty.py`
im echten lokalen PTY geprüft. Die Emulator-/OS-Matrix bleibt Phase 6.
| Referenzbereich | Befehle / Bedienung | Zuständig | Ausführbare Verhaltensnachweise |
|---|---|---|---|
| File | New Project, Open Project, Save Project | 01, 02 | `crates/tb-cli/src/ide_acceptance.rs::create_save_debug_help_exports_and_reopen` |
| File | New Form, New Module, Add File, Remove File | 01, 02 | `crates/tb-ide/tests/app.rs::file_picker_save_conflict_and_project_replacement_use_real_events`; `crates/tb-ide/tests/documents.rs::startup_removal_requires_explicit_replacement_and_never_deletes_a_file` |
| File | Save File, Save File As, Load Text, Save Text | 01; Textbearbeitung 03 | `crates/tb-ide/tests/app.rs::file_picker_save_conflict_and_project_replacement_use_real_events`; `crates/tb-ide/tests/documents.rs::shared_views_text_import_and_undo_are_document_transactions`; `crates/tb-cli/src/ide_acceptance.rs::create_save_debug_help_exports_and_reopen` |
| File | Print | 04 | `crates/tb-ide/tests/execution.rs::print_uses_utf8_protected_output_and_does_not_mutate_source` |
| File | Shell | 04 | `crates/tb-ide/tests/execution.rs::shell_request_is_resumed_once_and_error_is_basic_error` |
| File | Exit / Alt+F4 | 02; Dokumententscheidungen 01, Sitzungsende 04 | `crates/tb-ide/tests/app.rs::exit_with_partial_save_retains_dialog_and_unsaved_documents` |
| Edit | Undo/Ctrl+Z, Cut/Ctrl+X, Copy/Ctrl+C, Paste/Ctrl+V, Clear/Del | Text 03, Controls 05 | `crates/tb-ide/tests/editor.rs::cua_unicode_overwrite_wordstar_and_shared_undo`; `crates/tb-ide/tests/designer.rs::clipboard_children_arrays_and_delete_preserve_code` |
| Edit | New Sub, New Function | 03 | `crates/tb-ide/tests/editor.rs::procedures_and_shared_save_hook_preserve_drafts_and_frm_code` |
| Edit | Event Procedures / F12 | 05, Codeoperationen 03 | `crates/tb-ide/tests/designer.rs::event_array_signature_twice_and_shared_form_identity`; `crates/tb-cli/src/ide_acceptance.rs::create_save_debug_help_exports_and_reopen` |
| View | Code / F2, Form / Shift+F12 | 02, 03, 05 | `crates/tb-cli/src/ide_acceptance.rs::function_keys_conflict_contexts_and_errors_keep_a_single_input_owner`; `crates/tb-ide/tests/designer.rs::event_array_signature_twice_and_shared_form_identity` |
| View | Next Statement | 06 | `crates/tb-ide/tests/debugger.rs::calls_procedure_step_cursor_history_and_restart` |
| View | Output Screen / F4 | 04 | `crates/tb-ide/tests/execution.rs::output_viewports_resize_and_end_preserve_buffer_and_basic_colors` |
| View | Included File, Included Lines | 03 | `crates/tb-ide/tests/editor.rs::includes_and_diagnostic_selection_use_original_physical_files` |
| View (Designer) | Menu Bar / F10, Grid Lines | 05 auf 02 | `crates/tb-ide/tests/designer.rs::property_bar_types_readonly_and_no_status_line`; `crates/tb-ide/tests/designer.rs::toolbox_double_click_tool_drag_grid_and_window_form_selection` |
| Search | Find, Selected Text/Ctrl+Backslash, Repeat Last Find/F3, Change | 03 | `crates/tb-ide/tests/editor.rs::search_scope_direction_wrap_and_shortening_replace_all` |
| Run | Start/Shift+F5, Restart, Continue/F5, Modify COMMAND$ | 04 | `crates/tb-ide/tests/execution.rs::restart_command_run_targets_and_missing_target_preserve_documents`; `crates/tb-cli/src/ide_acceptance.rs::saved_project_ide_source_cli_and_tbc_have_identical_sessions` |
| Run | Unterbrechen / Ctrl+Break | 04; zusätzlicher zugänglicher IDE-Befehl | `crates/tb-ide/tests/execution.rs::f5_input_pause_copy_and_single_delivery_keep_the_same_vm` |
| Run | Set Start-up File | Persistenz 01, Ausführungswirkung 04 | `crates/tb-ide/tests/execution.rs::startup_selection_matches_shared_cli_pipeline_and_legacy_mak` |
| Run | Make EXE File, Make Library | Vollständige UI 02, UI-Abnahme 08; native Erzeugung und reale Backend-Anbindung Phase 6 | `crates/tb-ide/tests/app.rs::export_dialogs_validate_and_render_all_results_without_creating_files`; `crates/tb-cli/src/ide_acceptance.rs::create_save_debug_help_exports_and_reopen` |
| Debug | Add Watch, Instant Watch/Shift+F9, Watchpoint, Delete Watch, Delete All Watch | 06 | `crates/tb-ide/tests/debugger.rs::dispatcher_marks_edits_rebind_and_readonly_watches`; `crates/tb-ide/tests/debugger.rs::immediate_wait_run_transition_watchpoint_and_trace_are_session_owned`; `crates/tb-ide/tests/debugger.rs::calls_mouse_inspection_instant_key_delete_and_error_window` |
| Debug | Trace On, History On, Shift+F8/Shift+F10 | 06 | `crates/tb-ide/tests/debugger.rs::calls_procedure_step_cursor_history_and_restart`; `crates/tb-ide/tests/debugger.rs::immediate_wait_run_transition_watchpoint_and_trace_are_session_owned` |
| Debug | Toggle Breakpoint/F9, Clear All Breakpoints, Break on Errors, Set Next Statement | 06 | `crates/tb-ide/tests/debugger.rs::dispatcher_marks_edits_rebind_and_readonly_watches`; `crates/tb-vm/tests/debugger.rs::immediate_handler_resume_and_break_on_errors`; `crates/tb-vm/tests/debugger.rs::set_next_rejects_loop_gosub_handler_and_pending_operations_atomically` |
| Debug-Tasten | F7 bis Cursor, F8 Single Step, F10 Procedure Step | 06 | `crates/tb-ide/tests/debugger.rs::calls_procedure_step_cursor_history_and_restart`; `crates/tb-vm/tests/debugger.rs::gosub_step_over_breakpoint_priority_and_temporary_target_cleanup` |
| Options | Display, Set Paths, Right Mouse, Save, Syntax Checking | 02; Wirkung in 01/03/07 | `crates/tb-ide/tests/app.rs::options_apply_persist_across_bases_and_preserve_unsaved_values_on_error`; `crates/tb-ide/tests/editor.rs::line_leave_incomplete_invalid_disabled_and_current_revisions`; `crates/tb-ide/tests/help.rs::right_click_configuration_and_external_link_follow` |
| Window | New Window, Arrange All, offene Codefensterliste | 02 | `crates/tb-ide/tests/app.rs::windows_restore_geometry_and_resize_without_document_loss` |
| Window | Calls, Debug, Immediate | Fenster 02, Inhalt 06 | `crates/tb-ide/tests/debugger.rs::calls_mouse_inspection_instant_key_delete_and_error_window`; `crates/tb-ide/tests/debugger.rs::immediate_wait_run_transition_watchpoint_and_trace_are_session_owned` |
| Window | Help, Output, Project | Fenster 02, Inhalt 07/04/01 | `crates/tb-ide/tests/app.rs::status_and_project_buttons_use_the_keyboard_command_path`; `crates/tb-ide/tests/execution.rs::output_viewports_resize_and_end_preserve_buffer_and_basic_colors`; `crates/tb-ide/tests/help.rs::links_history_resize_scroll_and_focus_use_real_events` |
| Window (Designer) | Color Palette, Menu Design Window, Toolbox, Help, Formliste | 05, Hilfe 07 | `crates/tb-ide/tests/designer.rs::toolbox_double_click_tool_drag_grid_and_window_form_selection`; `crates/tb-ide/tests/designer.rs::property_menu_buttons_palette_drag_and_modal_mouse_are_connected`; `crates/tb-ide/tests/help.rs::help_preserves_fullscreen_and_does_not_modify_designer_or_source` |
| Help | Index, Contents, Keyboard, Topic/F1, Using Help/Shift+F1, Tutorial, About | 07 | `crates/tb-ide/tests/help.rs::twenty_back_steps_end_of_contents_and_all_entries` |
| Menüs | Alt/F11, Mnemonic, Esc, Dialogkennzeichnung „…“, Help rechts | 02 | `crates/tb-ide/tests/app.rs::start_snapshot_and_reference_menus_have_all_commands`; `crates/tb-cli/src/ide_acceptance.rs::every_menu_command_has_native_keyboard_and_mouse_routing` |
| Fenster | F6/Ctrl+F6, Shift+F6, Ctrl+F4, Ctrl+F7/F8/F9/F10/F5, Alt+Minus | 02 | `crates/tb-ide/tests/app.rs::windows_restore_geometry_and_resize_without_document_loss` |
| Projektfenster | Anfangs rechts, Form/Code-Buttons, Enter | 02 auf 01 | `crates/tb-ide/tests/app.rs::status_and_project_buttons_use_the_keyboard_command_path`; `crates/tb-ide/tests/app.rs::clicking_project_members_and_dialog_choices_transfers_focus_and_applies_values` |
| Toolbox/Tools | Check Box, Combo Box, Command Button, Dir List, Drive List, File List, Frame, HScrollBar, Label, List Box, Option Button, Picture Box, Text Box, Timer, VScrollBar; zusätzlich bereits unterstützte Spinvarianten | 05 | `crates/tb-ide/tests/designer.rs::all_metadata_tools_persist_and_preview_matches_runtime_without_events` |
| Designgesten | Toolbox-Doppelklick, Aufziehen, Drag, Sizing Handles, Pfeile, Ctrl+Pfeile, Shift+Pfeile, Tab/Shift+Tab, Ctrl+Klick | 05 | `crates/tb-ide/tests/designer.rs::container_multiselect_keyboard_drag_resize_and_undo`; `crates/tb-ide/tests/designer.rs::all_eight_handles_resize_transactionally_and_preserve_identity_after_structure_edits`; `crates/tb-ide/tests/designer.rs::toolbox_double_click_tool_drag_grid_and_window_form_selection` |
| Properties Bar | Property/Value-Dropdown, Koordinaten/Größe, F2 Value, F10 Barwechsel, keine Statuszeile | 05 auf 02 | `crates/tb-ide/tests/designer.rs::property_bar_types_readonly_and_no_status_line`; `crates/tb-ide/tests/designer.rs::property_menu_buttons_palette_drag_and_modal_mouse_are_connected` |
| Menüdesigner | Caption (&/-), CtlName, Tag, Index, Checked/Enabled/Visible/Separator, Shortcut, Einfügen/Löschen/Ebene/Reihenfolge/Done | 05 | `crates/tb-ide/tests/designer.rs::menu_hierarchy_arrays_shortcuts_and_palette_use_events`; `crates/tb-ide/tests/designer.rs::menu_indent_outdent_rename_and_field_changes_have_one_undo` |
| Farbpalette | ForeColor/BackColor, Drag auf Form | 05 | `crates/tb-ide/tests/designer.rs::property_menu_buttons_palette_drag_and_modal_mouse_are_connected` |
| Editor | Line-Leave-Prüfung/-Normalisierung, Syntax-Checking-Toggle, DECLARE, DEFtype | 03 | `crates/tb-ide/tests/editor.rs::line_leave_incomplete_invalid_disabled_and_current_revisions`; `crates/tb-ide/tests/editor.rs::external_include_revision_and_project_context_declarations` |
| Editor-Tasten | CUA, Einfg, Pos1/Ende, WordStar Ctrl+Q, Ctrl+Y, Bookmarks Ctrl+K,03 / Ctrl+Q,03, Shift+F2/Ctrl+F2 | 03 | `crates/tb-ide/tests/editor.rs::cua_unicode_overwrite_wordstar_and_shared_undo`; `crates/tb-ide/tests/editor.rs::crlf_navigation_word_boundaries_readonly_and_normalization_keep_views` |
| Immediate | PRINT, Zuweisungen, Prozeduraufrufe, ERROR n im Break-Modus | 06 | `crates/tb-vm/tests/debugger.rs::watches_immediate_byref_and_errors_preserve_continuation`; `crates/tb-vm/tests/debugger.rs::immediate_wait_interrupt_nested_error_end_and_run`; `crates/tb-ide/tests/debugger.rs::immediate_wait_run_transition_watchpoint_and_trace_are_session_owned` |
| Hilfe | F1/Rechtsklick, Shift+F1, ◄Thema►, Tab/Shift+Tab, Enter, Alt+F1 (20), Ctrl+F1, Esc | 07 | `crates/tb-ide/tests/help.rs::links_history_resize_scroll_and_focus_use_real_events`; `crates/tb-ide/tests/help.rs::right_click_configuration_and_external_link_follow`; `crates/tb-ide/tests/help.rs::twenty_back_steps_end_of_contents_and_all_entries` |
| Darstellung | DOS-Farben, Magenta-Titel, Desktopzeichen, Tabweite, klickbare Statuskürzel, Zeile/Spalte | 02 | `crates/tb-ide/tests/app.rs::start_snapshot_and_reference_menus_have_all_commands`; `crates/tb-ide/tests/app.rs::status_and_project_buttons_use_the_keyboard_command_path` |
| Start | Begrüßung, Version/Copyright, Untitled-Code/Projekt, keine Easy/Full-Modi | 02, About 07 | `crates/tb-ide/tests/app.rs::start_snapshot_and_reference_menus_have_all_commands`; `crates/tb-ide/tests/help.rs::twenty_back_steps_end_of_contents_and_all_entries` |
| Terminal | Dynamische Größe, physisches Minimum 80×25 | 02; Runtime-/Output-Abgrenzung 04 | `crates/tb-ide/tests/app.rs::windows_restore_geometry_and_resize_without_document_loss`; `crates/tb-ide/tests/execution.rs::output_viewports_resize_and_end_preserve_buffer_and_basic_colors`; `crates/tb-cli/src/ide_acceptance.rs::function_keys_conflict_contexts_and_errors_keep_a_single_input_owner` |
| Laufzeitparität | Dieselbe TBVM in IDE, CLI und TBC | 04/06; Gesamtnachweis 08 | `crates/tb-cli/src/ide_acceptance.rs::saved_project_ide_source_cli_and_tbc_have_identical_sessions` |

View File

@@ -24,6 +24,6 @@ Keine bestehenden Anforderungen werden ersetzt; die neue IDE-Fähigkeit ergänzt
tb-ide-Integrationstests, vorhandene CLI-/VM-/Forms-Tests und Benchmarks, docs/ sowie PLAN.md. Featureimplementierung bleibt jeweils beim verantwortlichen Change; dieser Change schließt Integration und Nachweise. tb-ide-Integrationstests, vorhandene CLI-/VM-/Forms-Tests und Benchmarks, docs/ sowie PLAN.md. Featureimplementierung bleibt jeweils beim verantwortlichen Change; dieser Change schließt Integration und Nachweise.
**Abhängigkeiten:** [phase-5-01-projekt-und-dokumentmodell](../archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/proposal.md), [phase-5-02-ide-rahmen](../archive/2026-09-06-phase-5-02-ide-rahmen/proposal.md), [phase-5-03-editor-und-inkrementelle-uebersetzung](../archive/2026-09-06-phase-5-03-editor-und-inkrementelle-uebersetzung/proposal.md), [phase-5-04-ausfuehrung-und-output](../archive/2026-09-06-phase-5-04-ausfuehrung-und-output/proposal.md), [phase-5-05-formular-designer](../archive/2026-09-06-phase-5-05-formular-designer/proposal.md), [phase-5-06-debugger-und-direktfenster](../archive/2026-09-06-phase-5-06-debugger-und-direktfenster/proposal.md), [phase-5-07-hilfesystem](../archive/2026-09-07-phase-5-07-hilfesystem/proposal.md). **Abhängigkeiten:** [phase-5-01-projekt-und-dokumentmodell](../2026-09-06-phase-5-01-projekt-und-dokumentmodell/proposal.md), [phase-5-02-ide-rahmen](../2026-09-06-phase-5-02-ide-rahmen/proposal.md), [phase-5-03-editor-und-inkrementelle-uebersetzung](../2026-09-06-phase-5-03-editor-und-inkrementelle-uebersetzung/proposal.md), [phase-5-04-ausfuehrung-und-output](../2026-09-06-phase-5-04-ausfuehrung-und-output/proposal.md), [phase-5-05-formular-designer](../2026-09-06-phase-5-05-formular-designer/proposal.md), [phase-5-06-debugger-und-direktfenster](../2026-09-06-phase-5-06-debugger-und-direktfenster/proposal.md), [phase-5-07-hilfesystem](../2026-09-07-phase-5-07-hilfesystem/proposal.md).
**Gesamtplanung:** [Phase-5-Übersicht](../archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md). Die Nummern geben eine gültige Umsetzungsreihenfolge an; OpenSpec erzwingt Change-Abhängigkeiten nicht automatisch. **Gesamtplanung:** [Phase-5-Übersicht](../2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md). Die Nummern geben eine gültige Umsetzungsreihenfolge an; OpenSpec erzwingt Change-Abhängigkeiten nicht automatisch.

View File

@@ -1,21 +1,21 @@
## 1. Vollständiger Arbeitsablauf ## 1. Vollständiger Arbeitsablauf
- [ ] 1.1 Den headless App-Testpfad aus 02 für Datei-/Dialogaktionen und deterministische VM-Ereignisse verwenden; ein Einstiegstest startet die echte App und erstellt ein leeres Projekt ohne vorbereitete Dokumentobjekte. - [x] 1.1 Den headless App-Testpfad aus 02 für Datei-/Dialogaktionen und deterministische VM-Ereignisse verwenden; ein Einstiegstest startet die echte App und erstellt ein leeres Projekt ohne vorbereitete Dokumentobjekte.
- [ ] 1.2 Den Hauptablauf mit zwei BAS-Modulen, FRM, Control-Array, Menü, Include, F12-Ereigniscode und Startdatei über UI-Ereignisse aufbauen; Save/Close/Reopen muss den gleichen Projekt-/Formularstand ergeben. - [x] 1.2 Den Hauptablauf mit zwei BAS-Modulen, FRM, Control-Array, Menü, Include, F12-Ereigniscode und Startdatei über UI-Ereignisse aufbauen; Save/Close/Reopen muss den gleichen Projekt-/Formularstand ergeben.
- [ ] 1.3 Den Ablauf um Start, Include-Breakpoint, Prozedurschritt, Watch, Immediate-Wertänderung und Continue ergänzen; Sollausgabe und Variablenwerte belegen die erwartete Wirkung. - [x] 1.3 Den Ablauf um Start, Include-Breakpoint, Prozedurschritt, Watch, Immediate-Wertänderung und Continue ergänzen; Sollausgabe und Variablenwerte belegen die erwartete Wirkung.
- [ ] 1.4 F1 aus Editor und Designer sowie Tab/Enter/Alt+F1 in denselben Ablauf aufnehmen; Hilfeziel und wiederhergestellter Fokus müssen stimmen. - [x] 1.4 F1 aus Editor und Designer sowie Tab/Enter/Alt+F1 in denselben Ablauf aufnehmen; Hilfeziel und wiederhergestellter Fokus müssen stimmen.
## 2. Grenzfälle und vollständige Bedienung ## 2. Grenzfälle und vollständige Bedienung
- [ ] 2.1 Die gesamte Befehlsmatrix der Phase-5-Übersicht mit konkreten Tests verknüpfen und auf fehlende Einträge prüfen; alle Phase-5-Aktionen müssen im zulässigen Kontext mehr als einen Platzhaltereffekt besitzen. - [x] 2.1 Die gesamte Befehlsmatrix der Phase-5-Übersicht mit konkreten Tests verknüpfen und auf fehlende Einträge prüfen; alle Phase-5-Aktionen müssen im zulässigen Kontext mehr als einen Platzhaltereffekt besitzen.
- [ ] 2.2 Mehrfachbelegungen von F2/F4/F10/F11/F12 sowie Tastatur-/Menü-/Mausäquivalenz über native Eingabeevents prüfen; kein Event darf zugleich als IDE-Aktion und BASIC-Eingabe wirksam werden. - [x] 2.2 Mehrfachbelegungen von F2/F4/F10/F11/F12 sowie Tastatur-/Menü-/Mausäquivalenz über native Eingabeevents prüfen; kein Event darf zugleich als IDE-Aktion und BASIC-Eingabe wirksam werden.
- [ ] 2.3 Kurze Fehlerabläufe für Teilspeichern, externe Änderung, Compilefehler, Pause beim Eingabewarten, RUN-Zielfehler, Shell-Rückkehr und Shrink/Resize hinzufügen; Dokumenterhalt und anschließende Bedienbarkeit müssen belegt sein. - [x] 2.3 Kurze Fehlerabläufe für Teilspeichern, externe Änderung, Compilefehler, Pause beim Eingabewarten, RUN-Zielfehler, Shell-Rückkehr und Shrink/Resize hinzufügen; Dokumenterhalt und anschließende Bedienbarkeit müssen belegt sein.
- [ ] 2.4 Beide nativen Exportdialoge vollständig über App-Ereignisse prüfen; gültige/ungültige Ziele, Überschreibentscheidung und kontrollierte Status-/Fehlerrückmeldungen funktionieren, während der Phase-5-Produktionspfad die fehlende Erzeugung erklärt und keine native Datei vortäuscht. - [x] 2.4 Beide nativen Exportdialoge vollständig über App-Ereignisse prüfen; gültige/ungültige Ziele, Überschreibentscheidung und kontrollierte Status-/Fehlerrückmeldungen funktionieren, während der Phase-5-Produktionspfad die fehlende Erzeugung erklärt und keine native Datei vortäuscht.
## 3. Parität, Leistung und Abschluss ## 3. Parität, Leistung und Abschluss
- [ ] 3.1 Dasselbe gespeicherte Projekt als IDE-Lauf, CLI-Quelllauf und TBC-Lauf mit identischem Hostskript vergleichen; Ausgabe, Anfangszustand, Dateieffekte und Fehler müssen bis auf dokumentierte IDE-Halte übereinstimmen. - [x] 3.1 Dasselbe gespeicherte Projekt als IDE-Lauf, CLI-Quelllauf und TBC-Lauf mit identischem Hostskript vergleichen; Ausgabe, Anfangszustand, Dateieffekte und Fehler müssen bis auf dokumentierte IDE-Halte übereinstimmen.
- [ ] 3.2 Die vorhandenen relevanten Workspace-Regressionen und Release-Compile-Benchmarks einmal auf dem finalen Stand ausführen; Ergebnisse, Referenzhardware, Profil, Größen und Zeitgrenzen in verification.md festhalten. - [x] 3.2 Die vorhandenen relevanten Workspace-Regressionen und Release-Compile-Benchmarks einmal auf dem finalen Stand ausführen; Ergebnisse, Referenzhardware, Profil, Größen und Zeitgrenzen in verification.md festhalten.
- [ ] 3.3 Die endgültige IDE-Bedienung mit Keyboard/Tutorial/Hilfe abgleichen und PLAN.md ausschließlich anhand bestandener Nachweise aktualisieren; die Zuordnung jeder Phase-5-Checkbox und die separat offene Phase-6-Plattformmatrix sind im Abschlussbericht prüfbar. - [x] 3.3 Die endgültige IDE-Bedienung mit Keyboard/Tutorial/Hilfe abgleichen und PLAN.md ausschließlich anhand bestandener Nachweise aktualisieren; die Zuordnung jeder Phase-5-Checkbox und die separat offene Phase-6-Plattformmatrix sind im Abschlussbericht prüfbar.
- [ ] 3.4 Alle acht Changes zusammen gegen die Hauptspezifikation und Befehlsmatrix verifizieren; offene Befunde an ihrem zuständigen Change beheben und den betroffenen Nachweis erneut ausführen, bis keine Phase-5-Befunde übrig sind. - [x] 3.4 Alle acht Changes zusammen gegen die Hauptspezifikation und Befehlsmatrix verifizieren; offene Befunde an ihrem zuständigen Change beheben und den betroffenen Nachweis erneut ausführen, bis keine Phase-5-Befunde übrig sind.

View File

@@ -0,0 +1,165 @@
# Verification Report: phase-5-08-integration-und-phasenabnahme
Stand: 2026-09-07. Geprüft wurden Proposal, Design, sämtliche Aufgaben, das
Delta `ide-phasenabnahme` sowie die sieben synchronisierten Phase-5-Hauptspecs.
Der zusätzliche Abgleich umfasst sämtliche 45 Zeilen der Referenzbedienung.
## Ergebnis
| Dimension | Ergebnis |
| --- | --- |
| Completeness | 12/12 Aufgaben; 5/5 Anforderungen des Changes; 45/45 Matrixzeilen |
| Correctness | 5/5 neue Szenarien; zusammen mit 0107 insgesamt 47 Anforderungen und 67 Szenarien |
| Coherence | Bestehende App, Dokumenttransaktionen, Compiler, VM, CLI und TestBackend wiederverwendet |
| Offene Befunde | 0 CRITICAL, 0 WARNING, 0 SUGGESTION |
## Anforderungen und neue Nachweise
Alle sechs neuen Tests liegen in `crates/tb-cli/src/ide_acceptance.rs`.
Sie laufen über `cargo test -p tb-cli --bin tbc ide_acceptance` und im
Workspace-Lauf. Als Testmodul des bestehenden CLI-Einstiegs können sie dessen
wirkliche `cmd_build`, `compile` und `run_chain` verwenden. Die bereits im
Workspace vorhandenen crossterm-/ratatui-Abhängigkeiten sind dafür zusätzlich
als CLI-Testabhängigkeiten deklariert; es gibt keine neue Laufzeitabhängigkeit.
| Requirement / Scenario | Implementierung und bestandener Nachweis |
| --- | --- |
| Vollständiger IDE-Arbeitsablauf / Schreiben bis Wiederaufnahme | `create_save_debug_help_exports_and_reopen` startet `App::new` ohne vorbereitete Dokumente. Native Tastatur-, Paste- und Mausereignisse erzeugen main.bas, helper.bas, Form1.frm, ein zweielementiges CommandButton-Array, ein Menü, work.bi sowie project.mak. F12 erzeugt Form_Load über Objekt- und Ereignisauswahl. Nach Startup-/Save-Dialog, Projektwechsel und Öffnen stimmen geladene BAS-Texte, vollständiges FormFile und Startdatei mit den gespeicherten Dateien überein; der wieder geöffnete Stand läuft erneut. |
| Arbeitsablauf: Debugger und Hilfe | Derselbe Test öffnet das Include über View → Included File, setzt F9 an Zeile 2, hält vor dem modulübergreifenden Bump-Aufruf und führt ihn mit F10 bis Zeile 3 aus. Add Watch zeigt total=2. Window → Immediate setzt total=9; F5 und Eingabe abc führen zu `? abc\n 9 abc!\n` und Dateiwert 9. Sämtliche gespeicherten Quellbytes bleiben unverändert; Wiederöffnen läuft wieder mit 2. F1 im Designer und auf PRINT im Editor, Tab/Enter/Alt+F1/Esc prüfen Hilfeziel und restaurierten Fokus. |
| Nachgewiesene Befehlsabdeckung / Mehrfach belegte Funktionstasten | `every_menu_command_has_native_keyboard_and_mouse_routing` bedient jeden Menüeintrag beider Modi über Alt/Mnemonic/Pfeile/Enter sowie gerenderte Maus-Hitbereiche. `function_keys_conflict_contexts_and_errors_keep_a_single_input_owner` und der Hauptablauf prüfen F2/F4/F10/F11/F12 einschließlich Designer-Value, Menü, Code-/Ereignisauswahl, Output-Vollbild und Debug-Prozedurschritt. IDE-Kommandos gelangen nicht zusätzlich in die BASIC-Eingabe. Die Facheffekte und zulässigen Kontexte stehen in der separaten, vollständig ausgefüllten [Befehlsmatrix](befehlsmatrix.md). `reference_matrix_is_complete_and_points_to_runnable_tests` vergleicht alle drei Originalspalten der 45 Referenzzeilen und prüft jeden verlinkten Rust-Test auch nach Archivierung. |
| CLI-Parität und robuste Übergänge / Vergleich derselben Sitzung | `saved_project_ide_source_cli_and_tbc_have_identical_sessions` erstellt dasselbe Projekt durch die UI, baut mit der tatsächlichen CLI-Funktion ein TBC und vergleicht IDE, Quell-CLI und TBC-CLI. Auf allen Wegen: 80×25, Uhrstart 0, Eingabe abc/Enter bei 100 ms, identische Anfangsdatei und COMMAND$ (`!` sowie `error`). Ausgabe, berechneter Wert 2, Dateibytes und Resultat END beziehungsweise Fehler 5 stimmen überein. Anfangs sind Form sichtbar, Datei 1 geschlossen und IDE-Eingaben leer; der zusätzliche IDE-Neustart liefert wieder dieselben Resultate. Der deterministische Host ersetzt ausschließlich das Terminal, nicht den CLI-Ausführungspfad. |
| Robuste Übergänge | `partial_save_and_external_edit_recover_without_losing_documents` erzeugt zwei Module ausschließlich über die UI. Ein fehlendes Zielverzeichnis lässt nur das erste Dokument gespeichert, das zweite bearbeitet und das Manifest ungeschrieben; Reparatur im offenen Dialog gelingt. Eine externe Änderung bleibt bis zur expliziten Überschreibentscheidung erhalten; anschließend läuft der erhaltene Projektcode. Der Funktionstasten-/Fehlertest ergänzt Compilefehler, Pause bei INPUT, fehlendes RUN-Ziel, Shell-Callback und Shrink auf 40×10 mit Rückkehr auf 100×30. Reale Shell-Übergabe und Terminalwiederherstellung werden zusätzlich im PTY geprüft. |
| Leistungs- und Dokumentationsabnahme / Abschlussbericht | Alle Workspace-Regressionen, Clippy, Formatprüfung, lokale PTY-Fälle und das vorhandene Release-Compile-Budget sind bestanden; Befehle, Ergebnisse, Hardware und Revision stehen unten. PLAN.md ordnet alle elf Phase-5-Punkte dieser Abnahme zu. Tutorial, Tastatur, Bedienungsreferenz und eingebettete Hilfe wurden auf die endgültigen Eingabewege abgeglichen. |
| Export-UI / Exportvorbereitung ohne Scheinerfolg | Im Hauptablauf werden Make EXE und Make Library über Menüs geöffnet. System/Architektur, leerer Pfad, abgewiesenes TBC-Ziel, bestehende Datei mit ausdrücklicher Überschreibentscheidung und gültiges neues Ziel werden über App-Eingaben geprüft. Das vorhandene `export_dialogs_validate_and_render_all_results_without_creating_files` ergänzt weitere Pfad-/Projektkollisionen. Die Produktionsaktion erklärt Phase 6 und schreibt keine Datei. Kontrollierte Running/Failed/Cancelled/Success-Rückmeldungen prüfen die vorhandene Auftrags-/Ergebnisübergabe; sie sind ausschließlich UI-Tests. Esc erhält den Projektstempel und alle ursprünglichen Dateibytes. |
## Gesamtprüfung der acht Phase-5-Changes
Die folgenden synchronisierten Spec-Dateien wurden mit ihren Implementierungs-
und Testpfaden abgeglichen. Die archivierten Berichte enthalten die detaillierte
Zuordnung der Vorgängerszenarien; ihre Tests wurden auf dem jetzigen Stand erneut
im Workspace ausgeführt. Die neue Befehlsmatrix ergänzt den gemeinsamen nativen
Eingabepfad und verlinkt konkrete ausführbare Facheffekte jeder Referenzzeile.
| Change / Capability | Anforderungen / Szenarien | Zuständiger Code und erneuter Nachweis |
| --- | --- | --- |
| 01 / ide-projekte | 5 / 9 | `tb-ide/src/documents.rs`, `tb-vm/src/project_io.rs`; 14 Dokumenttests, CLI-Projekttests, nativer Hauptablauf und Save-Fehlerablauf |
| 02 / ide-oberflaeche | 7 / 11 | `tb-ide/src/app.rs`, `commands.rs`, `render.rs`, `export.rs`; 12 App-Tests, alle Menürouten, F-Tastenkonflikte, Export-UI und PTY |
| 03 / ide-editor | 7 / 10 | `tb-ide/src/editor.rs`, Frontend-Editing und Projektcompiler; 7 Editortests, VM-Inkrementaltests, Include-Debugablauf und Release-Benchmark |
| 04 / ide-ausfuehrung | 6 / 9 | `tb-ide/src/execution.rs`, `tb-vm/src/project_io.rs`, CLI `run_chain`; 12 Ausführungstests, tatsächliche Dreiweg-Parität, Reset-/Fehlerabläufe und PTY |
| 05 / ide-formulardesigner | 6 / 6 | `tb-ide/src/designer.rs`, gemeinsame FormFile-/Forms-Metadaten; 18 Designertests einschließlich neuer Array-Merge-Gegenproben, UI-Hauptablauf und gespeicherter CLI-Lauf |
| 06 / ide-debugger | 7 / 11 | `tb-ide/src/debugger.rs`, VM-Debugger; 6 IDE- und 14 VM-Debuggertests, tatsächlicher F9/F10/Watch/Immediate/F5-Ablauf |
| 07 / ide-hilfe | 4 / 6 | `tb-ide/src/help.rs`, native F1-Aufrufer, eingebettete docs; 11 Hilfetests, Offline-Kindprozess sowie Hauptablauf mit Editor-/Designerhilfe |
| 08 / ide-phasenabnahme | 5 / 5 | Sechs neue CLI-Abnahmetests, Befehlsmatrix, PLAN-Zuordnung und dieser Bericht |
Keine Phase-5-Platzhalteraktion ist mehr vorhanden. Die einzige weiterhin
vorbereitete, ausdrücklich noch nicht ausführbare Erzeugungsleistung betrifft
das in Phase 6 geplante native Exportbackend.
## Behobene Befunde
1. **Control-Array über CtlName nicht herstellbar (Featurebereich 05).**
Die gemeinsame Umbenennung lehnte jeden bereits vorhandenen Namen ab.
Jetzt dürfen explizit indizierte Controls derselben Klasse und Form zu
einer Arraygruppe zusammengeführt werden. Formvalidierung und vollständige
Kandidatenübersetzung prüfen Index- und Ereigniskollisionen vor dem Commit.
Die Bindungsprüfung verwendet die semantische Objektidentität statt des
durch Zusammenführen verschobenen numerischen Katalogindex.
`indexed_controls_merge_atomically_with_bindings_and_undo` prüft erfolgreiche
Zusammenführung samt Ziel-/Quell-/unbeteiligten Codebindungen, vollständiges
Undo sowie atomare Ablehnung von Skalar-, Index-, Klassen- und Handlerkonflikten.
Alle bisherigen Umbenennungs-/Include-/Schattenbindungsregressionen bestehen.
2. **Tutorial verkürzte die tatsächliche Bedienfolge (Featurebereich 07).**
F12 zeigt zuerst die Objekt-, danach die Ereignisauswahl. Das Tutorial nennt
jetzt beide Bestätigungen und den nötigen F10-Wechsel zur Properties Bar
nach Menübedienung. Control-Array-Erzeugung und ihre Grenzen sind in Tutorial
und Bedienungsreferenz beschrieben. Help-Katalog und native Aufrufer bestehen.
Die fachliche Korrektur liegt im bestehenden Designerpfad; die Integration
führt keinen zweiten Designer und keinen alternativen Compiler-/VM-Pfad ein.
Es wurden keine Fachanforderungen abgeschwächt und keine bisherigen Tests entfernt.
## PLAN-Zuordnung
Alle elf Checkboxen unter Phase 5 sind anhand folgender bestandener Nachweise
abgehakt; die Bezeichnung „Eigenschaftenfenster“ wurde zu „Properties Bar“ präzisiert.
| Phase-5-Punkt | Nachweis |
| --- | --- |
| IDE-Rahmen | App-Tests, vollständige native Menürouten, Resize-/Fokusprüfung |
| Editor | Editortests, Help-Aufrufer, Include-Navigation, Inkrementaltests und Compile-Budget |
| Projektverwaltung | UI-Hauptablauf, drei persistierte Mitglieder, Save/Close/Reopen, CLI-/TBC-Parität |
| Formular-Designer | Vollständige Metadaten-Toolbox, 18 Designerregressionen, Array/Menü/Form_Load im UI-Ablauf |
| Ausführen aus der IDE | Start/Pause/Continue/Restart, Fehlerabläufe, deterministische Dreiweg-Parität |
| Export-Bedienung | Beide vollständigen Dialoge im Hauptablauf und App-Exportregression |
| `$INCLUDE` | Bestehende relative/Zyklus-/Overlayprüfungen; physischer Include-Breakpoint und Dreiweg-Parität |
| `RUN` | Bestehende Ziel-/Resetprüfungen, fehlendes Ziel im nativen Fehlerablauf und sauberer Neustart |
| Debugger | IDE-/VM-Debuggertests und tatsächliche Watch-Wertänderung mit F10/F5 im Hauptablauf |
| Hilfe-System | Elf Hilfetests, Offline-Kindprozess, Kataloglinks und beide tatsächlichen F1-Aufrufer |
| Vollständiges IDE-Programm | `create_save_debug_help_exports_and_reopen` und anschließender Dreiweg-Lauf derselben gespeicherten Dateien |
## Abschlussprüfungen und Messung
Geprüfter Code: Basisrevision `cc828308893b95fb614fbd2ec102a811358c7762`
plus der uncommittete Implementierungsstand dieses Changes. Keine nachträgliche
Produktcodeänderung nach dem finalen Workspace-/Clippy-/Benchmark-Lauf.
Implementierungsfingerabdruck SHA-256: `b616a1b94ce60bb215b104e5c6b4fe0edde4033703dda7a2d6124fea5e9e38b2`
(Pfade lexikografisch sortiert, jeweils UTF-8-Pfad, NUL, Dateibytes, NUL:
`Cargo.lock`, `crates/tb-cli/Cargo.toml`, `crates/tb-cli/src/main.rs`,
`crates/tb-cli/src/ide_acceptance.rs`, `crates/tb-ide/src/designer.rs`,
`crates/tb-ide/tests/designer.rs`).
Die abschließenden PLAN-/Bericht-/Task-Änderungen wurden separat validiert;
wegen des eingebetteten PLAN wurde die Hilfeprüfung danach erneut ausgeführt.
| Befehl | Ergebnis |
| --- | --- |
| `cargo test -p tb-cli --bin tbc ide_acceptance` | 6 bestanden |
| `cargo test -p tb-ide --test designer` | 18 bestanden |
| `cargo test --workspace` | 605 bestanden plus 1 erfolgreicher Offline-Kindprozess; 0 Fehler; 2 bestehende Ignore-Fälle |
| `cargo clippy --workspace --all-targets -- -D warnings` | bestanden |
| `cargo fmt --all -- --check` | bestanden |
| `cargo build -p tb-ide` | bestanden |
| `python3 tests/support/ide-execution-pty.py` | 3 bestanden: Shell Exit 7, Ctrl+C im Shell-Kind, File → Shell; Rückkehr und Terminalrestaurierung geprüft |
| `cargo bench -p tb-vm --bench compile` | alle vier bestehenden Zeitbudgets eingehalten, einmaliger Abschlusslauf |
| `cargo test -p tb-ide --test help` | 11 bestanden plus Offline-Kindprozess, nach PLAN-Abschluss |
| `openspec validate --all --strict --no-interactive` | 23/23 gültig; bestehende INFO-Längenhinweise sind keine Fehler |
| `git diff --check` | bestanden |
Referenzhardware: Apple M5 Max, aarch64, 18 CPU-Kerne, 128 GiB RAM;
macOS 26.6.2 (25G83). rustc 1.97.1 (8bab26f4f, 2026-07-14),
cargo 1.97.1 (c980f4866, 2026-06-30). Bestehender eigener Benchmark-Harness,
Release mit opt-level=3, LTO=true, codegen-units=1. Kein neues Benchmarkverfahren.
| Messung | Größe / Verfahren | Ergebnis | Bestehendes Budget |
| --- | --- | --- | --- |
| Vollcompile Einzelmodul | 508 Zeilen; Aufwärmen, bester von 10 Läufen | 0,74 ms | <50 ms |
| Vollcompile Projekt | 20 Module, 49.760 Zeilen, 335.421 Instruktionen; ein Lauf | 98 ms | <1.000 ms |
| Cacheänderung Einzelmodul | Private Rumpfänderung; Median aus 7; 1 neu / 0 wiederverwendet | 0,77 ms | <50 ms |
| Cacheänderung Projekt | Private Rumpfänderung; Median aus 7; 1 neu / 19 wiederverwendet; Invalidierung + Link | 21,94 ms | <1.000 ms |
Die beiden bestehenden Ignore-Fälle sind der explizite Golden-Output-Generator
und die externe VBDOS-Referenzsuite (`TB_VBDOS_REPO`); kein Phase-5-Test ist
ignoriert. Die normale vorhandene Workspace-Regression wurde vollständig ausgeführt.
Die lokale Abnahme verwendet native crossterm-Ereignisse, TestBackend und einen
realen Unix-PTY. Sie ersetzt nicht die separat offene Phase-6-Matrix für Windows
Terminal, mehrere Linux-Emulatoren und macOS samt systematischen Maus-/Sondertasten.
Auch native Executable-/Library-Erzeugung und deren Verbraucherprüfung bleiben
unverändert offene Phase-6-Aufgaben. Kontrollierte Exportcallbacks behaupten
keinen tatsächlichen Export.
Keine Prüfdimension dieses Changes wurde ausgelassen. Die erneute Verifizierung
meldet keine offenen Befunde. Der Change ist zur separaten Synchronisierung und
Archivierung bereit.
## Synchronisierung und Archivierung
Am 2026-09-07 wurden alle fünf Anforderungen einschließlich Purpose unverändert
in `openspec/specs/ide-phasenabnahme/spec.md` synchronisiert. Der vollständige
Vergleich mit dem Delta und die strikte Validierung aller 23 Hauptspecs bestanden
vor dem Archivieren. Der Change liegt jetzt unter
`openspec/changes/archive/2026-09-07-phase-5-08-integration-und-phasenabnahme/`.
Abhängigkeits- und Übersichtslinks wurden an den Archivpfad angepasst.
Der ausführbare Matrixnachweis besteht auch am Archivort; OpenSpec führt
keine aktiven Changes mehr. Der oben dokumentierte Produktcode-Fingerabdruck
ist unverändert, daher wurden die bereits bestandenen Produkt- und
Leistungsprüfungen nicht erneut ausgeführt.

View File

@@ -0,0 +1,42 @@
# ide-phasenabnahme Specification
## Purpose
Sichert die vollständige Nutzbarkeit der Phase-5-IDE über zusammenhängende Arbeitsabläufe, gemeinsame Laufzeitsemantik und reproduzierbare Abnahmekriterien.
## Requirements
### Requirement: Vollständiger IDE-Arbeitsablauf
Ein Benutzer SHALL ausschließlich über die IDE ein Projekt mit mindestens zwei BAS-Modulen, einem Formular, einem Control-Array, einem Menü und einem Include erstellen, bearbeiten, speichern, schließen und erneut öffnen können. Danach SHALL dasselbe Projekt mit Ereigniscode ausführbar und mit Breakpoints, Prozedurschritt, Watch und Direktfenster debugbar sein. Kontext-Hilfe SHALL aus Editor und Designer erreichbar sein.
#### Scenario: Schreiben bis Wiederaufnahme
- **WHEN** dieser Arbeitsablauf durch Menüs, Tastatur und Maus durchgeführt wird und während eines Halts ein Wert im Direktfenster geändert wird
- **THEN** läuft das Programm mit der Änderung weiter, die gespeicherten Quellen bleiben konsistent und nach erneutem Öffnen sind Projekt, Formularstruktur, Ereigniscode und Startdatei erhalten
### Requirement: Nachgewiesene Befehlsabdeckung
Alle Phase-5-Funktionen und die ihnen zugeordneten Menü-, Tastatur- und Mauswege aus docs/ide-referenz.md SHALL einen zuständigen Change und einen ausführbaren Nachweis besitzen. Ein Test nur des internen Befehls SHALL keinen Nachweis für die tatsächliche Tasten-/Mausverteilung ersetzen. Noch nicht implementierte Phase-5-Aktionen MUST NOT als vollständige Phasenabnahme gelten.
#### Scenario: Mehrfach belegte Funktionstasten
- **WHEN** die Abnahme F2, F4, F10, F11 und F12 in ihren verschiedenen Kontexten einspeist
- **THEN** bestätigt sie die jeweils erwartete Aktion und den Ausschluss einer zweiten Zustellung an das BASIC-Programm
### Requirement: CLI-Parität und robuste Übergänge
Gespeicherte Projekte SHALL über IDE, CLI-Quellstart und CLI-Kompilatstart bei gleichen Eingaben, Zeiten, Größen und COMMAND$ dieselben BASIC-Ausgaben, Datenwerte und Fehler liefern, abgesehen von den ausdrücklich spezifizierten IDE-Halten. Speichern, Projektwechsel, Reset, Shell-Rückkehr und Resize SHALL auch nach Fehlern die Dokumente und Terminalbedienbarkeit erhalten.
#### Scenario: Vergleich derselben Sitzung
- **WHEN** ein Forms-/Include-Projekt mit identischer Ereignisfolge über die drei Ausführungswege gestartet wird
- **THEN** stimmen Ausgabe, Dateieffekte und Laufzeitresultat überein und ein anschließender IDE-Neustart enthält keinen Zustand des vorherigen Laufs
### Requirement: Leistungs- und Dokumentationsabnahme
Phase 5 SHALL erst als umgesetzt dokumentiert werden, wenn die Change-Szenarien, die relevanten bisherigen Regressionen und das bestehende Release-Compile-Budget nachgewiesen sind. PLAN.md und die IDE-Hilfe SHALL den verifizierten Stand wiedergeben. Prüfberichte SHALL konkrete Befehle, Ergebnisse, Revision und verbleibende Grenzen nennen; vollständige OpenSpec-Artefakte allein SHALL nicht als Implementierungsnachweis gelten.
#### Scenario: Abschlussbericht
- **WHEN** Phase 5 abgenommen wird
- **THEN** verweisen alle Phase-5-Checkboxen auf bestandene Nachweise und der Bericht unterscheidet die lokale headless Abnahme von der weiterhin separat geplanten Plattformmatrix aus Phase 6
### Requirement: Export-UI als eigene Phasenleistung
Die Phase-5-Abnahme SHALL die vollständige Bedienung von Make EXE File und Make Library einschließlich Zielsystem/-architektur, Ausgabepfad, Validierung, Überschreibentscheidung sowie Status-/Fehler-/Abbruchdarstellung prüfen. Dabei SHALL die noch nicht verfügbare native Erzeugung aus Phase 6 ausdrücklich erkennbar sein. Die Abnahme SHALL Oberflächennachweise von der späteren tatsächlichen Erzeugung eigenständiger nativer Executables und nativer Systembibliotheken unterscheiden.
#### Scenario: Exportvorbereitung ohne Scheinerfolg
- **WHEN** beide Exportdialoge im durchgängigen IDE-Ablauf mit gültigen und ungültigen Angaben bedient werden
- **THEN** funktionieren Eingabe, Validierung und Rückkehr zum Projekt, die Erzeugungsaktion nennt die Phase-6-Abhängigkeit und weder eine falsche Erfolgsmeldung noch eine als natives Artefakt ausgegebene TBC-Datei entsteht