Exercise bounded reference chooser with mounted input tests
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# Contextual reference chooser — bounded design
|
||||
|
||||
Codex-956atf /root/behavior_audit. Started 2026-09-10; completed design 2026-09-11. **Proposal, source inspection only.** No production edits, Cargo or native interaction. Root owns acceptance and cross-host source coordination. The revised full objective was read from the 2026-09-11 attachment; the separate requirement for complete, low-noise text-only modeling remains open work and is not satisfied by this GUI proposal.
|
||||
|
||||
## Problem and evidence
|
||||
|
||||
An engineer needs to select the correct compatible object among 1,000 same-layer objects, many with the same name. A long flat list makes containment and purpose difficult to recognize. The control should reveal context when needed without permanently enlarging every property.
|
||||
|
||||
Current source anchors below were checked before incoming scenario integration; use the named functions if lines shift.
|
||||
|
||||
| Evidence | Consequence |
|
||||
| --- | --- |
|
||||
| `forms.rs:1940`, `property`, builds an unbounded `pick_list` from `reference_choices` (`:920`). Labels generally contain name and alias, with special placement and missing/incompatible labels. | This is the verified single-reference target. Do not claim all large-list UI is covered. |
|
||||
| `forms.rs:976`, `reference_ids`, already applies ownership, viewpoint, port, cycle and other compatibility rules. | Filter its eligible identities; do not replace these rules with same-layer or same-kind heuristics. Core still admits the complete draft. |
|
||||
| `forms/view_fields.rs:82`, `members`, has query and 24-entry pages for multi-membership. | Preserve that separate control in this slice. Its checkbox/detail cards are not the proposed result layout. |
|
||||
| `forms.rs:1828` routes trace baselines to `history_fields::view` before generic references. | Historical UUID observations remain historical free text. UUID syntax alone must never enable this chooser. |
|
||||
| `forms.rs:268`, `context_value`, uses workflow context or materialized inputs, falling back to structured draft value on parse failure. | Preserve this existing context policy. A displayed candidate is not proof that all current raw inputs form a valid model. |
|
||||
| `app.rs:1729` handles `FieldShape` with whole-draft `shape`; `forms.rs:415`, `shape_scoped`, retains unrelated raw inputs. | A new reference-selection adapter should replace only its authorized scalar path, rather than parse or discard invalid siblings. |
|
||||
| `semantic_review/row.rs:885`, `form_event`, maps known forms messages; unknown messages become `Preview`. | A new shared forms event requires an explicit review adapter. Inspector-only wiring would silently fail in review forms. |
|
||||
| `views.rs:782–799` puts forms in a 320px inspector, with 28px outer horizontal padding and 14px body right padding. | Design for roughly 278px of field width. Do not widen the inspector to accommodate the chooser. |
|
||||
|
||||
## Three small alternatives
|
||||
|
||||
| Candidate | Benefit | Cost / falsifier |
|
||||
| --- | --- | --- |
|
||||
| Search input above the native pick list | Smallest code change; familiar control. | Two separate focus locations; long contextual labels remain single list entries. Reject if repeated names require trial selection or clipped text to disambiguate. |
|
||||
| Field-local disclosure with a query and four compact result rows | Uses ordinary existing widgets; easy first prototype. | Expands the scrolling form at every open location. A bottom-field trial must prove search, results and cancellation remain reachable without moving the engineer away from the field. |
|
||||
| **Anchored searchable popup** | Keeps the closed form compact; one search/focus location; contextual rows appear only during selection. | Requires real overlay focus, viewport clamping and dismissal tests. Existing context-menu code supplies patterns, not a ready-made searchable widget. |
|
||||
|
||||
**Recommended candidate for the first implementation trial: anchored popup.** The extra bounded widget work directly addresses minimum-window fit and repeated selection in large models. Revisit the inline disclosure if the popup cannot pass keyboard/focus and minimum-layout gates without substantial application-wide machinery. This is a reversible UI decision, not new model semantics.
|
||||
|
||||
## Interaction contract
|
||||
|
||||
1. The closed field shows its current name and alias, or `Choose…` / `Not assigned`. One restrained context line is available for the current kind and containment. Clicking it or pressing Enter/Space while focused opens one chooser; opening another closes the previous one.
|
||||
2. The popup contains one search box, a short compatibility/scope description, compact two-line result rows, a count and Previous/Next controls. The row shows `Name · @alias`, then `Kind · containment path`. No full card, separate button row or repeated explanation per candidate. At most six results are mounted; constrained geometry reduces this count. Long content must not increase row or popup dimensions without bound.
|
||||
3. Begin with **all compatible objects**, not an implicit nearest-container subset. The scope caption states the actual restriction, for example `SA · ports of Controller`. Query terms match name, alias, kind, containment and UUID; whitespace-separated terms are ANDed, and `@term` restricts that term to alias, following the explorer convention. Search resets pagination. Stable ordering is normalized name, alias, then identity; page/highlight are clamped after filtering. Empty results explain that the query matched no compatible objects and allow clearing the query. No speculative fuzzy ranking or permanent filter toolbar in this slice.
|
||||
4. Containment is meaningful context, not an identifier. Element rows show their owning ancestry; relation rows show kind and qualified endpoints; placement rows show their semantic object and owning diagram while retaining the **placement UUID**. Other eligible record families receive an honest family/context label. A highlighted-row detail area exposes the complete path and UUID without mounting more result cards; long detail can scroll within a fixed area. Metadata traversal must tolerate absent parents and cycles defensively. Do not use the element-only `explorer::Index` as a catalog of every identity family.
|
||||
5. The current value is separate from filtered results and stays visible even if missing, incompatible or outside the query. `Missing reference`, `Current choice needs review`, and invalid raw UUID text are retained states, not eligible replacement candidates. Merely opening, typing or closing never changes them to nil. Optional fields have an explicit `Clear reference` action; required fields have no selectable nil placeholder. Choosing a compatible object or explicitly clearing an optional reference changes the staged scalar only.
|
||||
6. Opening focuses search once for that invocation. Typing only filters. Up/Down moves the highlight; PageUp/PageDown moves a result page; Enter chooses the highlighted eligible row. Query entry never auto-selects a result. Escape closes and restores field focus; Tab closes and advances through normal form focus without committing a candidate. Clicking outside closes without invoking an underlying model action from the same gesture. A parent modal owns input over this popup. Empty/no-highlight Enter does nothing.
|
||||
7. Place the popup above or below the field, clamp it inside the app viewport, and keep its query and close/cancel affordance visible. Proposed measurement budgets: width between the field width and 420px, height at most 360px, 8px viewport margin; derive visible rows from the remaining space after controls/detail. These are design budgets, not observed fits. No inspector expansion and no new persistent toolbar. At the minimum supported 1100×700 window, top/middle/bottom field trials must show the full interaction can be completed.
|
||||
|
||||
## Minimal integration and stale-state boundary
|
||||
|
||||
Add `forms/reference_chooser.rs` for candidate metadata, normalized query, bounded result selection, state and typed events; keep the overlay implementation in `forms/reference_chooser/widget.rs` if it would obscure the pure logic. The existing `reference_choices` API has other review consumers and should remain available. Preserve `reference_ids` as the eligibility adapter. Build metadata once per opened context, filter that catalog as the query changes, and recompute eligibility before a choice is accepted. Do not rebuild every candidate widget or rescan the project for every result row.
|
||||
|
||||
Use a new event such as `FieldReferenceChooser(Event)` with separate Open, Query, Page/Highlight, Choose, Clear and Close actions. Only Choose/Clear may write a staged field. Each action carries a private transient draft-instance token, original record identity, exact JSON pointer and chooser generation; query-dependent selection also identifies the result generation. The host verifies its current document/record or review facet before forwarding. A delayed event from another document, rebuilt draft, old query or replaced array slot is inert.
|
||||
|
||||
Keep one active single-reference chooser in `Draft`, distinct from the existing multi-membership `reference_browsers`. A newly created/recovered draft gets a fresh instance token; recovery serializes neither catalog nor query, focus, highlight or popup token. Rebuilding, array insertion/removal/reordering, changing a variant or authored dependency, rebinding workflow context, and switching/reloading the document close/invalidate the chooser. Do not transfer a chooser by numeric array index. Draft copies installed as a different editing session must receive a fresh token. If a host retains a draft through a model revision, it must explicitly invalidate the chooser at that context refresh; merely checking that the selected UUID still exists is insufficient.
|
||||
|
||||
After those checks, revalidate the live-reference path, optionality and selected identity against the latest context, then use a tightly scoped setter around `shape_scoped`. Reject forged non-reference or historical paths. Do not call `App::apply`, synthesize a model command, navigate to the selected object or save on open/search/focus/selection. Existing explicit Apply commits the property change. Preserve inactive constructor drafts and unrelated invalid raw input exactly. Keep ordinary pending source/behavior guards for Choose/Clear; browsing and cancellation must not bypass any parent modal or trigger automatic draft application.
|
||||
|
||||
| Production boundary for a later lease | Bounded change |
|
||||
| --- | --- |
|
||||
| New `forms/reference_chooser.rs`, optional `widget.rs`, focused tests | Shared metadata/search/state/widget; no core/source/API schema changes. First independent implementation partition. |
|
||||
| `forms.rs:44–86`, Draft state; `:123–216`, recovery/construction/rebuild; `:268`, context rebinding; `:415–550`, scoped shape/array paths | Add transient chooser lifetime and invalidate it at actual context/path changes. Reuse raw-preserving setters. Leave historical field rendering and recovery payload schema alone. |
|
||||
| `forms.rs:920–976`, choice/eligibility boundary; `:1940–1984`, generic reference branch | Delegate just live single-reference rendering; preserve eligibility and current-value semantics. Do not convert history UUID strings. |
|
||||
| `app.rs:372`, Message; `:1142`, pending guard; `:1729–1770`, forms handlers; `:676`, `refresh_draft` | Route typed events, separate browse from staged writes, verify active context, and perform scoped focus commands. No `starts_form`/implicit Apply path. |
|
||||
| `semantic_review.rs:74`, RowEvent; `semantic_review/row.rs:292`, browse; `:521`, edit; `:784`, refresh; `:885`, form_event | Explicit event mapping; require selected candidate facet and its allowed roots, select the proper primary/group/attachment draft, verify its token. Open/query/close do not replan, clear accepted impacts or change the proposal. Choose/Clear use the same scoped edit authorization as existing shapes. Context refresh invalidates old chooser events. |
|
||||
| `semantic_review/view.rs:748,777` and `attachments_view.rs:74`; `views.rs:782` | Confirm the same widget works through both host adapters. Touch only event/context/focus plumbing if necessary; preserve review layout and inspector width. |
|
||||
| `explorer.rs:96,180,205`; `context_menu.rs` Popup/Geometry/Area | Read/reuse query and cycle-safe ancestry conventions; follow token, clamping and swallowed-dismissal patterns. Do not add search input to the text-action menu by assumption or refactor explorer/menu as a prerequisite. |
|
||||
|
||||
Root can first lease only the new chooser module/tests for the catalog, query and mounted widget trial. Keep it unintegrated until shared forms/App/review boundaries are coordinated. The next coherent slice must include **both** inspector and semantic-review routing; it is not complete when an unused widget or inspector-only control compiles. Existing multi-member browsing, behavior-specific pickers and other direct `Choice` consumers remain separately inventoried work.
|
||||
|
||||
## Evaluator before optimization
|
||||
|
||||
Use an independently specified fixture with 1,000 eligible SA records under repeated-name containers, duplicated leaf names, distinctive aliases, long Unicode names and paths; include OA and wrong-kind distractors. Add a placement field whose semantic and placement IDs differ, a missing current ID, an incompatible current ID and an invalid recovered raw UUID. Run equivalent smaller OA cases. Expected identities and containment are literal fixture facts, not values adopted from chooser output.
|
||||
|
||||
| Trial | Required observation |
|
||||
| --- | --- |
|
||||
| Repeated-name selection | Name plus containment query and exact `@alias`/UUID query each find the intended identity without scanning hundreds of rows. All compatible objects remain reachable; distractors remain excluded. Result widget count stays bounded regardless of model size. |
|
||||
| Staged/raw preservation | Before browsing, put invalid numeric raw text in another field. Open/type/page/cancel preserve Draft recovery bytes and exact Project/SourceBundle. Choose updates only the intended staged reference and retains that invalid raw text and inactive constructors; the model/source/history remain unchanged until explicit Apply. |
|
||||
| Stale events | Capture actual widget events, then reorder/remove an array row, switch its variant, switch review facet, change context/model, replace the document retaining the same IDs, or recover a new draft. Deliver the old selection. No other field or proposal changes. Search-change and close/reopen also invalidate old-result selection. |
|
||||
| Historical separation | Open trace baseline UUID-valued observations, including nested typed values. They retain historical text editing and raw recovery. No live chooser, query or eligibility filter appears. Live trace endpoints still use compatible live references. |
|
||||
| Host parity | Repeat selection through the inspector and an authorized semantic-review candidate facet, including a group/attachment draft. Browse does not replan or clear accepted impacts. Pending source/behavior/modal guards block unauthorized writes. |
|
||||
| Actual keyboard/geometry | Mount the widget and deliver real input events, then repeat natively at 1100×700 and a normal larger window. Top/middle/bottom anchors, resize, long detail, empty results, arrows/pages/Enter/Escape/Tab and outside dismissal all work; no clipped essential control, hidden focus, accidental Apply or click-through. A constructed view or token assertion alone is not evidence of this. |
|
||||
| Model/source equivalence | After repairing the unrelated raw error, Apply the intended reference once. Compare the full expected typed Project and emitted source against an independently constructed text-authored equivalent. Undo equals the exact pre-Apply snapshot; Redo and reopen equal the exact accepted snapshot, including identities. Search/query/context labels never appear in source or REST payloads. |
|
||||
| Large-list responsiveness | On the measured build/host, record cold Open and warm query-to-visible-result times for the 1,000-record fixture, including a relation endpoint with no opposite endpoint yet. Provisional target: warm query p95 below 100ms and cold Open below 250ms. Time eligibility separately: some existing endpoint paths may enumerate candidate pairs. If it dominates, fix/cache the measured context computation with correct invalidation rather than merely hiding lag behind pagination. |
|
||||
|
||||
Acceptance needs source tests **and** actual keyboard/minimum-window evidence. No runtime result is claimed by this report. The first experiment should falsify popup geometry/focus and staged-raw safety before migrating further reference controls. Text-only development, notation clarity and complete UI/text/REST parity remain release-level evaluators beyond this chooser slice.
|
||||
|
||||
## Isolated experiment handoff — 2026-09-11
|
||||
|
||||
Root authorized a reversible implementation trial after the design handoff (Gitea #106/#110). The new `crates/oa-desktop/src/forms/reference_chooser.rs`, `reference_chooser/widget.rs` and `reference_chooser/tests.rs` are written and deliberately **undeclared** at this checkpoint. Existing forms/App/review source has not been changed by this worker. Three new-file whitespace checks passed. Root holds Cargo and native input; no compile or runtime pass is claimed here.
|
||||
|
||||
Implemented boundaries:
|
||||
|
||||
- `Context::live` identifies document, revision, editing-session nonce, record, exact pointer and optionality. It rejects baseline paths; it deliberately does not discover schema or reproduce eligibility. `State::open` takes the already eligible catalog, retains current invalid/missing/incompatible values separately, rejects nil/duplicate candidate IDs, and sorts/searches deterministic name/alias/kind/scope/containment/UUID metadata. Placement identity is explicit in the candidate contract.
|
||||
- `State::update` returns `Browsed`, `Closed`, `Ignored` or a `Write { context, value }` intent. It does not own a property Draft, accepted Project or command history. Only Choose/Clear are write intents. Every event has invocation and context identity; choices are tied to the displayed result generation. Query edits carry a monotonic input serial so multiple edits emitted before the application processes the previous query are preserved, while older serials are rejected.
|
||||
- `widget::Popup` wraps arbitrary host content and maps typed events into the host's message type. It uses an actual Iced text input, a retained overlay tree, viewport-clamped measured geometry and at most six compact rows. Query focus occurs once per invocation. Arrow/page/Enter/Escape/Tab, explicit Clear, pointer press/release identity, horizontal detail scrolling and copying complete detail via Ctrl+Shift+C are implemented. Close returns a reason so the host can restore or advance focus without applying a draft. Parent modal ownership and closed-field keyboard entry are host integration work.
|
||||
- Twelve tests are prepared. They exercise actual mounted widget layout and input with Iced's null renderer, including same-batch typing and query-to-Escape, ordered relative navigation, cursor position after controlled rebuild, repeated-name filtering, 1,000-entry pagination, placement identity, mouse/keyboard selection, stale-result rejection/unlocking, document/draft/path isolation, empty results, retained values, optional Clear and swallowed outside-dismissal release. The null renderer exercises widget behavior and geometry; it cannot establish real font layout, accessibility or native usability.
|
||||
|
||||
Source review during construction found and corrected two concrete mistakes before the first compile: Page Down initially advanced the visible window by one row; a stale Choose rejected after a query change could leave the widget marked closing. The prepared regressions require a full page advance and successful subsequent selection after rejection. These are source corrections awaiting execution, not measured test results.
|
||||
|
||||
A further same-batch counterexample was corrected under a narrow source lease: Query followed immediately by Escape previously invalidated the close event's result stamp. Dismissal, relative navigation and explicit optional Clear now require the current session/context but do not depend on the result generation. Absolute Highlight/Choose remain result-scoped. The mounted regression emits Query→Escape and PageDown→PageDown→Up before handling any message, then checks ordered outcomes and rejection of an older absolute row action.
|
||||
|
||||
The first focused compile can use **only** `#[cfg(test)] mod reference_chooser;` in `forms.rs` and filter `forms::reference_chooser::tests::`. Root must format/compile the candidate after the current declared-source freeze and send diagnostics back. Remove the temporary declaration or retain it as test-only until the following coherent host slice is ready.
|
||||
|
||||
### Exact next integration recipe
|
||||
|
||||
1. Add a host-level Open trigger containing editing-session identity, exact pointer and measured absolute field bounds. Build the catalog through existing `reference_ids`/metadata adapters and call `State::open`; do not create model objects or call `apply`. The popup module intentionally starts at this explicit opening boundary, rather than adding a second schema/path classifier. A closed-field trigger must support keyboard focus/open, and provide a stable focus target for return.
|
||||
2. Store the transient chooser and anchor with the correct property/review editing session. Route its events through both inspector `Message` and semantic `RowEvent` adapters. Revalidate the current document/revision, selected facet and allowed property roots before handling writes; recheck actual eligibility and optionality immediately before `shape_scoped`. A rejected pending-draft write must cancel/invalidate the popup safely, rather than leaving a widget awaiting an acknowledgement forever. Query/page/close must not refresh semantic proposals or clear accepted impacts.
|
||||
3. **Mount the Popup wrapper at a stable root**, not around each field: `App::view` currently wraps `views::view` in `context_menu::Popup` at approximately `app.rs:1009`, then hosted overlays. Retaining the chooser wrapper at that level lets it swallow a dismissal release before any underlying pane receives it. Select the active inspector or semantic-review chooser explicitly; route the root callback to that same host. The semantic-review modal is the owning editing surface and may open its own chooser. A different modal, hosted dialog or model menu invalidates/suppresses the chooser. Preserve wrapper tree identity while closed and verify overlay priority in the complete app.
|
||||
4. On `Closed(Escape)` restore the original field focus once; on `Closed(Tab { backwards })` advance in the host form's actual focus order once. Outside/resize/window-unfocus dismissal must not issue model/navigation commands. Wire context invalidation at Draft rebuild, variant/array change, workflow context refresh, review facet change, source/model apply, reload and recovery. Recovery continues to omit chooser state; a cloned Draft installed as a new editing session needs a fresh nonce.
|
||||
5. Run the report's full raw-draft/Apply/source/history and host-parity evaluator after wiring. The current isolated tests cannot establish those properties because the host adapters intentionally do not exist yet. Do not ship the new generic branch to the inspector while semantic-review routing remains a fallback to Preview.
|
||||
|
||||
Remaining native gates: measure actual glyph clipping/detail discoverability at 1100×700 and larger sizes; closed-field keyboard opening and return/Tab focus; modal priority; parent scrolling/resize; complete mouse/touch cancellation; disabled/required/optional states; query latency including expensive endpoint eligibility. The provisional 100ms/250ms targets have not been measured. Complete Project/SourceBundle/Undo/Redo, invalid sibling recovery and text-authored equivalence remain integration tests, not claimed outcomes of this isolated popup.
|
||||
|
||||
### First executed gate and input-fixture correction
|
||||
|
||||
Root added a test-only module declaration and ran the first focused gate under its sole Cargo lease. `.local/logs/reference-chooser-first-20260911.log` records a warning-free compile in 3m08s, followed by **9/12 passing and three failing tests** in 0.19s. This executes the prototype only in the test binary; it remains unshipped. The worker did not run Cargo or native input.
|
||||
|
||||
The three failures were traced to a concrete incorrect event fixture. The helper put `@controller_0731`, `no match exists`, or `@controller_0720` into one physical `KeyPressed.text`. The installed primary source, `iced_widget-0.14.2/src/text_input.rs` around lines 1020–1035, intentionally inserts only `text.chars().next()` for that event. Clipboard paste (around 950–990) and IME Commit (around 1285–1310) insert complete strings. Consequently the actual queries were `@`, `n`, and `@`; the first matched all aliases and `n` matched `Function`, explaining both the count and unexpected Enter intent. The popup forwarded Iced input correctly. This diagnosis does not justify changing production keyboard handling.
|
||||
|
||||
The narrow repair changes only the new test file: `typed` now accepts a single `char`, and whole-string typing emits individual key events. The three tests retain their full literal query, expected candidate identity, empty-result and close expectations; they additionally verify the actual query/count before selection. Query→Escape still runs the entire physical-key sequence and Escape in one runtime batch before handling messages. One additional mounted test separately exercises real clipboard paste with ModifiersChanged/KeyV, and multi-character Unicode IME preedit/commit. Preedit must publish no query; commit/paste must each publish one complete query and leave the retained current value and model-write boundary unchanged.
|
||||
|
||||
The revised candidate has **13 tests prepared** at the worker handoff. No production-widget repair was required for these three failures. Whitespace check is clean; full native/host integration gates above remain open.
|
||||
|
||||
### Second executed gate — root integration checkpoint
|
||||
|
||||
Root ran the corrected candidate through `cargo test --locked -p oa-desktop forms::reference_chooser::tests::`. The log `.local/logs/reference-chooser-second-20260911.log` records a warning-free compile in 1m47s and **13/13 passing**, zero failures/ignored, 642 filtered out, in 0.41s. This includes actual mounted clipboard paste and Unicode IME commit alongside the corrected physical-key fixtures. The module is declared under `#[cfg(test)]` only. This result establishes the isolated widget/state tests; the complete inspector/review adapters, native font/focus/geometry, responsiveness and model/source/history acceptance remain open.
|
||||
Reference in New Issue
Block a user