Merge PR #238 (a6b5acc2) into the 2026-09-24 integration wave

This commit is contained in:
Edward Langley
2026-09-24 02:44:40 -07:00
5 changed files with 106 additions and 14 deletions
+31 -10
View File
@@ -312,16 +312,37 @@ carries it or has to agree with it. The TUI colouring the ticket hoped for is
still possible later, and would want the severity *on the message*; that is a
separate change with a separate staleness problem.
**The classification is per writer, and it is not total.** Every refusal in
`ui/effect.rs` and in `cmd/{cell,commit,fit,grid,mode,search,text_buffer}.rs`
goes through `refuse`. `EditOrDrill`'s collapsed-group arm and the three
spelled-paste declines in `cell.rs` (aggregate, coordinate column, inside a
drill) joined them with improvise-i3o5. Those still written with `set_status`
exit 0 as before, and are listed on that bead: three in `cmd/panel.rs`, and
`command/keymap.rs::dispatch`'s arg-error arm. The sweeps that pin refusals
revert `refuse(` calls, so they **cannot see a refusal written as
`set_status`**. Finding one takes a read of every `set_status` on a decline
path, and that is how these were found. `q` on a dirty document is deliberately left a
**The classification is per writer.** Every refusal in `ui/effect.rs`, in
`cmd/{cell,commit,fit,grid,mode,panel,search,text_buffer}.rs` and in
`command/keymap.rs::dispatch` goes through `refuse`. The last of them were
converted in two steps:
- **improvise-i3o5:** `EditOrDrill`'s collapsed-group arm, and the three
spelled-paste declines in `cell.rs` (aggregate, coordinate column, inside a
drill).
- **improvise-cwfj:** the three `cmd/panel.rs` declines, and `dispatch`'s
arg-error arm.
The sweeps that pin refusals revert `refuse(` calls, so they **cannot see a
refusal written as `set_status`**. Finding one takes a read of every
`set_status` on a decline path, and that is how both batches were found.
**A bound key whose command will not build is a refusal** (the orchestrator's
ruling, improvise-cwfj). The key asked for something that did not happen,
which is this section's definition. So `dispatch` answers a constructor's
`Err` with `refuse(e)` rather than `set_status(e)`. That arm is where every
key-path decline lands, including `p`, `x` and `i` onto an aggregate, a fold,
a coordinate column or inside a drill.
Before the conversion, the key path was checked for any reader of the flag:
- `last_batch_refused` is read in production only by `Host::mutating_frame`,
which handles frames and not keys, and by `main.rs`'s headless loop, which
handles `:` lines and not keys.
- `Refuse` is `ViewOnly`, like `SetStatus`, so the `Dirty` a key returns and
the projections built from it are unchanged.
- The browser lockstep suite compares `ViewState`, including `status_msg`,
never the flag, and `Refuse` writes the same message.
The whole workspace stayed green with the change. `q` on a dirty document is deliberately left a
report: headless, its text is false — the CLI saves anyway (improvise-ep5o).
The two rivals udt2 named both stay green: a successful `:w` and a successful
`fit-measure` report through `set_status` and exit 0.
+22
View File
@@ -6397,6 +6397,28 @@ runs `cargo test --workspace --no-fail-fast`. Four of four fail this test, and
nothing else fails. None of the four needs an abort: each is the only effect its
command emits, and `p`/`i` are single bindings, not sequences.
#### improvise-cwfj: the category panel's declines, and a bound key that will not build
Four more cases in `every_refusal_site_refuses_its_failure_and_only_its_failure`.
The first three are `cycle-axis-at-cursor`, `open-item-add-at-cursor` and
`delete-category-at-cursor`, each run with the panel cursor past the tree's last
row (99). Each twin runs the same command at row 1, deliberately not row 0.
The fourth is `p` pressed through `App::handle_key` on an aggregate, so the
paste constructor's `Err` reaches `command/keymap.rs::dispatch`. Its twin is `p`
onto a stored record at (1, 1). The expected text is `AGGREGATE_REFUSAL`, which
`dispatch` prefixes with the command name ("paste: …").
**Red at `bb5dfe5`**, one site at a time: each case failed with its own text and
`refused=false` until its own site was converted.
**Per-site revert:** `/tmp/refuse-fu/sweep.py`, with `command/keymap.rs` added to
its file list, reverts each of the four `refuse(` calls alone and runs
`cargo test --workspace --no-fail-fast`. Four of four fail this test, and
nothing else fails. The full suite stayed green with `dispatch` converted, which
is the empirical half of the check that nothing relied on a failed key leaving
`last_batch_refused` false.
### a_formula_rows_error_does_not_poison_the_pooled_total
improvise-b5dp, the user's ruling on the conflict between wm20's rulings (1) and
@@ -722,7 +722,7 @@ impl Cmd for CycleAxisAtCursor {
.extend(cycled.map(|axis| effect::set_status(axis_change_status(&cat_name, axis))));
effects
} else {
vec![effect::set_status(
vec![effect::refuse(
"Move cursor to a category header to change axis".to_string(),
)]
}
@@ -740,7 +740,7 @@ impl Cmd for OpenItemAddAtCursor {
if let Some(cat_name) = ctx.cat_at_cursor() {
vec![effect::change_mode(AppMode::item_add(cat_name))]
} else {
vec![effect::set_status(
vec![effect::refuse(
"No category selected. Press n to add a category first.",
)]
}
@@ -832,7 +832,7 @@ impl Cmd for DeleteCategoryAtCursor {
effects.extend(retreat_from_deleted_item(ctx));
effects
}
None => vec![effect::set_status("No category to delete")],
None => vec![effect::refuse("No category to delete")],
}
}
}
@@ -61,7 +61,7 @@ fn effects_of(
Ok(acc)
}),
};
Some(built.unwrap_or_else(|e| vec![crate::ui::effect::set_status(e)]))
Some(built.unwrap_or_else(|e| vec![crate::ui::effect::refuse(e)]))
}
/// The shared keymap table plus the registry that gives its names meaning.
+49
View File
@@ -3300,6 +3300,18 @@ mod tests {
app
}
/// One key through the keymap, as the TUI delivers it; true when it refused.
fn press_key(app: &mut App, code: KeyCode) -> bool {
app.handle_key(KeyEvent::new(code, KeyModifiers::NONE))
.unwrap();
app.last_batch_refused()
}
fn panel_cursor_at(mut app: App, at: usize) -> App {
app.view_state.cat_panel_cursor = at;
app
}
/// Storage holding one loadable document, `doc.improv`, and nothing else.
fn with_a_document_to_load() -> App {
let storage = crate::storage::MemoryFs::new();
@@ -3609,6 +3621,43 @@ mod tests {
|| yanked(records_model_with_two_rows()),
|app| run_line(app, "paste _Index/1 _Dim/2"),
),
// improvise-cwfj: the category panel's three "nothing at the cursor"
// declines, and a bound key whose command will not build.
(
"cycle-axis-at-cursor past the panel's last row",
|| panel_cursor_at(two_col_model(), 99),
|app| run_line(app, "cycle-axis-at-cursor"),
"Move cursor to a category header",
|| panel_cursor_at(two_col_model(), 1),
|app| run_line(app, "cycle-axis-at-cursor"),
),
(
"open-item-add-at-cursor past the panel's last row",
|| panel_cursor_at(two_col_model(), 99),
|app| run_line(app, "open-item-add-at-cursor"),
"No category selected",
|| panel_cursor_at(two_col_model(), 1),
|app| run_line(app, "open-item-add-at-cursor"),
),
(
"a bound key whose command will not build (p onto an aggregate)",
|| yanked(aggregated_grid(true)),
|app| press_key(app, KeyCode::Char('p')),
crate::command::cmd::core::AGGREGATE_REFUSAL,
|| yanked(two_col_model()),
|app| {
app.cursor_mut().selected = (1, 1);
press_key(app, KeyCode::Char('p'))
},
),
(
"delete-category-at-cursor past the panel's last row",
|| panel_cursor_at(two_col_model(), 99),
|app| run_line(app, "delete-category-at-cursor"),
"No category to delete",
|| panel_cursor_at(two_col_model(), 1),
|app| run_line(app, "delete-category-at-cursor"),
),
];
for (what, fail_setup, fails, says, ok_setup, succeeds) in cases {
let mut app = fail_setup();