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.
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.
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.
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.
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.
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.
macro.run runs a macro as one undo step ("Run Macro: <name>"); an error
at any step rolls everything back and names the step and its path (3,
2.1 inside a Repeat, 4.then.1 inside an If). It works in four parts:
- Policy (macro_policy). File, app, window, automate and macro commands
and Undo and Redo never run in a macro. Reading or writing files
(data.getData, data.fromTextCsv, insert.picture, Page Layout
Background, or an export with a path) needs the command in the
macro's permissions and the person's approval. A macro in a workbook
opened from a file asks before it runs. Approval holds for the
session until the macro's content changes; it is asked for with the
new EngineError::Approve and given with approve: true. A step that
would open a dialog or a web page fails instead.
- Inputs. Each declared input is bound (given, its default, or the
selection for the first one), whole columns shrink to the used range,
and the shape and column types are checked with messages that name
the cells: "Input column 2 (Amount) must hold numbers; C4 holds text".
- Footprint. Unless the macro may overwrite, it first runs in dry-run
mode on the real workbook inside an undo group that is rolled back,
with the session's clipboard, journal, windows and the document's
trust put back. CellStore::diff on every sheet, plus the cells
commands wrote (DocState::touched), sheets and objects, gives what it
would write outside its inputs; cells that only recalculated do not
count. Writing over data, deleting a sheet or deleting an object is a
conflict: ask (the new EngineError::Confirm, so programmatic runs fail
unless they pass onConflict or overwrite), fail, overwrite, or shift
the data down or right with Insert Cells and check again. A step that
would fail, for example past the edge of the sheet, fails the run
before anything changes. File steps and Solver stop the dry run early
and are reported.
- Steps run on a machine with an explicit frame stack: Repeat over the
rows or columns of a range with Row or Column bound, If on one cell
with Excel's comparisons, and Stop, which keeps the changes or rolls
them back. A run executes at most 100,000 steps within a time limit
(60 s by default, timeLimit to change; the web build has no clock, so
only the step limit applies there).
macro.check reports the bound inputs, the output anchor, the approval
needed, the footprint and the conflicts without changing anything;
nothing from an unapproved macro runs, not even as a dry run.
A command that changed the workbook cleared the redo history even when an open group owned
its step. When the group was then rolled back inside the same command (the debugger's Stop
with Undo Changes), the rollback put the redo history back and the command's bookkeeping cleared
it again. Now only a command that makes a step of its own clears the redo history; a group
clears it when it ends as a step, and a rollback restores it.
The record of the last group step (document, history length, epoch) outlived the Undo of that
step. A later command that opened and closed its own groups at the same history length, after
rolling one back, passed the epoch check (the record's epoch merely differed from the one the
command started with) and took the name of the nested edit it had made instead of its own.
The step must now come from a group ended during the command (an epoch past the command's
starting one), and a rollback drops a record of a step it truncated away.
Paste Special › Formats copies cell styles and merged areas. It copies no conditional formats,
formulas or validation, so it carries nothing that runs. It still marked the destination
FromFile: copying a formula cell from an opened file and pasting its formats into a new workbook
made that workbook ask before running its own active content.
Formats pastes now count as passive, like values, column widths and notes. The comment and
docs/architecture.md no longer say that pasted formats carry trust.
When the history held 100 steps, group_end pushed its step and dropped the oldest one at once,
so execute's collapse compared against a length that no longer matched. A command that was one
group kept the group's step and added its own, losing two old steps. After test.editThenGroup,
the second Undo brought back the intermediate state after the outside edit.
The history limit now applies after the outermost command has collapsed its steps, and when a
group ends outside any command. A document with an open group is trimmed when the group ends.
execute treated any change in the group epoch as 'the group owns the step'. A command that edited outside its own group, or whose group rolled back before it edited, left that edit with no undo step or only part of it. Now only a group that was open before the command (or is still open after) owns the step; a command that opened and closed its own groups replaces their steps with one entry for the command.
A linked paste writes formulas even when it pastes values, but the values exception skipped the trust update, so a Local workbook stayed Local after linking to a FromFile one. The exception now applies only to unlinked pastes.
Active content such as macros must not run only because a workbook was
opened. DocState gets a trust state: Local for workbooks made in the
session (new workbooks and samples) and FromFile for anything opened
with file.open. Active content in a FromFile workbook asks before it
runs. A second window on a workbook shares its trust.
The clipboard carries the trust of its source document. Pasting
formulas, formats (conditional formats hold formulas) or validation
from a FromFile workbook into another workbook makes that workbook
FromFile too; pasting values, column widths or notes carries nothing
active and changes nothing. Sheet moves and copies stay inside one
workbook, so its trust is unchanged. document.inspect reports the trust.
A macro recorder needs more than a journal index: the journal is
trimmed past 10,000 entries, it also holds the commands that other
commands run (so playback would run them twice), and it keeps no
selection, which relative references need.
Session now tracks how deeply execute calls are nested. While recording,
a buffer (record.rs) keeps each successful, journaled top-level call with
the document uid, the active sheet (index and name) and the selection it
ran in, then its id and parameters without secrets, as the journal
stores them. Queries, failed commands and the command that starts
recording are not kept. The buffer holds up to 100,000 steps and counts
any it drops. Session::start_recording, stop_recording, is_recording and
depth are the interface.
Automate › Record Actions keeps recording through the journal as before.
Session::grouped(label, f) runs f as one undo step on the active
document. On success the group leaves exactly one undo entry holding the
workbook from before the group; on error it restores the workbook,
selection, undo and redo stacks as they were and returns the error.
group_begin, group_end and group_rollback do the same across separate
calls, for an action that runs one step per call.
While a group is open on a document, undoable commands on it add no
undo steps of their own, so a long group neither grows the history nor
pushes older entries past the undo limit, and Undo and Redo are
disabled there. Groups nest: the outermost one makes the step, and a
failing inner group rolls back only its own changes. Closing a document
drops its groups, and a command that panics drops the groups it opened.
history.list reports whether a group is open.
Insert Copied Cells folds its nested steps into one undo step by
counting revisions; inside a group the nested steps push no entries, so
it now leaves the history alone there instead of removing entries from
before the group.