diff --git a/crates/oa-desktop/src/app.rs b/crates/oa-desktop/src/app.rs index ee3d5a69..07a7a591 100644 --- a/crates/oa-desktop/src/app.rs +++ b/crates/oa-desktop/src/app.rs @@ -130,6 +130,7 @@ pub enum Message { FieldShape(String, Value), FieldAppend(String, Value), FieldValuePath(String, crate::data_values::FieldPathKey), + FieldValueBrowse(String, Option, usize), FieldVariant(String, Value), FieldArray(String, Option, i32), Apply, @@ -1139,6 +1140,18 @@ impl App { self.status = error; } } + Message::FieldValueBrowse(path, scope, page) => { + if let Some(draft) = &mut self.draft + && draft + .value + .pointer(&path) + .is_some_and(|row| row.get("expression").is_some()) + { + draft + .value_browsers + .insert(path, crate::forms::ValueBrowser { scope, page }); + } + } Message::FieldValuePath(path, key) => { if let Some(draft) = &mut self.draft && let Err(error) = draft.value_path(&path, key) diff --git a/crates/oa-desktop/src/data_values.rs b/crates/oa-desktop/src/data_values.rs index 64241d40..14e10f4f 100644 --- a/crates/oa-desktop/src/data_values.rs +++ b/crates/oa-desktop/src/data_values.rs @@ -97,6 +97,7 @@ fn sorted_choices(mut choices: Vec) -> Vec { choices } +#[cfg(test)] pub fn choices( project: &Project, exchange: &Relation, @@ -142,6 +143,19 @@ pub fn roots(project: &Project, exchange: &Relation, phase: &MessageKind) -> Vec ) } +/// Browse one record at a time, including explicitly chosen recursive hops. +pub fn at_scope( + project: &Project, + exchange: &Relation, + phase: &MessageKind, + scope: Option<&FieldPathKey>, +) -> Result, String> { + match scope { + Some(key) => children(project, exchange, phase, key), + None => Ok(roots(project, exchange, phase)), + } +} + /// Resolve authored finite paths as well as the finite representative picker list. /// In particular, optional recursive Value records may be traversed repeatedly. pub fn resolve( diff --git a/crates/oa-desktop/src/forms.rs b/crates/oa-desktop/src/forms.rs index a7f30ba1..36563e68 100644 --- a/crates/oa-desktop/src/forms.rs +++ b/crates/oa-desktop/src/forms.rs @@ -32,6 +32,12 @@ pub struct Input { pub raw: String, pub editor: text_editor::Content, } +#[derive(Clone, Default)] +pub struct ValueBrowser { + pub scope: Option, + pub page: usize, +} + pub struct Draft { pub kind: RecordKind, pub original: Id, @@ -40,6 +46,7 @@ pub struct Draft { pub inputs: BTreeMap, pub exact_strings: bool, port_contract_drafts: BTreeMap, + pub value_browsers: BTreeMap, } impl Draft { @@ -77,11 +84,14 @@ impl Draft { inputs: BTreeMap::new(), exact_strings: false, port_contract_drafts: BTreeMap::new(), + value_browsers: BTreeMap::new(), }; draft.rebuild(); Some(draft) } fn rebuild(&mut self) { + // Array/variant edits can change the meaning of a row path. + self.value_browsers.clear(); self.inputs.clear(); fn visit(value: &Value, path: &str, out: &mut BTreeMap) { match value { @@ -710,10 +720,15 @@ fn reference_ids(draft: &Draft, project: &Project, path: &str) -> Vec { .. }, .. - } => project - .relations - .get(id) - .and_then(|r| crate::data_values::choices(project, r, message_kind).ok()), + } => project.relations.get(id).and_then(|r| { + let scope = address_path.split_last().map(|(&field, path)| { + crate::data_values::FieldPathKey { + path: path.to_vec(), + field, + } + }); + crate::data_values::at_scope(project, r, message_kind, scope.as_ref()).ok() + }), _ => None, }) .unwrap_or_default() diff --git a/crates/oa-desktop/src/forms/data_fields.rs b/crates/oa-desktop/src/forms/data_fields.rs index 05f9f1e8..2f87a22d 100644 --- a/crates/oa-desktop/src/forms/data_fields.rs +++ b/crates/oa-desktop/src/forms/data_fields.rs @@ -330,104 +330,152 @@ fn message_value<'a>( } => project.relations.get(id).map(|r| (r, message_kind)), _ => None, }); - let choices = context - .map(|(r, phase)| crate::data_values::choices(project, r, phase)) - .unwrap_or_else(|| Ok(vec![])); - let mut out = column![text("Qualified carried field").size(11).color(style::MUTED)].spacing(6); - match choices { - Err(error) => out = out.push(text(error).size(11).color(style::MUTED)), - Ok(mut choices) => { - let current_key = current.as_ref().map(|v| crate::data_values::FieldPathKey { - path: v.path.clone(), - field: v.field, - }); - let mut selected = current_key + let current_key = current + .as_ref() + .map(|value| crate::data_values::FieldPathKey { + path: value.path.clone(), + field: value.field, + }); + let selected = context.and_then(|(exchange, phase)| { + current_key + .as_ref() + .and_then(|key| crate::data_values::resolve(project, exchange, phase, key)) + }); + let browser = draft + .value_browsers + .get(path) + .cloned() + .unwrap_or_else(|| ValueBrowser { + scope: selected .as_ref() - .and_then(|key| choices.iter().find(|c| &c.key == key)) - .cloned() - .or_else(|| { - context.and_then(|(r, phase)| { - current_key - .as_ref() - .and_then(|key| crate::data_values::resolve(project, r, phase, key)) - }) - }); - if let Some(choice) = &selected - && !choices.iter().any(|c| c.key == choice.key) - { - choices.push(choice.clone()); - } - if selected.is_none() - && let Some(key) = current_key.clone() - && !key.field.is_nil() - { - out = out.push(text("Current address needs review. Choose an explicit path to resolve a legacy, ambiguous or incompatible value.").size(11).color(style::MUTED)); - let retained = crate::data_values::FieldPathChoice { - picker_label: format!( - "{} · current address needs review", - project.named(key.field) - ), - label: format!( - "Current address · {} / {}", - key.path - .iter() - .map(|id| project.named(*id)) - .collect::>() - .join(" → "), - project.named(key.field) - ), - key, - }; - selected = Some(retained.clone()); - choices.push(retained); - } - if let Some(choice) = &selected { - out = out.push(text(choice.label.clone()).size(11).color(style::MUTED)); - } - let owned = path.to_owned(); - out = out.push( - pick_list(choices, selected, move |choice| { - Message::FieldValuePath(owned.clone(), choice.key) - }) - .placeholder("Choose a carried field path…") - .text_size(12) - .padding(8) - .width(Fill), - ); - } + .and_then(|choice| crate::data_values::parent(&choice.key)), + page: 0, + }); + let mut out = column![text("Qualified carried field").size(11).color(style::MUTED)].spacing(6); + if let Some(choice) = &selected { + out = out.push(text(choice.label.clone()).size(11)); + } else if let Some(key) = ¤t_key + && !key.field.is_nil() + { + out = out.push( + text(format!( + "Current address needs review: {} / {}. Choose an explicit field.", + key.path + .iter() + .map(|id| project.named(*id)) + .collect::>() + .join(" → "), + project.named(key.field) + )) + .size(11) + .color(style::MUTED), + ); } - if let (Some((exchange, phase)), Some(current)) = (context, current) { - let key = crate::data_values::FieldPathKey { - path: current.path, - field: current.field, - }; - if crate::data_values::resolve(project, exchange, phase, &key).is_some() { - if let Some(parent) = crate::data_values::parent(&key) { - out = out.push( - button(text("Up to enclosing field").size(11)) + if let Some((exchange, phase)) = context { + let location = browser + .scope + .as_ref() + .and_then(|key| crate::data_values::resolve(project, exchange, phase, key)) + .map(|choice| choice.label) + .unwrap_or_else(|| "Root carried fields".into()); + out = out.push( + text(format!("Browse: {location}")) + .size(11) + .color(style::MUTED), + ); + if let Some(scope) = &browser.scope { + let mut navigation = row![button(text("↑ Up").size(11)).style(style::nav).on_press( + Message::FieldValueBrowse(path.into(), crate::data_values::parent(scope), 0) + )] + .spacing(4); + if !scope.path.is_empty() { + navigation = navigation.push( + button(text("Root fields").size(11)) .style(style::nav) - .on_press(Message::FieldValuePath(path.into(), parent)), + .on_press(Message::FieldValueBrowse(path.into(), None, 0)), ); } - match crate::data_values::children(project, exchange, phase, &key) { - Ok(children) if !children.is_empty() => { - let path = path.to_owned(); + out = out.push(navigation); + } + match crate::data_values::at_scope(project, exchange, phase, browser.scope.as_ref()) { + Ok(choices) => { + let count = choices.len(); + let (visible, page, pages) = field_page(choices, browser.page); + // Selection detail stays visible independently of browser location/page. + let visible_selection = selected + .clone() + .filter(|choice| visible.iter().any(|c| c.key == choice.key)); + let owned = path.to_owned(); + out = out.push( + pick_list(visible, visible_selection, move |choice| { + Message::FieldValuePath(owned.clone(), choice.key) + }) + .placeholder("Choose a field here…") + .text_size(12) + .padding(8) + .width(Fill), + ); + if pages > 1 { out = out.push( - pick_list( - children, - None::, - move |choice| Message::FieldValuePath(path.clone(), choice.key), - ) - .placeholder("Choose a field inside this record…") - .text_size(12) - .padding(8) - .width(Fill), + row![ + button(text("Previous").size(11)) + .style(style::nav) + .on_press_maybe((page > 0).then(|| Message::FieldValueBrowse( + path.into(), + browser.scope.clone(), + page - 1 + ))), + text(format!("{} / {pages} · {count} fields", page + 1)).size(11), + button(text("Next").size(11)) + .style(style::nav) + .on_press_maybe((page + 1 < pages).then(|| { + Message::FieldValueBrowse( + path.into(), + browser.scope.clone(), + page + 1, + ) + })) + ] + .spacing(4) + .align_y(iced::Alignment::Center), + ); + } else if count == 0 { + out = out.push( + text("No compatible fields at this location.") + .size(11) + .color(style::MUTED), ); } - Err(error) => out = out.push(text(error).size(11).color(style::MUTED)), - _ => {} } + Err(error) => out = out.push(text(error).size(11).color(style::MUTED)), } + if let Some(choice) = &selected + && browser.scope.as_ref() != Some(&choice.key) + && crate::data_values::children(project, exchange, phase, &choice.key) + .is_ok_and(|children| !children.is_empty()) + { + out = out.push( + button( + text(format!( + "Browse fields in {} →", + project.named(choice.key.field) + )) + .size(11), + ) + .style(style::nav) + .on_press(Message::FieldValueBrowse( + path.into(), + Some(choice.key.clone()), + 0, + )), + ); + } + } else { + out = out.push( + text("Choose a message exchange to browse its carried fields.") + .size(11) + .color(style::MUTED), + ); } if let Some(expression) = value.get("expression") { out = out.push(super::property( @@ -442,6 +490,25 @@ fn message_value<'a>( out.into() } +const FIELDS_PER_PAGE: usize = 24; + +fn field_page( + choices: Vec, + requested: usize, +) -> (Vec, usize, usize) { + let pages = choices.len().div_ceil(FIELDS_PER_PAGE).max(1); + let page = requested.min(pages - 1); + ( + choices + .into_iter() + .skip(page * FIELDS_PER_PAGE) + .take(FIELDS_PER_PAGE) + .collect(), + page, + pages, + ) +} + #[cfg(test)] mod tests { use super::*; @@ -497,6 +564,25 @@ mod tests { format!("{path}/expression"), "newly typed".into(), )); + let before_browse = app.draft.as_ref().unwrap().materialize().unwrap(); + for (scope, page) in [ + (None, 0), + ( + Some(crate::data_values::FieldPathKey { + path: vec![f.origin], + field: next, + }), + 999, + ), + ] { + let _ = app.update(Message::FieldValueBrowse(path.into(), scope, page)); + let _ = super::super::view(app.draft.as_ref().unwrap(), app.project()); + assert_eq!( + app.draft.as_ref().unwrap().materialize().unwrap(), + before_browse + ); + assert_eq!(app.project(), &f.project); + } let _ = app.update(Message::FieldValuePath( path.into(), crate::data_values::FieldPathKey { @@ -560,6 +646,43 @@ mod tests { let _ = super::super::view(app.draft.as_ref().unwrap(), app.project()); } + #[test] + fn field_browser_pages_every_direct_field_without_flattening_other_records() { + let f = crate::data_values::tests::wide_payload(); + let exchange = &f.project.relations[&f.exchange]; + let roots = + crate::data_values::at_scope(&f.project, exchange, &MessageKind::Call, None).unwrap(); + assert!(roots.len() > FIELDS_PER_PAGE); + assert!(roots.iter().all(|choice| choice.key.path.is_empty())); + let scope = crate::data_values::FieldPathKey { + path: vec![], + field: f.origin, + }; + let children = + crate::data_values::at_scope(&f.project, exchange, &MessageKind::Call, Some(&scope)) + .unwrap(); + assert!(children.len() > FIELDS_PER_PAGE); + assert!( + children + .iter() + .all(|choice| choice.key.path == vec![f.origin]) + ); + for choices in [roots, children] { + let (_, _, pages) = field_page(choices.clone(), 0); + let visited: Vec<_> = (0..pages) + .flat_map(|page| { + let (visible, actual, _) = field_page(choices.clone(), page); + assert_eq!(actual, page); + assert!(visible.len() <= FIELDS_PER_PAGE); + visible + }) + .collect(); + assert_eq!(visited, choices); + assert_eq!(field_page(choices, usize::MAX).1, pages - 1); + } + assert_eq!(field_page(vec![], usize::MAX), (vec![], 0, 1)); + } + #[test] fn numeric_bound_drafts_keep_partial_and_full_precision_values_out_of_accepted_state() { let mut p = Project::empty(); diff --git a/harness/DECISIONS.md b/harness/DECISIONS.md index 579d2765..a10397eb 100644 --- a/harness/DECISIONS.md +++ b/harness/DECISIONS.md @@ -63,3 +63,9 @@ State camera/tool lifetime follows effective machine and diagram identity, even when selection changes through Catalog or Layer outside Behavior. Reset only on actual context change; preserve same-view return. Independent review reproduced the leak and native Windows/Linux tests verified the correction in316dc55. + +- Carried-value property browsing follows one exact record scope at a time, with + bounded pages and a separate read-only browser location. Flattened representative + paths were proven ambiguous in native recursive/reused data. Browsing does not + change the selected field or expression; choosing a field remains an explicit + draft edit through the ordinary validated command. No persistent browser schema. diff --git a/harness/STATE.md b/harness/STATE.md index feae4ca5..cca2e587 100644 --- a/harness/STATE.md +++ b/harness/STATE.md @@ -134,10 +134,10 @@ plan and decision are recorded in that checkout. No overlapping writers or targe ## Environment and native ownership -The user explicitly reauthorized native testing after Escape. Root's current R6 -window1835476 uses the frozen ab661f2 binary and ui-r6-windows/model/project.arcadia; -13objects/4links/1 expected missing-SA-System warning, saved. Currently1100x700. -Original Capella is present. Older protected user7210312/6228978 were absent; +The user explicitly reauthorized native testing after Escape. Root's f5467e5 R6 +window3476204 and exec93768 closed normally after verified source migration and +55-object recursive scenario reopen; no runtime output. The property-browser +replacement build is running. Original Capella is present. Older protected user7210312/6228978 were absent; do not reset/save/close them if they return. Re-inventory before switching apps. Evidence: D:/CodexBuilds/Capella2-release1-01a06e2e. State source checkout/target: @@ -150,10 +150,12 @@ Windows: cargo test --locked --workspace; cargo clippy --locked --workspace WSL Ubuntu24.04 uses target/linux-cargo.toml and Mesa software Vulkan for native WSLg. No remote push or usage reset authorized. No broad recursive target deletion. -Latest R6 checkpoint: native recursive scenario authoring passed two distinct -shared-leaf values, paired Create/Undo/Redo and save (55objects/5links). Runtime -found unreadable long-path picker names (fixed74989b6,262 desktop tests/Clippy/build) -and single-file source3 migration discarding comments on save (desktop shortcut -now corrected; actual disk include/single-file regression passes). Final build and -native retests remain. Old native exec37598 closed normally. See the updated R6 -verification note; earlier ab661f2 suite evidence remains valid only for that freeze. +Latest R6 checkpoint: Windows264 desktop tests and strict Clippy pass for the +one-record property browser; optimized build/native retest pending. Paging and +location changes retain accepted model and typed expressions. f5467e5 Windows +and Linux263 desktop tests, strict Clippy and optimized release passed. Native +Windows source3 migration preserved comments/nesting through explicit Save All +and clean source4 reopen. Native recursive55-object scenario values and paired +Call/Reply history were verified; flat property paths prompted the latest fix. +Linux native R6 workflows and final independent browser review remain. See +release-data-verification.md. Main remains a8f451e; R6 is not yet accepted. diff --git a/harness/TASK_LEDGER.md b/harness/TASK_LEDGER.md index d7a6147e..67aa3ef3 100644 --- a/harness/TASK_LEDGER.md +++ b/harness/TASK_LEDGER.md @@ -39,9 +39,13 @@ requires native workflows, semantic evaluators and fresh independent review. whole-model equality or flat reformatting. Independent counterexamples were fixed. - R3 existing-target reconciliation remains name/description only. R6 data shape expansion is active implementation, not accepted functionality. -- R6 core/source and actual HTTP semantics pass focused Windows and full Linux - core checks; desktop257 tests pass after independent draft/path/paging fixes. - Native R6 acceptance and bounded scenario-value authoring remain outstanding. +- R6 ab661f2 full suites passed Windows607/Linux610; f5467e5 desktop263 and + strict Clippy/release passed on both platforms. Native Windows data editing, + typed defaults, allocations, recursive qualified Call/Reply values, history and + saved reopening passed. Source3 migration now preserves comments and nesting. + Native found flat property browsing still ambiguous; one-record browsing with + bounded pages now passes264 desktop tests/Clippy, pending rebuilt native checks. + Linux native R6 acceptance remains outstanding. ## Assumptions and open questions @@ -51,11 +55,11 @@ requires native workflows, semantic evaluators and fresh independent review. gestures still need their own occurrence/order semantics and task verification. - Routing offers a usable initial path plus manual pins; arbitrary dense diagrams are not guaranteed crossing-free. Measured dense full solve119ms remains a limit. -- R3 typed historical bodies plus finite proposals appear simpler than a separate - recursive semantic-value schema. An isolated probe supports scalar partial review - and deleted historical references, but ordered membership/order acknowledgements - need separate typed order checkpoints. Historical source reference parsing is - still unimplemented; this remains a candidate awaiting independent review. +- R3 selected design C uses finite self-tagged typed facets, separate membership + and order acknowledgements, and transient constructor groups. Source5 historical + references are implemented in the isolated semantic checkout. Independent review + found narrow-acknowledgement/canonical-agreement/rollup defects; the worker has + added regressions and fixes. Full catalog, GUI/API/spec and acceptance remain. ## Decisions and current strategy diff --git a/harness/release-data-verification.md b/harness/release-data-verification.md index 32f75489..3a631ac1 100644 --- a/harness/release-data-verification.md +++ b/harness/release-data-verification.md @@ -95,3 +95,25 @@ Full desktop/Clippy/build and native retest are in progress; do not accept R6 ye The old Windows native window1835476 closed normally; exec37598 exited0 with no runtime output beyond a newline. Re-inventory before opening the rebuilt binary. + +## Migration retest and bounded property browsing + +f5467e5 passed263 desktop tests, strict all-target desktop Clippy and optimized +release on Windows/Linux. Frozen Windows SHA256 +33D1B2B6709BBC734E265568C7F93CB86A506497DEF3F4531669E5ABF8B2F039. +Native fresh legacy.arcadia import preserved original bytes until Save All, +displayed8defaults/2allocations/1unresolved address, and retained comments and +nested fields in its text editor. Save All kept the same file and source4 wording; +reopen was clean15objects/3links/4checks without a migration report. Saved SHA256 +A8C0A689FEA6CC352007ACB127DE4A45E05290830DE815385CFFBAB777255543. +Evidence: ui-r6-final-windows/migration/legacy.arcadia. + +The55-object recursive scenario reopened with both qualified expressions intact. +Leaf-first labels fixed the hidden terminal names, but existing-value properties +still flattened every representative path, causing repeated indistinguishable +leaf names. The follow-up browses one record at a time, with24-choice pages and +wrapping current address/location text. Up/root/page actions change only browser +state; draft field and expression are preserved.264 desktop tests and strict +Clippy passed, including wide-record page coverage and draft preservation across +recursive browsing. Optimized build and native retest remain pending. Frozen +f5467e5 window3476204 closed normally; exec93768 had no runtime output.