Field note · July 24, 2026
Spreadsheet changes that can pass CI
For important spreadsheets, the painful question is rarely “which file changed?” It is “did someone replace the calculation behind this number, what now depends on it, and did we notice before the model was shared?” Excel is flexible precisely because the model and its logic live together. That flexibility turns ordinary version history into a weak review surface: Git sees a binary file, while a reviewer needs to see a change in intent.
Workbook-aware Git diffs and formula anomaly analysis already address valuable parts of the problem. I wanted the missing merge-boundary layer: a small, local tool that could describe a semantic workbook change, trace its visible consequences, and enforce a rule that a reviewer can read.
The merge boundary is where trust gets practical
FormulaFence compares two .xlsx or .xlsm workbooks without executing formulas or macros. It detects formula-to-value substitutions, formula changes, sheet visibility and defined-name changes, Excel-table definition changes, explicit external references, broken #REF! formulas, calculation-setting changes, and macro payload changes. For each changed cell, it follows statically visible A1-style, ordinary named-range, safely expandable formula-defined-name, supported table, and direct 3-D worksheet dependencies with deterministic shortest-path samples.
formulafence check approved.xlsx candidate.xlsx \
--policy formulafence.yml \
--format sarif --output results.sarif
The policy is intentionally plain YAML. It can protect an output cell, confine routine edits to an input range, ban formula overrides, and cap downstream impact. A policy is not a substitute for judgment. It is a way to make the review expectation visible and repeatable.
When the underlying workbook parser encounters an OOXML extension it cannot fully interpret, FormulaFence records a coverage note. A candidate that adds such a warning can be rejected with no_new_parser_warnings.
FormulaFence 0.7.0 resolves ordinary workbook and sheet-local names whose destinations are static A1 ranges, plus a conservative set of Excel-table references: table names, static columns or contiguous column ranges, and #All/#Data/#Headers/#Totals regions. Profiles inventory the table definitions behind those references, and a diff emits FF013 if one changes. Teams can make that a hard boundary with no_table_definition_changes.
The table support also traces common row-scoped forms without turning a row calculation into a dependency on every row of a table. Inside a table data cell, [@[Sales Amount]] and [Sales Amount] bind to that row. Qualified forms such as Sales[@Amount] and Sales[[#This Row],[Amount]:[Rate]] bind to the named table's data row even from an adjacent cell. That follows Excel's documented structured-reference semantics; header, total, cross-sheet, ambiguous, and complex bracket-escape cases remain coverage notes instead of guessed dependency paths.
Version 0.6.0 also expands a direct internal 3-D A1 reference such as Jan:Mar!B2 across every worksheet tab between its endpoints. Profiles identify those formulas. Since inserting, moving, or removing a tab can change that span without changing the formula text, the diff reports FF014; teams can turn it into a CI boundary with no_3d_reference_scope_changes. This follows Excel's documented 3-D-reference behavior. External, malformed, endpoint-missing, and non-A1 variants remain visible coverage limits rather than guessed dependencies.
Version 0.7.0 follows the static paths behind formula-defined names as well. A name such as DiscountedValue can itself use a second name such as TaxRate; when the complete definition resolves to internal dependencies or a constant, FormulaFence traces through to the source cells. It handles nested workbook and sheet-local definitions without evaluating the formulas. Relative, cyclic, external, dynamic, 3-D-in-name, and tokenizer-unsupported definitions remain explicit coverage gaps instead of guessed paths. That matches Excel's support for names representing formulas and constants.
Profiles still make the remaining blind spots concrete: they identify unresolved range tokens and dynamic INDIRECT/OFFSET formulas, while a diff can flag newly introduced instances. Teams that need a stricter boundary can enable no_new_unresolved_references and no_new_dynamic_references in policy.
Testing the uncomfortable case
A synthetic workbook is necessary for unit tests, but it is not enough for a file reader. I validated the profile path against the public Foresight Cap Table and Exit Waterfall Tool, an 18-sheet workbook containing 6,623 non-empty cells and 4,228 formula cells. The inspection surfaced an unsupported OOXML extension as a structured coverage note rather than allowing a raw dependency warning to pollute CI output. It also identified 36 cells using INDIRECT, making a real model’s dynamic-reference surface visible without pretending to calculate it.
On a local, non-distributed copy, changing one exit-waterfall formula to a number produced a formula-to-value finding and traced 330 downstream formula cells. The starter policy rejected both the override and its impact. The details and constraints are recorded in the project’s validation notes.
I also checked the table path against a public structured-reference workbook: FormulaFence identified its one table and 15 table-reference formulas with no unresolved tokens. Changing one table data cell on a local copy traced 16 downstream formulas, including a table total and an output outside the table. For the new row-scoped logic, a controlled workbook changed one input and traced exactly its calculated-row cell, a neighboring qualified row-reference cell, and an external summary—without marking the other two table rows. The validation record describes the boundary and evidence.
What it does not claim
FormulaFence does not calculate Excel, prove a financial model correct, or resolve every named formula or dynamic reference. Relative, cyclic, external, and tokenizer-unsupported name definitions, unsupported or ambiguous table syntax, and features such as INDIRECT can still hide dependencies from static analysis. It calls that out rather than inventing a graph it cannot justify. Material models still need a qualified owner, a recalculation path in the approved spreadsheet engine, and independent review.
The current release is FormulaFence 0.7.0 on GitHub under MIT. If your team keeps significant logic in a workbook, I hope it makes the next change easier to understand before it becomes difficult to unwind.