collective

Browse / github.com/storytold/gridcraft / patch

Excel open check: save workbooks with the CLI and open them in real Excel

Excel open check: save workbooks with the CLI and open them in real Excel Adds a repeatable check that saves workbooks with `gridcraft-cli` and opens each result in real Excel through COM, so "does Excel open what we save" (gap 1, issue 891) can be re-run on every release. No engine code changes. `packaging/windows/excel-open-check/check.ps1`: for every `.xlsx`/`.xlsm` in a folder it runs `gridcraft-cli run --in --out`, and for each `*.jsonl` scenario next to it builds a feature workbook from the Sales sample. Each file is opened in a fresh Excel, read-only, `CorruptLoad` = normal load (a part Excel would repair is a failure, not a silent fix), alerts off, macros d

Status
open
Score
0.00
Adopters
0 other operators
Author
Harrison-Booth (imported from pull request #2650, open)
Base
f6b0cec67e328f2f6352768b5cb2feb7e562c5db
Size
+203 −2 in 11 files
Review
pending
Shared
2026-10-28
Id
61c15d7c188d02986fd23a3d

How it went

No other operator has reported using it yet.

Diff

raw · ROADMAP.md docs/gaps.md packaging/windows/excel-open-check/README.md packaging/windows/excel-open-check/charts.jsonl packaging/windows/excel-open-check/check.ps1 packaging/windows/excel-open-check/conditional-formats.jsonl packaging/windows/excel-open-check/pivots.jsonl packaging/windows/excel-open-check/protection-sheets.jsonl packaging/windows/excel-open-check/shapes-pictures.jsonl packaging/windows/excel-open-check/tables-names.jsonl packaging/windows/excel-open-check/validation.jsonl

diff --git a/ROADMAP.md b/ROADMAP.md--- a/ROADMAP.md+++ b/ROADMAP.md@@ -2,7 +2,7 @@  **Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-25 · **Change:** minor (AutoSum over a selected row puts its total to the right, and fills a selection's empty last row and column) · **Target:** Microsoft Excel (Microsoft 365)+> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-26 · **Change:** minor (Excel open check script; AutoSum over a selected row puts its total to the right, and fills a selection's empty last row and column) · **Target:** Microsoft Excel (Microsoft 365)  GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web.@@ -135,6 +135,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in  | Date | What landed | |---|---|+| 2026-10-26 | Excel open check (`packaging/windows/excel-open-check`): saves workbooks with `gridcraft-cli`, opens each in real Excel through COM so a repair counts as a failure, and records pass or the failure per file; 0.12.1 over 18 public fixtures and 7 feature scenarios: 24 of 25 open cleanly, the other is the legacy-formula slowness of #410 | | 2026-10-25 | AutoSum over a selection does what Excel does: a selected row gets one total to its right (in its last cell if that is empty), not a one-cell total under each cell; a block whose last column is empty gets each row's total there, one whose last row is empty each column's, and with both the corner gets the total of the block; otherwise totals go under each column as before | | 2026-10-24 | Go To lists the named ranges of every sheet (`Sheet2!Name` for a name scoped to another sheet; hidden names, constants and formulas left out) and the references entered before, a double-click goes to an entry, and the Reference box takes several names or references separated by commas (`Price, Type`, `B14:C22,F19:G30`) | | 2026-10-24 | Status bar: a right-click opens a menu that sets which selection statistics show (Average, Count and Sum by default; Numerical Count, Minimum and Maximum are new), kept with the preferences (`view.statusBar`) |@@ -242,6 +243,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in  | Date | Change | Summary | |---|---|---|+| 2026-10-26 | minor | Progress log: Excel open check | | 2026-10-25 | minor | Progress log: AutoSum over a selected row, and a selection's empty last row and column | | 2026-10-23 | minor | Progress log: accounting negatives in number text | | 2026-10-23 | minor | Progress log: INDEX with one index on a 2-D reference is #REF! |diff --git a/docs/gaps.md b/docs/gaps.md--- a/docs/gaps.md+++ b/docs/gaps.md@@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-24 · **Change:** minor (§8: PivotTable Enable show details; earlier: PivotTable Show Details) · **Target:** Microsoft Excel (Microsoft 365)+> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-26 · **Change:** minor (§1: repeatable Excel open check; §8: PivotTable Enable show details; earlier: PivotTable Show Details) · **Target:** Microsoft Excel (Microsoft 365)  Every known shortfall against Excel, one entry each, ranked by user impact (what blocks someone using GridCraft for real work first). This is the work list: pick from the top, check the open@@ -22,6 +22,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha- - **Evidence:** round-trip tests cover our own files only (`crates/xlsx/src/tests/`); PivotTable   round-trip noted as "unverified in real Excel" since 10-07; #64 (dynamic-array flag) shows the   class of bug.+- **Check:** `packaging/windows/excel-open-check/check.ps1` saves workbooks with `gridcraft-cli` and opens each result in real Excel through COM with `CorruptLoad` = normal load, so a repair is a failure. The 0.12.1 run (18 public synthetic fixtures + 7 feature scenarios: charts, pivots with pivot chart and slicer, CF, validation, tables and names, shapes and pictures, sheets and protection) gave 24 of 25 saved files opening cleanly; the 25th (`gc_legacy_to_array`) is not a repair but Excel taking over 200 s on legacy formulas saved as whole-column array formulas (#410, open PRs #417, #1367, #1660). Still open: a run over a large real-world corpus. - **Impact:** the main format; a repair prompt or silent loss destroys trust. - **Estimate:** 15–25 h, plus owner time in Excel. [file-format-parity.md](file-format-parity.md) @@ -183,6 +184,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha-  | Date | Change | Summary | |---|---|---|+| 2026-10-26 | minor | §1: `packaging/windows/excel-open-check` opens saved workbooks in real Excel and records pass or the exact failure; first run: 24 of 25 open cleanly | | 2026-10-24 | minor | §8: PivotTable Enable show details: `pivot.options` `enableShowDetails` turns Show Details off for a PivotTable, read and written in XLSX as `enableDrill` | | 2026-10-23 | minor | §8: PivotTable Show Details: double-clicking a value cell lists its source records on a new sheet (#2364) | | 2026-10-22 | minor | §20: a native window per workbook window, New Window on the same workbook and Arrange All on the desktop; left: Arrange All across monitors, the web's one window, pen pressure (estimate 10–14 h → 5–8 h) |diff --git a/packaging/windows/excel-open-check/README.md b/packaging/windows/excel-open-check/README.mdnew file mode 100644--- /dev/null+++ b/packaging/windows/excel-open-check/README.md@@ -0,0 +1,13 @@+# Excel open check++Saves workbooks with `gridcraft-cli` and opens each result in real Excel (COM), recording per file `PASS` or the+exact failure. Windows with Excel only. See `check.ps1` for the options.++```powershell+.\check.ps1 -Cli C:\gridcraft\gridcraft-cli.exe -Inputs C:\workbooks -Csv result.csv+```++Inputs are any `.xlsx`/`.xlsm` (the synthetic `gridcraft/` fixtures in a public fixtures repo are a good start); each+`*.jsonl` here is a feature scenario run on the Sales sample. Add a scenario by dropping in another `.jsonl`+(`gridcraft-cli run --script` format). Results: `Original` is the input opened in Excel (so a file Excel itself+rejects or loads slowly isn't blamed on GridCraft), `Saved` is the GridCraft-saved copy.diff --git a/packaging/windows/excel-open-check/charts.jsonl b/packaging/windows/excel-open-check/charts.jsonlnew file mode 100644--- /dev/null+++ b/packaging/windows/excel-open-check/charts.jsonl@@ -0,0 +1,12 @@+# One chart of each common kind over the sales table+insert.chart={"range":"B4:F8","type":"bar","at":"K36"}+insert.chart={"range":"B4:F8","type":"line","subtype":"markers","at":"S3"}+insert.chart={"range":"B4:F8","type":"area","subtype":"stacked","at":"S19"}+insert.chart={"range":"C5:D8","type":"scatter","at":"S36"}+insert.chart={"range":"B4:F8","type":"radar","at":"AA3"}+insert.chart={"range":"B4:C8","type":"doughnut","at":"AA19"}+insert.chart={"range":"B4:C8","type":"waterfall","at":"AA36"}+insert.chart={"range":"B4:C8","type":"treemap","at":"AI3"}+insert.chart={"range":"B4:C8","type":"histogram","at":"AI19"}+insert.sparkline={"range":"C5:F5","location":"J5","type":"line"}+insert.sparkline={"range":"C6:F6","location":"J6","type":"column"}diff --git a/packaging/windows/excel-open-check/check.ps1 b/packaging/windows/excel-open-check/check.ps1new file mode 100644--- /dev/null+++ b/packaging/windows/excel-open-check/check.ps1@@ -0,0 +1,125 @@+<#+Excel open check: saves workbooks with GridCraft, opens each result in real Excel (COM) and records per+file whether Excel loads it as it is or has to repair it (docs/file-format-parity.md, "Excel open check").++  check.ps1 -Cli C:\gridcraft\gridcraft-cli.exe [-Inputs C:\workbooks] [-Out C:\out] [-Csv result.csv]+            [-TimeoutSec 120]++Files saved:+  - every .xlsx / .xlsm in -Inputs, opened and saved with `gridcraft-cli run --in --out` (the original+    is opened in Excel too, so a file Excel itself dislikes or loads slowly is told apart from a+    GridCraft regression);+  - every *.jsonl scenario next to this script (one feature each), run on the Sales sample.++Each file is opened read-only with CorruptLoad = xlNormalLoad (0), so a part Excel would repair is a+failure instead of a silent fix, with DisplayAlerts off and VBA macros disabled so nothing waits on a prompt.+AutomationSecurity does not cover Excel 4.0 (XLM) macros, which would still prompt, so a workbook that+holds XLM macro sheets is never opened: it is reported as SKIP-XLM and the run does not pass.+Excel is a fresh instance per file; a file that hangs or kills Excel is recorded as TIMEOUT or CRASH and+that instance (found by its window's process id) is stopped. Nothing is written next to the inputs; any other+Excel, such as one the person starts meanwhile, is never touched. Exit code 1 when any saved file is not PASS.+#>+param(+    [Parameter(Mandatory = $true)][string]$Cli,+    [string]$Inputs,+    [string]$Out = (Join-Path ([IO.Path]::GetTempPath()) 'gridcraft-excel-open-check'),+    [string]$Csv,+    [int]$TimeoutSec = 120+)+$ErrorActionPreference = 'Stop'++# Runs in a child PowerShell so a hang can be killed after $TimeoutSec without taking this script with it.+$probe = {+    param($path)+    $x = $null+    try {+        # Compiled before Excel starts, so nothing between the start and the PID line can fail or stall on it.+        Add-Type -Namespace Win -Name U -MemberDefinition '[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);'+        $x = New-Object -ComObject Excel.Application+        # Report this instance's process id so the caller can stop exactly it and nothing else.+        $xlPid = [uint32]0+        [void][Win.U]::GetWindowThreadProcessId([IntPtr]$x.Hwnd, [ref]$xlPid)+        "PID`t$xlPid"+        $x.Visible = $false+        $x.DisplayAlerts = $false+        $x.AutomationSecurity = 3 # msoAutomationSecurityForceDisable: no VBA macros (XLM is screened by Test-HasXlm)+        # Named arguments: PowerShell can't skip the optional ones in between positionally.+        $wb = [System.__ComObject].InvokeMember('Open', [Reflection.BindingFlags]::InvokeMethod, $null, $x.Workbooks,+            @($path, 0, $true, 0), $null, $null, @('Filename', 'UpdateLinks', 'ReadOnly', 'CorruptLoad'))+        $sheets = $wb.Worksheets.Count + $wb.Chartsheets.Count+        $wb.Close($false)+        "PASS`t$sheets sheets"+    } catch {+        "FAIL`t" + ($_.Exception.Message -replace '\s+', ' ')+    } finally {+        # Also covers a failure before the PID line: this process's own instance is quit, no other is touched.+        if ($x) { try { $x.Quit() } catch {} }+    }+}++# True when the package holds Excel 4.0 macro sheets, which ForceDisable does not stop and Excel would prompt for.+function Test-HasXlm([string]$path) {+    Add-Type -AssemblyName System.IO.Compression.FileSystem+    # Not a zip at all: nothing to screen, Excel gets to report it.+    try { $zip = [IO.Compression.ZipFile]::OpenRead($path) } catch { return $false }+    try { return [bool]($zip.Entries | Where-Object { $_.FullName -like 'xl/macrosheets/*' }) } finally { $zip.Dispose() }+}++function Test-InExcel([string]$path) {+    if (Test-HasXlm $path) { return [pscustomobject]@{ State = 'SKIP-XLM'; Seconds = 0; Detail = 'Excel 4.0 macro sheets; not opened' } }+    $clock = [Diagnostics.Stopwatch]::StartNew()+    $job = Start-Job -ScriptBlock $probe -ArgumentList $path+    $done = Wait-Job $job -Timeout $TimeoutSec+    $lines = @(Receive-Job $job -Keep -ErrorAction SilentlyContinue)+    $failed = $job.State -eq 'Failed'+    if (-not $done) { Stop-Job $job }+    Remove-Job $job -Force+    $xlPid = ($lines | Where-Object { $_ -like "PID`t*" } | Select-Object -First 1) -replace '^PID\t', ''+    $out = $lines | Where-Object { $_ -notlike "PID`t*" } | Select-Object -Last 1+    if (-not $done) { $out = "TIMEOUT`tno answer in $TimeoutSec s" } elseif ($failed -or -not $out) { $out = "CRASH`tno result (Excel died)" }+    if ($xlPid) {+        # A clean Quit lets Excel exit by itself; give it a moment, then stop only this instance.+        Wait-Process -Id ([int]$xlPid) -Timeout 15 -ErrorAction SilentlyContinue+        Stop-Process -Id ([int]$xlPid) -Force -ErrorAction SilentlyContinue+    }+    $state, $detail = $out -split "`t", 2+    [pscustomobject]@{ State = $state; Seconds = [math]::Round($clock.Elapsed.TotalSeconds, 1); Detail = $detail }+}++$saved = Join-Path $Out 'saved'+Remove-Item -LiteralPath $saved -Recurse -Force -ErrorAction SilentlyContinue+New-Item -ItemType Directory -Path $saved -Force | Out-Null++# (name, original or $null, saved) per file+$work = @()+if ($Inputs) {+    foreach ($f in Get-ChildItem -LiteralPath $Inputs -File | Where-Object { $_.Extension -in '.xlsx', '.xlsm' } | Sort-Object Name) {+        $work += , @($f.Name, $f.FullName, (Join-Path $saved $f.Name), @('run', '--in', $f.FullName, '--quiet'))+    }+}+foreach ($s in Get-ChildItem -Path $PSScriptRoot -Filter *.jsonl | Sort-Object Name) {+    $work += , @("scenario:$($s.BaseName)", $null, (Join-Path $saved "$($s.BaseName).xlsx"), @('run', '--sample', 'sales', '--script', $s.FullName, '--quiet'))+}++$rows = @()+foreach ($w in $work) {+    $name, $orig, $dest, $cliArgs = $w+    # The CLI reports on stderr, which Stop would turn into an exception.+    Remove-Item -LiteralPath $dest -Force -ErrorAction SilentlyContinue+    $ErrorActionPreference = 'Continue'+    & $Cli @cliArgs --out $dest *> $null+    $code = $LASTEXITCODE+    $ErrorActionPreference = 'Stop'+    $base = if ($orig) { (Test-InExcel $orig) } else { $null }+    $r = if ($code -eq 0 -and (Test-Path -LiteralPath $dest)) { Test-InExcel $dest } else { [pscustomobject]@{ State = 'SAVE-FAIL'; Seconds = 0; Detail = "gridcraft-cli exit code $code" } }+    $rows += [pscustomobject]@{+        File = $name; Original = if ($base) { $base.State } else { '-' }; Saved = $r.State+        Seconds = $r.Seconds; Detail = $r.Detail+    }+}+$rows | Format-Table -AutoSize -Wrap | Out-String -Width 220 | Write-Output+if ($Csv) { $rows | Export-Csv -NoTypeInformation -Path $Csv }+$bad = @($rows | Where-Object { $_.Saved -ne 'PASS' -or $_.Original -eq 'SKIP-XLM' }).Count+$savedBad = @($rows | Where-Object Saved -ne 'PASS').Count+"{0} of {1} saved files pass" -f ($rows.Count - $savedBad), $rows.Count+if ($bad) { exit 1 }diff --git a/packaging/windows/excel-open-check/conditional-formats.jsonl b/packaging/windows/excel-open-check/conditional-formats.jsonlnew file mode 100644--- /dev/null+++ b/packaging/windows/excel-open-check/conditional-formats.jsonl@@ -0,0 +1,8 @@+# Colour scale, data bar, icon set, top 10, formula and text rules+home.conditionalFormat={"range":"C5:F8","rule":{"type":"colorScale"}}+home.conditionalFormat={"range":"C5:C8","rule":{"type":"dataBar"}}+home.conditionalFormat={"range":"D5:D8","rule":{"type":"iconSet"}}+home.conditionalFormat={"range":"E5:E8","rule":{"type":"top10","rank":2}}+home.conditionalFormat={"range":"F5:F8","rule":{"type":"cellIs","operator":"greaterThan","value":"60000","preset":"lightRedFill"}}+home.conditionalFormat={"range":"B5:B8","rule":{"type":"containsText","text":"th","preset":"yellowFill"}}+home.conditionalFormat={"range":"C5:F8","rule":{"type":"expression","formula":"=C5>AVERAGE($C$5:$F$8)","preset":"greenFill"}}diff --git a/packaging/windows/excel-open-check/pivots.jsonl b/packaging/windows/excel-open-check/pivots.jsonlnew file mode 100644--- /dev/null+++ b/packaging/windows/excel-open-check/pivots.jsonl@@ -0,0 +1,4 @@+# PivotTable on the sales table, a pivot chart, a table slicer+insert.pivotTable={"source":"Sales","destination":"Sales!K40","rows":["Region"],"values":[{"field":"Q1","func":"sum"}]}+insert.pivotChart={"pivot":"Sales!K42","type":"column","at":"S50"}+insert.slicer={"table":"Sales","column":"Region","at":"AA50"}diff --git a/packaging/windows/excel-open-check/protection-sheets.jsonl b/packaging/windows/excel-open-check/protection-sheets.jsonlnew file mode 100644--- /dev/null+++ b/packaging/windows/excel-open-check/protection-sheets.jsonl@@ -0,0 +1,9 @@+# Extra sheets (renamed, coloured, hidden), grouping and a protected sheet+home.insertSheet={}+sheet.rename={"name":"Second"}+sheet.tabColor={"color":"#FF0000"}+cell.set={"cell":"A1","input":"=Sales!B5&\" \"&Sales!C5"}+home.insertSheet={}+sheet.hide={}+sheet.activate={"sheet":"Sales"}+review.protectSheet={"formatCells":true}diff --git a/packaging/windows/excel-open-check/shapes-pictures.jsonl b/packaging/windows/excel-open-check/shapes-pictures.jsonlnew file mode 100644--- /dev/null+++ b/packaging/windows/excel-open-check/shapes-pictures.jsonl@@ -0,0 +1,5 @@+# Shapes with text and a picture from base64 (a 1x1 PNG)+insert.shape={"kind":"rectangle","at":"K30","width":120,"height":40,"text":"Box","fill":"#FFC000"}+insert.shape={"kind":"ellipse","at":"O30","width":80,"height":40}+insert.shape={"kind":"arrow","at":"K33","width":80,"height":30}+insert.picture={"base64":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==","mime":"image/png","at":"B20","width":60,"height":60,"alt":"dot"}diff --git a/packaging/windows/excel-open-check/tables-names.jsonl b/packaging/windows/excel-open-check/tables-names.jsonlnew file mode 100644--- /dev/null+++ b/packaging/windows/excel-open-check/tables-names.jsonl@@ -0,0 +1,13 @@+# Table style and total row, defined names, merged cells, frozen panes, hyperlink-free cell notes+selection.set={"cell":"C5"}+table.totalRow={}+table.style={"style":"TableStyleLight9"}+formulas.defineName={"name":"NorthQ1","refersTo":"=Sales!$C$5"}+formulas.defineName={"name":"AllRegions","refersTo":"=Sales!$B$5:$B$8"}+formulas.defineName={"name":"Rate","refersTo":"=0.2","comment":"tax rate"}+cell.set={"cell":"K14","input":"=SUM(AllRegions)"}+cell.set={"cell":"K15","input":"=NorthQ1*Rate"}+home.mergeCells={"range":"K16:M16"}+view.freezePanes={"cell":"C5"}+review.newNote={"cell":"C5","text":"checked"}+review.newComment={"cell":"D5","text":"threaded"}diff --git a/packaging/windows/excel-open-check/validation.jsonl b/packaging/windows/excel-open-check/validation.jsonlnew file mode 100644--- /dev/null+++ b/packaging/windows/excel-open-check/validation.jsonl@@ -0,0 +1,8 @@+# One rule of each kind+data.validation={"range":"K2:K5","type":"list","formula1":"North,South,East,West"}+data.validation={"range":"K6:K8","type":"whole","operator":"between","formula1":"1","formula2":"100","inputTitle":"Count","inputMessage":"1 to 100","errorStyle":"warning"}+data.validation={"range":"L2:L5","type":"decimal","operator":"greaterThan","formula1":"0"}+data.validation={"range":"L6:L8","type":"date","operator":"greaterOrEqual","formula1":"2026-01-01"}+data.validation={"range":"M2:M5","type":"textLength","operator":"lessOrEqual","formula1":"10"}+data.validation={"range":"M6:M8","type":"custom","formula1":"=ISNUMBER(M6)"}+data.validation={"range":"N2:N5","type":"list","formula1":"=$B$5:$B$8"}