Commit Graph
946 Commits
Author SHA1 Message Date
Marco Arnulfo 0c14f59555 PivotTables: Show Details, double-click a value cell to list its source records on a new sheet (#2364) 2026-10-23 20:21:27 +02:00
aiongg a7608ada6d CI: one test binary per crate, nextest, line tables only (#2372)
* CI: dev builds keep line tables only

* CI: one integration-test binary per crate

* CI: xtask runs tests with nextest, doctests apart, --keep-going on clippy and wasm

cargo xtask test runs the workspace tests (--lib --bins --tests) with cargo-nextest when it is installed, else cargo test, then the doctests. cargo xtask ci uses the same two steps.

* CI: docs name cargo xtask test

* CI: agent guides name the moved fuzz test

* CI: tests added on main join their crate's test binary
2026-10-23 12:59:46 -04:00
MarekSpodBiedry 599762f534 AutoFilter: grow the range when a value is entered in the row below it (#2342) 2026-10-23 17:36:39 +02:00
Brandon ThomasandClaude Opus 5.5 f1e29fa828 Review fixes for #2267 and #2292
- range.setValues with parse: false still turns on Wrap Text for a line break (CodeRabbit).
- Japanese: the fill-handle hint says AutoFill (オートフィル), not paint (CodeRabbit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:15:31 -04:00
Brandon Thomas bcf55f0f00 Merge PR #2274
# Conflicts:
#	docs/ui-parity.md
2026-10-23 10:11:33 -04:00
Brandon Thomas e92d8b4e48 Merge PR #2295 2026-10-23 10:11:12 -04:00
Brandon Thomas 50e5a3e928 Merge PR #2267 2026-10-23 10:11:11 -04:00
D. Fender 09f78cb3a8 Tables: Format as Table inside a table restyles it
Home > Format as Table on a cell or range inside an existing table
failed with "A table can't overlap another table." Excel applies the
picked style to that table instead. The command now returns that table
and its range, as it does for a new table. A range crossing a table's
edge is still rejected.

Without a range, the active area of the selection decides, as it does
for Insert > Table. The native Format menu's Format as Table now runs
home.formatAsTable instead of insert.table.
2026-10-22 11:43:58 -07:00
aiongg 2ed3df2bd6 G4: two windows on one workbook without copying it; Arrange All
- The workbook's active sheet is an atomic set in place (Workbook::active_sheet /
  set_active_sheet), so a window swap or sheet switch never clones the
  Arc<Workbook> or marks it dirty. Selections already live in DocState and the
  window views. Save records the sheet of the most recently active window.
- A dialog taken out of its window while it runs goes back to that window when
  a command run from it switched windows (dialogs::show, ui.dialog.confirm).
- view.arrangeAll {arrange?: tiled|horizontal|vertical|cascade, activeWorkbook?}
  returns the visible windows, active first; the desktop moves and sizes the
  native windows over the main window's monitor. New Window and Arrange All
  buttons on the View ribbon; Arrange Windows dialog.
- Docs: ui-parity, hardware-parity, gaps #20, target-app-parity, ROADMAP.
2026-10-22 16:50:10 +00:00
Higgs32584andClaude Opus 5.5 b5b3969377 scripts: trust gate review fixes (#2178)
- automate.trustScripts: only a missing `trusted` defaults to true; a
  non-boolean value is an error and leaves the workbook untrusted.
- The refusal message points at Trust Scripts in View > Macros (Alt+F8)
  and at automate.trustScripts.
- Catalog row for automate.trustScripts; parity checklist regenerated.
- Script steps run inside an io::ScriptSandbox (thread-local) so
  read_file/write_file refuse whatever the parameter is called, and a
  step that raises OpenUrl is dropped and stops the script. The id/param
  deny list stays as the up-front refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 12:43:48 -04:00
Higgs32584 6dedb9310b Merge remote-tracking branch 'origin/main' into fix/2178-script-trust-gate 2026-10-22 12:32:28 -04:00
aiongg 5bd5d241a0 G3: a native window per workbook window
Each engine window is drawn in its own native window through craft-window's
immediate-viewport driver (desktop only; the web app keeps one window). The
root viewport shows one window and adopts another when its window closes.

- WindowUi: the cell draft, text-box draft, dialog, Backstage, message,
  toast, notice, grid state, name box, key tips, selected chart, Tab run and
  edit mode are per window. SheetApp's fields hold the active window's; the
  others are parked and swapped in when a window is drawn or activated, and
  after any command that switched windows.
- Session::show_window makes a window current for drawing without counting
  as an activation.
- The ribbon in each window acts on it; menu bar commands and scripts go to
  the window focused last. Leaving a window enters its draft as Enter would.
- A window's close button runs window.close: one of several windows on a
  workbook closes without asking, a workbook's last asks when unsaved.
  SheetApp::request_quit (macOS Quit item, Dock quit) asks about each unsaved
  workbook in its window; Cancel stops. Save Changes answers moved to
  SheetApp::answer_save_changes.
- The last window closed quits on Windows and Linux; macOS hides the root
  and keeps running until New or Open.
- Fonts are bound for every open workbook, since each may be showing.

craft-window is pinned to the aiongg/craft-libs window-list branch until it
merges into storytold/craft-libs.
2026-10-22 16:21:59 +00:00
aiongg 45d1d1acc0 G2: windows on workbooks in the engine; New Window shares the workbook
Session keeps a window list (stable ids, per-window selection, active sheet
and per-sheet selections swapped into the workbook without dirtying it)
in place of the index-based hidden list and the copying New Window.
Titles read Name:1 / Name:2 for a workbook with several windows.
File > Close closes one window of several; window.close, window.list and
window ids for window.activate, view.switchWindows, view.hideWindow and
view.unhideWindow. Control channel and MCP tools take window/doc ids;
scroll positions and cell drafts belong to their window.
2026-10-22 14:50:53 +00:00
Dominik StrebelandClaude Opus 5.5 840ba5c3a5 view: Show Outline Symbols (Ctrl+8), read and written
A sheet can hide its outline's level buttons and brackets while keeping
the outline (Excel's Ctrl+8, saved as showOutlineSymbols="0"). It wasn't
modelled, so such sheets showed a wide outline band Excel doesn't.

- model: Sheet::show_outline_symbols (default on, also for older JSON).
- xlsx: read and written on the sheet view.
- engine: view.showOutlineSymbols {on?} (Ctrl+8), undoable.
- ui: no outline bands while it's off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:54:06 +02:00
Hamza 99f92f4a28 range.setValues: parse: false stores a string as the text it is (#2255)
Every string in `values` went through typed input, which reads it with en-US
rules, and nothing said so:

    "05.04.2026"  (dd.mm.yyyy)  → the date 4 May 2026
    "02/11/2026"  (dd/mm/yyyy)  → the date 11 February 2026
    "2.500"       (thousands)   → the number 2.5
    "4.25 %"      (text)        → 0.0425, formatted 0.00%

A workbook built from Turkish-format exports therefore held different values
than the script put in it, and the only way to notice was to read the saved
file back cell by cell.

`parse: false` stores every string as the text it is. A formula is still a
formula — `=` means a formula in a cell, and the ticket asks for that — and
numbers, booleans and null are unaffected either way. A leading apostrophe is
part of the text rather than the marker that makes it text, since nothing is
being marked.

The default is unchanged: `parse` is absent in every existing call, and those
calls behave exactly as before. A `parse` that is neither true nor false is a
usage error rather than a quiet default.

The test pins both sides — the swapped date, the decimal point and the
percentage under today's default; each of the six strings stored verbatim with
`parse: false`, with `=1+1` still calculating 2; numbers and booleans either
way; and the refused `parse` values. Without the change the first string comes
back as a date.
2026-10-22 18:05:53 +05:00
prxnvt 67fa9f7b4e Merge main into #2152
# Conflicts:
#	ROADMAP.md
#	docs/architecture.md
#	docs/target-app-parity.md
#	docs/ui-parity.md
2026-10-22 02:39:09 -04:00
prxnvt bca1483e2e Merge main into #2145 2026-10-22 02:37:04 -04:00
prxnvt 38ad2dba83 Merge main into #2144
# Conflicts:
#	crates/engine/src/tests.rs
2026-10-22 02:37:04 -04:00
prxnvt 3eed81318f Merge main into #2151 2026-10-22 02:36:09 -04:00
prxnvt faff24f3aa Merge main into #465
# Conflicts:
#	crates/engine/src/io.rs
2026-10-22 02:36:09 -04:00
prxnvt 66f7993ba5 Merge main into #462
# Conflicts:
#	crates/engine/src/lib.rs
#	crates/engine/src/tests.rs
#	docs/architecture.md
2026-10-22 02:30:25 -04:00
prxnvt bbf1bc441b Engine: stopping a recording waits while a macro in its workbook is being debugged
Stopping a recording saved the macro with a nested macro.save while a debugged run's undo group could be open on the same workbook. Stopping the run without keeping its changes then deleted the macro that macro.record had reported as saved. Stopping now fails with "Finish or stop the macro first" under macro.record before anything changes, and the recording goes on, as when a save fails for another reason, so stopping again after the run ends saves it.
2026-10-22 02:26:33 -04:00
prxnvt ea691f703c Engine: a stopped recording reports the steps cut at the macro step limit in dropped
A recording keeps up to 100,000 commands, and inlined macro and script runs can add more, but a macro holds at most 10,000 steps. Stopping truncated the converted steps to that limit without saying so: dropped counted only the recorder's own overflow, so a 10,005-step recording saved a 10,000-step macro and reported nothing missing. The steps cut at the macro limit are now added to dropped.
2026-10-22 02:26:33 -04:00
prxnvt 3f5f6ea4f1 Engine: a name given when a recording stops never replaces an existing macro
Stopping a recording with a name that a macro already has (in any case) saved over that macro's
steps, inputs and description, with only Undo to get them back. A taken name now gets a suffix
("Tidy (2)"), and the save passes replace: false as a backstop.
2026-10-22 02:26:33 -04:00
prxnvt 6a004bf610 Engine: stopping Record Actions waits while a macro in its workbook is being debugged
Stopping Record Actions saves a script into the workbook, which the debugger lock refuses while a
macro there is paused. The refusal came after Record Actions had taken its recording, so the
recording was lost. Stopping is now refused before anything changes, and stopping again after
the run saves the script.
2026-10-22 02:26:32 -04:00
prxnvt 045cf428f5 Engine: a column header made by a formula matches its token while formulas are shown
`col("Amount")` read the header through the cell's display text, which is the formula while
Show Formulas is on, so a header such as ="Amo"&"unt" matched nothing. The header is now the
cell's value as formatted, whatever the sheet shows.
2026-10-22 02:26:32 -04:00
prxnvt 6312c7cb40 Engine: only hidden names are read as macro storage
A visible workbook-scope name such as `_gc_macro_0_0` matched the storage key and was read as a
macro and removed when the workbook opened. Macro storage is written hidden, so only hidden names
are read back; a person's own name of that shape stays a name.
2026-10-22 02:26:32 -04:00
prxnvt 8797b7183c Engine: stopping a macro recording saves it in the workbook where recording started
Stopping a recording converted and saved it against the active workbook. For example, starting a
recording in Book1 and switching to Book2 before stopping saved the macro into Book2, inferred the
input's header and column types from Book2's cells and looked up inlined macros and scripts
there. The recording is now converted and saved against the workbook where it started, and the
active workbook stays active. If that workbook was closed, stopping returns an error and ends the
recording.
2026-10-22 02:26:32 -04:00
prxnvt f553619b33 Engine: the macro debugger lock also refuses edits that another command runs
Session::execute checked the debugger lock only for top-level undoable commands. A command that is not undoable could run edits as nested commands and skip it: stopping Record Actions while a macro in the same workbook was being debugged saved the script inside the run's undo group, and stopping the run without keeping its changes deleted the script again. The lock now applies at every depth, and only the debugged run's own steps go ahead.
2026-10-22 02:26:32 -04:00
prxnvt 0ea5d3ee33 Engine: a Name: token reaches a defined name that holds periods
`Name:` read a name only up to the first period, so `Name:Sales.Q1` looked up `Sales` and
treated `Q1` as a method, and a workbook name called Sales.Q1 could not be used in a macro,
alone or as `Name:Sales.Q1.cell(1,1)`.

The name now runs through periods, and the longest defined name or table that exists and ends
at a period (or the end) is used; what follows is read as methods. So `Name:Data.row` is a
name called Data.row, and `Name:Sales.offset(1,0)` still moves Sales. When no name matches,
the error names the text before the first method, and a bad method after a dotted name is
still refused. Tests cover direct resolution, reference parameters, `$ref` and `{{...}}`,
with and without a chained method.
2026-10-22 02:26:32 -04:00
prxnvt 3c74dfcbb0 Engine: renaming macros with clashing names on open takes near-linear work
Opening a workbook gave each clashing macro name a suffix by trying " (2)", " (3)"... in turn,
and each try scanned every macro accepted so far. A file with 1,000 macros of the same
255-character name made about 300 million name comparisons.

Load now keeps a set of the names in use (ASCII case-insensitive, as find compares them) and,
for each shortened name and suffix width, the next suffix to try, so each suffix is looked up
about once. The result is unchanged: the smallest free suffix, within the name length limit,
and the renamed macro still passes the final checks. unique_name keeps its signature. A test
opens, saves and reopens 1,000 same-name macros in JSON and xlsx and keeps them all, and
another counts lookups instead of timing them.
2026-10-22 02:26:32 -04:00
prxnvt efce63f75f Engine: macro recorder applies the macro policy to the steps of a recorded script run
An Automate script run while recording was replaced by all of its steps except automate.* ones,
without the policy check that top-level commands get. For example, a script that runs
window.activate or macro.list produced a recorded macro that the runner refused on playback. The
steps of a recorded script run, and of an inlined macro run, now go through the same policy as
top-level commands; refused ones are left out and listed as skipped.
2026-10-22 02:26:32 -04:00
prxnvt 06bbcb504a Engine: macro approvals are keyed on the macro's canonical JSON, not a 64-bit hash
Approvals were stored as (document, FNV-1a hash of the macro's JSON).
FNV-1a is not collision resistant, so a crafted workbook could hold a
harmless macro and a different one with the same hash, and approving
the first would let the second run without asking.

The key is now the macro's canonical JSON text itself (object keys
sorted), so two different macros never share an approval.
macros::hash is replaced by macros::fingerprint.
2026-10-22 02:26:32 -04:00
prxnvt 77150be455 Engine: col("Header") is refused on a range without a header row
col("...") always matched against the first row of the range, whatever
its header flag. On an input declared without a header row,
Input.col("Amount") silently picked the first column whose first data
cell read "Amount". After .data, the natural Input.data.col("Amount")
failed, because the first row was then a data row. The header row is
now carried through data, and col("...") is refused with a message when
there is none (an input without a header, a Name: base, or after any
other method).
2026-10-22 02:26:32 -04:00
prxnvt 7d31c5a4ed Engine: a macro in a newer format is dropped with a warning, not saved back as version 1
Opening a workbook accepted a macro whose format version `v` was above MACRO_VERSION. Serde
dropped the fields this build doesn't know, and the next save wrote the reduced macro back
with `v: 2`, so a newer build would read it as a complete version 2 macro.

A version above MACRO_VERSION is now refused by Macro::check_limits, and parse_macro checks
the raw `v` before reading the steps. Such a macro takes the existing per-macro path: it is
left out with the usual single warning (which now names a newer format), and the rest of the
workbook opens. Tests cover JSON and xlsx hidden names, with current and missing versions as
controls.
2026-10-22 02:26:32 -04:00
prxnvt 3f9223c85f Engine: macro recorder inlines a recorded macro run only when its steps stand alone
A macro run while recording was replaced by every command in it, including both branches of an
If and the body of a Repeat, whenever it had no inputs. For example, recording a run of a macro
with an If played back both the then and the else commands without a condition, and commands
using Row, Column or Output failed on playback. The run is now inlined only when the macro has no
inputs, no output and only plain commands (its disabled steps are left out); otherwise it is left
out and listed as skipped.
2026-10-22 02:26:32 -04:00
prxnvt ccea7a8db3 Engine: macro debugger reports a step that panics as the step's failure
When a debugged step panicked, the nested execute rolled the run back and dropped the debugger
before advance could reattach the machine, so the caller got "No macro is being debugged"
instead of the failed step. For example, stepping into a step whose command panics lost the
step path and the panic message. advance now sees that the run was already rolled back and
returns the usual failed result with the step error.
2026-10-22 02:26:32 -04:00
prxnvt b51d588cac Engine: macros can't open, hide or show windows
The policy denies window.* commands, but the View tab's window commands
are view.*: a macro could run view.newWindow, which adds a window that
the run's rollback doesn't remove, or view.hideWindow and
view.unhideWindow, which change the session's hidden windows.

The policy now denies those three. Arrange All changes nothing in the
engine and Switch Windows only lists windows, so both stay allowed.
2026-10-22 02:26:32 -04:00
prxnvt ac7c1ae639 Engine: a reference on a sheet named like a token stays a reference
is_token looked only at the leading identifier, so on a workbook with a
sheet called Output (or Row, Column, or the name of a declared input),
"Output!A1:B2" was read as the Output token, and the step failed with
"the macro has no output anchor" or "unexpected '!A1:B2'". This hit
reference keys, $ref and {{...}} alike, including references the
recorder writes for other sheets. A base followed by '!' is now a sheet
name, and the text resolves as a plain reference.
2026-10-22 02:26:32 -04:00
prxnvt c6dda8562f Engine: macro recorder keeps references on other sheets as they are
The recorder made every unqualified reference relative to the example input, whatever sheet the
step ran on. For example, starting a recording on Sheet1 with B2:C5 selected, activating Sheet2
and setting C4 recorded Input.cell(3,2), so playback wrote Sheet1 instead; a formula entered on
Sheet2 was changed to refer to Sheet1's cells. Each step now keeps the sheet it ran on, and only
references on the input's sheet become tokens.
2026-10-22 02:26:32 -04:00
prxnvt 45e580ad91 Engine: macro.debug steps through a macro as one undo step
macro.debug runs a macro one step at a time. start checks the macro as
macro.run does (policy, approval, inputs, footprint and output rule)
and pauses before the first step; step runs one step; continue runs to
the next breakpoint; runTo runs to a chosen step path, stopping at
breakpoints on the way; stop ends the run and keeps its changes or
rolls them back; state reports where it is. Breakpoints are the steps'
own flags plus a list of paths given with any action (breakpointsOnly
makes the list replace the flags), and they stop in every pass of a
Repeat.

The run lives on the session between calls, inside an undo group that
spans them (group_begin and group_end), so a debugged run is one undo
step "Run Macro: <name>", as a normal run is. A failing step ends the
run and rolls it back; so does a Stop step that asks for a rollback.
While a macro is being debugged, edits of the workbooks the run touched
(its own, and any other a step changed) are refused at the top level
with "Finish or stop the macro first"; other workbooks stay editable,
and selecting and queries still work. Every answer gives the next
step's path, kind, command, parameters as they would run now (tokens
resolved), the Repeat pass, the parameters of the step that just ran,
the steps executed and the cells changed so far. Each action gets its
own time limit.

If a command panics while a step runs, the session's panic guard rolls
the debugged run back as well, so its undo group never stays open.
2026-10-22 02:26:32 -04:00
prxnvt bf456c9ff8 Engine: a macro's footprint dry runs keep to the run's timeLimit
footprint always ran its dry run with the default 60 second limit, so
macro.run with timeLimit 5 could run for over two minutes (a dry run,
then another after a shift) and fail with "ran longer than 60 seconds".

footprint now takes the run's clock and limit. make_way starts the
run's clock before the dry runs, so the checks and the run share one
limit, and macro.check takes timeLimit too.
2026-10-22 02:26:32 -04:00
prxnvt 709a995290 Model: one damaged macro no longer stops a JSON workbook opening
Workbook::macros was read straight into typed macros, so one entry this
build couldn't read (a step type from a later build, an unknown
onConflict value, an entry that isn't an object) failed the whole file
with "not a GridCraft JSON workbook". The field is now read as raw
entries into Workbook::macros_read, and macros::load reads each one
with parse_macro, as for xlsx: short-form steps are accepted, the depth
limit is checked before deserializing, and a bad entry is dropped with
the same warning. Only the first 1,000 entries are read, as in xlsx.
2026-10-22 02:26:32 -04:00
prxnvt 252ffa95b6 Engine: macro recorder keeps a recording when saving it fails
Stopping a recording took the recorded commands and where recording started before saving the
macro, so a failed save threw both away. For example, stopping with a name that holds a control
character, or in a workbook at its macro limit, ended the recording, and stopping again with a
good name found nothing to save. A failed save now puts the recording back, so stopping again
saves what was recorded.
2026-10-22 02:26:32 -04:00
prxnvt 4c6962a97c Engine: a partial macro footprint no longer clears later writes; the run checks each step
The footprint dry run stops at an approved file step and at Solver, but
make_way took a partial footprint with no conflicts as proof that the
run was safe, also after a shift. A macro that exported a CSV and then
set B1 ran under onConflict fail or ask and overwrote B1 without asking.

When the footprint is partial and the rule is not overwrite, the run's
machine now checks every step as it runs: a step that writes over
cells, sheets or objects that held data when the run started stops the
run and rolls it back, with a confirmation under ask and an error
otherwise. make_way now returns the machine for the run along with
whether it shifted.
2026-10-22 02:26:32 -04:00
prxnvt 3bc1d91636 Engine: macro tokens, resolved to A1 references when a step runs
Step parameters can name where they act through tokens that are worked
out when the step runs, so a macro made on one block of data works on
another: a declared input (Input, or any other input's name), Output
(the output rule's anchor), Row or Column (the current item of the
Repeat step around the step) and Name:Total (a defined name or a
table's data). Methods narrow or move the range: col(n) or
col("Header"), row(n), cols(a..b), rows(2..), cell(r,c) relative to the
top-left cell, offset(r,c), resize(h,w), data (without the header row),
below(n) and right(n). Methods chain.

A token is written as {"$ref": "..."} anywhere, as the whole string of
a reference parameter (cell, range, target, keys ending in Cell or Range
and similar), or as {{...}} inside any string, which is how a formula
refers to its input. Other text parameters are never read as tokens, so
a cell can still hold the word Row and a chart can still be of type
column.

Every token becomes A1 text, sheet-qualified when it is on another
sheet, before the command runs, so no existing command changes. Plain A1
references in a step stay absolute. A reference that runs past the edge
of the sheet is reported apart from other mistakes, so the footprint
check can tell the two apart.
2026-10-22 02:26:32 -04:00
prxnvt 4966b9c86d Engine: opening a file reads its hidden macro names in one bounded pass
load listed every _gc_macro_ item, then read each one with a scan of
all the workbook's names, and removed each with another full scan.
Damaged items didn't count toward the macro limit, so a file with
20,000 damaged items took 20,000 scans of 20,000 names (seconds, and
far more for a larger file). The names are now taken out of the
workbook and grouped by item in one pass; only the first 1,000 items
are read, and the rest are counted in the warning without being read.
2026-10-22 02:26:32 -04:00
prxnvt 7011395fcf Engine: a macro renamed on a name clash keeps its name within the length limit
When a file held two macros with the same name, the second was renamed
"name (2)" without room for the suffix. Two macros with the same
255-character name both opened, but the second name grew to 259
characters, was saved that way, and the next open dropped that macro as
over the limits. unique_name now shortens the name to make room for the
suffix, and load checks the macro again after renaming it.
2026-10-22 02:26:32 -04:00
prxnvt b2740bdf33 Engine: macro recorder and macro.list, get, save, delete, rename, record
Commands: macro.list, macro.get, macro.save (a macro object or its
fields; replace defaults to true), macro.delete, macro.rename and
macro.record, beside macro.run, macro.check and macro.debug. Saving,
renaming and deleting are undoable. The catalog gains View › Macros ›
Record Macro (parity 275 / 291).

The recorder builds on the recording buffer of top-level commands, so
nested commands are never recorded twice and trimming the journal loses
nothing. When it stops, commands a macro may not run are left out and
listed; a macro run while recording is replaced by its steps when it
has no inputs, and an Automate script run by the script's steps;
sheet.activate and selection.set steps are added where the sheet or
selection changed between commands without one, and runs of them
collapse to the last. With relative references (the default) the
selection when recording started is the example input: references in
and near it become tokens (Input.col(2), Input.rows(2..),
Input.col(2).below, Input.row(4).right, Input.cell(r,c)), exact
references in formulas become {{...}}, and the input's width, header
and column types are inferred from the example. A recording replayed on
a longer block of data writes in the matching places.

A toggle such as Bold is recorded with the state it set ("on": true),
as Excel records Bold = True, so a replay sets it instead of flipping
it again.

Automate scripts are unchanged: automate.* keep their commands,
parameters, storage and results, and View › Macros still lists the
scripts and a preserved VBA project.
2026-10-22 02:26:32 -04:00
prxnvt 23c0107511 Engine: macro policy checks a step's parameters after tokens resolve; any path counts as file access
A path given as a token (`{"$ref": "Input"}` or `{{Input}}`) became a file name only when the
step ran, after the policy had passed it, so a macro without permissions or approval could write
a file, even from macro.check. Exports also wrote to blank paths the policy ignored. Any `path`
other than null now needs the permission and approval, and the runner checks the policy again on
the resolved parameters before the command runs.
2026-10-22 02:26:32 -04:00