DEV Community

SybilGambleyyu
SybilGambleyyu

Posted on • Edited on • Originally published at sybilgambleyyu.github.io

Spreadsheet changes that can pass CI

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 because the model and its logic live together. That same 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.

That tool is FormulaFence, an MIT-licensed, local-first CLI for .xlsx and .xlsm workbooks.

Put the control at the merge boundary

FormulaFence compares two workbooks without executing formulas or macros. It detects formula-to-value substitutions, formula changes, sheet visibility, defined-name, Excel-table, worksheet/table AutoFilter, sort, and row and column visibility controls (including zero-sized dimensions), ignored Excel error-checking, Named Sheet View alternate filter/sort, legacy Custom View alternate display/filter/print, Excel Table Style presentation controls, and legacy shared-workbook revision headers/logs, cell number-format, cell-font, cell-fill, effective cell-alignment, effective cell-border, material worksheet-dimension, worksheet-display, worksheet print-layout, and native worksheet-image controls, data-validation, conditional-formatting, operational protection, external-data refresh-control, external-link-package, package-wide external OPC relationships, Excel 4.0/XLM macro-sheet, Office RibbonX custom-UI, Office Web Add-in task-pane, worksheet-binding, and in-content controls, DrawingML chart definition/cached-series/overlay changes, PivotTable binding/layout/cache changes, Slicer and Timeline cache filter state, embedded Power Pivot/Data Model packages, What-If Data Table and Scenario Manager declarations, modern and legacy-VML worksheet controls/OLE, and Power Query formula/control changes, explicit external references, broken #REF! formulas, calculation-setting changes, and macro payload changes.

It also compares character-level rich-text run presentation and phonetic-hint controls held in shared strings or inline strings, without exposing the text or formatting material in a profile or report.

It also follows non-chart worksheet DrawingML relationships for regular text-box shapes, connectors, and nested groups, so a reviewer-facing warning or process link cannot be moved, restyled, relinked, reattached, or quietly made less visible outside the cell grid without a review event.

It also follows modern Excel threaded-comment and person package relationships, so a review instruction, reply, resolution, mention, or collaborator binding cannot silently change outside the worksheet grid.

FormulaFence 0.44.0 also protects conventional Excel Notes: their authors, text, properties, cell binding, VML visibility/layout, and the tc={GUID} compatibility placeholders that can accompany threaded comments. Those declarations live outside ordinary cells, but their private content never appears in reports.

FormulaFence 0.45.1 also protects ordinary worksheet-cell hyperlinks. A familiar cell value can stay fixed while its stored external/file target, in-workbook location, display override, or ScreenTip changes. FormulaFence compares those declarations and their selected relationship semantics privately; targets, references, locations, display strings, ScreenTips, and identifiers never appear in reports.

FormulaFence 0.46.0 also protects Office 2010 worksheet sparklines. A compact trend can keep every ordinary cell fixed while its source formula, destination cell, type, axes, markers, hidden-data behavior, line weight, or colour controls change in the x14:sparklineGroups extension. FormulaFence compares those stored bindings and controls privately before the ordinary reader drops the extension. It emits FF048 for a material change; no_worksheet_sparkline_changes makes that a fail-closed FFP048 boundary. Profiles expose aggregate counts only: source/date-axis formulas, output cells, control values, and colours never appear in reports.

FormulaFence 0.47.0 also protects SpreadsheetML XML Maps. An XML map can pair an embedded schema and map-level refresh/export behavior with XML table columns or individual cells, redirecting a field path, file/connection binding, target cell, or append/format/sort/validation setting while ordinary cells and formulas stay fixed. FormulaFence reads raw XML Map, table XML-column-property, and single-cell table parts and their workbook/worksheet relationships before ordinary readers normalize them. It emits FF049 for a material declaration, binding, or relationship change; no_xml_mapping_changes makes that a fail-closed FFP049 boundary. Profiles expose aggregate counts only: schemas, map names, XPath expressions, table identities, target cells, connection identities, and relationship targets never appear in reports. Equivalent Boolean and unsigned-integer spellings, relationship IDs/order, and equivalent internal target spelling stay quiet. Missing, malformed, unsafe, unbound, unreadable, oversized, or over-budget metadata becomes a visible coverage warning. FormulaFence does not import/export XML, validate XML data or schemas, open bindings, fetch data, calculate refresh results, or infer Excel client behavior.

FormulaFence 0.48.0 makes the separate package- and VBA-project signature-control surfaces reviewable. It follows the Open Packaging Convention’s digital-signature origin to XML signature and certificate relationships, and the VBA project relationship graph to classic, Agile, and V3 signature blobs. This complements—not replaces—Excel’s documented code-signing trust prompt. A material stored-control change emits FF050; no_digital_signature_changes adds the fail-closed FFP050 boundary. Profiles retain only aggregate origin, XML-signature, reference, embedded-certificate, certificate-part, and VBA-signature counts. Signature XML, reference URIs, digests, certificate material, relationship IDs, and binary signature payloads stay private. Equivalent relationship order/IDs, internal target spelling, and XMLDSIG Base64 whitespace stay quiet. Missing, duplicate, malformed, unsafe, unbound, unreadable, oversized, or over-budget data becomes coverage evidence; raw signature parts are bounded at 16 MiB each, 64 MiB total, and 512 parts. This is a provenance-control diff, not cryptographic verification. FormulaFence does not validate XMLDSIG transforms, references, digests, signature values, or signed-content coverage. It does not validate certificate-chain trust, identity, expiry, revocation, or timestamps. It does not establish that a VBA signature covers particular code or identify an authentic signer.

FormulaFence 0.49.0 also protects Excel rich-data controls. Rich Value Data can keep linked-entity values, provider-backed fields, web-image associations, and worksheet value-metadata bindings outside ordinary cells. FormulaFence reads raw Rich Value Data, Rich Value Structure, Rich Value Types, array, supporting property-bag, style, web-image, and rich-value-relationship parts; it also checks package/workbook relationships and XLRICHVALUE metadata bindings before normal readers can omit them. A material stored definition, value, binding, or relationship change emits FF051; no_rich_data_changes makes that a fail-closed FFP051 boundary. Profiles retain aggregate part, value, structure, array, property-bag, binding, bound-cell, image, relationship, and external-reference counts only. Entity values, provider data, field names, identifiers, URLs, image references, relationship IDs, and bound-cell locations stay private. Equivalent relationship IDs/order and internal target spelling stay quiet. Missing, malformed, unsafe, unreadable, oversized, or over-budget data becomes coverage evidence. FormulaFence does not contact providers, refresh values, calculate formulas, fetch relationship targets, validate target content, or infer Excel client behavior.

FormulaFence 0.50.0 also protects persisted custom workbook data stores. Generic Custom XML and its properties, schema references, and relationships; workbook Custom Data Properties and opaque data; and document custom properties can retain add-in state or review-relevant values outside ordinary cells. FormulaFence reads those raw package parts using Microsoft’s add-in state guidance and Custom Data/Custom Data Properties definitions. A material stored-control change emits FF052; no_custom_data_store_changes makes that a fail-closed FFP052 boundary. Profiles retain aggregate part, property, schema, and relationship counts only: XML, schemas, property names and values, storage identities, opaque payloads, relationship IDs, and target contents stay private. Custom XML item IDs and Custom Data IDs are compared privately because an add-in can bind state to them; relationship IDs and document-property PIDs are normalized. Power Query’s DataMashup stays under its existing Power Query boundary. Missing, malformed, unsafe, unbound, unreadable, oversized, or over-budget metadata becomes coverage evidence. FormulaFence does not execute an add-in, refresh or interpret its data, fetch a target, or infer Excel client behavior.

FormulaFence 0.51.0 also guards the workbook-level DrawingML Theme. A Theme can
change the colour, font, or effect schemes used by themed cells, charts, and
drawing objects without changing a local cell style. FormulaFence reads the raw
workbook Theme binding, transitional and strict Theme XML, and direct
Theme-image relationships and bounded payloads before ordinary readers flatten
that surface. A material stored-control change emits FF053;
no_workbook_theme_changes adds the fail-closed FFP053 boundary.
Profiles expose aggregate Theme-part, scheme, relationship, image, and
malformed-metadata counts only: Theme XML, colours, font names, image bytes,
relationship IDs, and targets remain private. Writer-selected relationship
IDs/order and equivalent internal target spelling stay quiet. FormulaFence does
not resolve effective styles, render a workbook, calculate contrast, decode an
image, fetch a target, or infer Excel client behavior. The stored surface is
described by the Open XML SDK
WorkbookPart
Theme relationship and Microsoft’s
Theme-indexed formatting guidance.

FormulaFence 0.52.0 also guards effective cell alignment. An unchanged value,
warning, or classification can be repositioned, rotated, wrapped, shrunk, or
indented without an ordinary cell diff. FormulaFence compares raw
<alignment> controls across base and cell XFs,
xfId/applyAlignment inheritance, and direct cell, row,
and column assignments. A material stored effective-control change emits
FF054; no_cell_alignment_changes adds the fail-closed
FFP054 boundary. Profiles retain structural counts only: alignment
values, style IDs, and targets stay private. Equivalent defaults,
Boolean/integer spellings, inert mergeCell, inheritance, and
column-range splitting stay quiet; malformed metadata becomes coverage
evidence. This is stored-declaration comparison, not width/height,
merged-layout, overflow, visibility, visual-style composition, or Excel
rendering. The scope follows the OOXML
alignment form
and style-xf inheritance.

FormulaFence 0.53.0 also guards material worksheet-display controls. A saved
view can make zeroes appear blank, show formulas instead of results, hide or
recolour gridlines, hide headers, outline symbols, rulers, or page whitespace,
switch direction/view mode, or split/freeze the reviewer’s surface without a
cell edit. FormulaFence compares raw transitional and strict SpreadsheetML
sheetViews/sheetView declarations before ordinary readers normalize them. A
material control change emits FF055; no_worksheet_display_control_changes
adds the fail-closed FFP055 boundary. Profiles expose structural counts only:
sheet names, pane/selection targets, gridline-colour IDs, and raw XML stay
private. Omitted/default controls, Boolean and active custom-gridline-colour
spellings, split decimals, and ordinary selection/top-left/zoom navigation
churn stay quiet; malformed metadata becomes visible coverage evidence. This is
stored-declaration comparison, not Excel rendering, palette-colour resolution,
viewport geometry, final visibility, extension behavior, or client state. The
scope follows the Open XML SDK
SheetView surface
and Microsoft’s worksheet-display guidance.

FormulaFence 0.54.0 also guards the saved print surface. A workbook can preserve
every ordinary cell yet print a smaller report, repeat different title rows,
change headings or gridlines, reframe pages with margins and fit/scaling
controls, alter a header or footer, or move a manual page break. FormulaFence
compares raw transitional and strict SpreadsheetML print-area/title names plus
worksheet print options, margins, page setup, fit-to-page, headers/footers, and
manual breaks before reader normalization. A material change emits FF056;
no_worksheet_print_layout_changes adds FFP056. Reports retain structural
counts only: ranges, header/footer text, page values, printer references, and
raw XML stay private. Omitted/default and equivalent Boolean/integer/decimal
spellings stay quiet, as do inactive first/even headers, disabled first-page
numbers, overridden scale, and automatic-break display noise. The boundary is
stored declarations, not a print engine: it does not render/preview Excel,
calculate pagination, resolve printer defaults or devMode, or cover
custom/legacy sheet-view print controls. The scope follows Microsoft’s
print-area guidance
and PageLayout surface.

FormulaFence 0.55.0 also guards effective cell-border controls. An unchanged
value can be reframed as a report edge, total, exception box, or warning without
an ordinary cell diff. FormulaFence compares raw transitional and strict
SpreadsheetML <borders>/<border> definitions, borderId, base and cell XF
inheritance (xfId/applyBorder), and direct cell, custom-row, and column
assignments. It covers ordinary edges, Office 2010 logical start/end edges,
diagonals/direction, outline, stored line styles, and stored colours. A material
effective change emits FF057; no_cell_border_changes adds the fail-closed
FFP057 boundary. Profiles retain structural counts only: definitions, colours,
style IDs, and targets remain private. Equivalent omitted/none sides,
Boolean/colour spelling, unused diagonal material, empty outline="false",
inheritance, and column splitting stay quiet; malformed material and ordinary
inner vertical/horizontal sides become explicit coverage evidence.
FormulaFence compares stored declarations, not a rendered workbook: it does not
resolve theme/palette colours, choose adjacent-cell precedence, apply
conditional-format/table/differential-style borders, calculate print output, or
infer Excel client behavior. The boundary follows OOXML's
border
and xf
forms, plus Microsoft's cell-border guidance.

FormulaFence 0.56.0 also guards material worksheet-dimension controls. A
workbook can retain every ordinary cell while stored defaults, direct positive
row/column dimensions, bestFit, Office 2010 x14ac:dyDescent baseline
adjustments, or automatic thick-border row adjustments reframe wrapped content
or automatic pagination. FormulaFence compares raw transitional and strict
sheetFormatPr, row, and layered col declarations before ordinary-reader
normalization. A material change emits FF058;
no_worksheet_dimension_changes adds the fail-closed FFP058 boundary.
Profiles retain aggregate counts only: dimensions, worksheet targets, writer
hints, and XML stay private. Zero/hidden dimensions remain under FF036.
Equivalent numeric/Boolean spelling, inert writer hints, and equivalent layered
column ranges stay quiet; malformed metadata becomes coverage evidence. This is
stored-declaration comparison, not final Excel layout, AutoFit calculation,
wrapped/merged overflow, or page-geometry prediction. The scope follows the
OOXML sheetFormatPr,
row,
and col
forms plus Microsoft's dyDescent extension documentation.

FormulaFence 0.57.0 also guards native worksheet image controls. A floating
DrawingML xdr:pic, direct worksheet <picture> background, or VML-backed
header/footer watermark can alter a report while every ordinary cell remains
unchanged. FormulaFence follows the relevant worksheet relationships, supports
transitional and strict DrawingML, and compares private anchor/picture/VML
declarations, relationship semantics, and bounded direct image-payload hashes.
A material change emits FF059; no_worksheet_image_changes adds the
fail-closed FFP059 boundary. Profiles expose aggregate structural counts only:
image bytes, names/descriptions, visual formatting, anchors, relationship IDs/
targets, and raw XML stay private. Writer-selected non-visual/VML IDs and
consistent relationship-ID rewrites stay quiet. This is a stored-package
comparison, not image decoding/rendering, external retrieval, visibility,
cropping/z-order, or pagination calculation. The scope follows Open XML's
xdr:pic Picture definition, Microsoft's worksheet background guidance, and header/footer watermark guidance. A fresh packaged wheel was exercised against independently maintained XlsxWriter picture, background, and watermark examples: replacing only direct media payload bytes emitted FF059 and FFP059 without exposing raw media paths in JSON, Markdown, or SARIF. The validation record has the exact artifacts and hashes.

FormulaFence 0.58.0 extends that same FF044 boundary to native worksheet
DrawingML connectors (xdr:cxnSp). A connector can encode a process or review
link by its geometry, style, anchor, and stCxn/endCxn attachments—even while
every cell is unchanged. FormulaFence supports transitional and strict
DrawingML, including group-contained connectors, and compares those stored
declarations and endpoint target semantics privately. Profiles expose only
connector and attachment counts; names, descriptions, anchors, non-visual IDs,
and endpoint target IDs stay out of JSON, Markdown, and SARIF. Consistent ID
rewrites stay quiet; a reattachment, styling, geometry, or anchor change emits
FF044, and the existing no_worksheet_drawing_shape_changes policy adds
FFP044. Missing, duplicate, or malformed attachment metadata is explicit
coverage evidence. This is stored-package comparison, not connector routing or
rendering, theme/visibility resolution, target retrieval, media parsing, or
coverage for graphic frames or SmartArt. A fresh wheel also profiled an
independently maintained Excel-style drawing part with seven free connectors
and no coverage warning; the validation record
has the exact artifact hash and source reference.

FormulaFence 0.59.0 guards legacy Excel Custom Views. A workbook-level
customWorkbookView can bind a named alternate display or print mode to
GUID-linked customSheetView declarations across the workbook, hiding rows or
columns, retaining filters, changing print settings, panes, comments, or object
visibility without an ordinary cell change. FormulaFence reads raw transitional
and Strict worksheet, dialog-sheet, and chart-sheet declarations before the
ordinary reader can discard them. A material change emits FF060;
no_custom_workbook_view_changes adds FFP060. Profiles expose structural
counts only: view names, GUIDs, sheet bindings, filter criteria, ranges, print
settings, pane locations, and XML remain private. Coordinated GUID and sheet-ID
rewrites stay quiet, while malformed, unsupported, incomplete, or over-budget
state is explicit coverage evidence. This is stored-declaration comparison, not
view activation/rendering, filtered-result calculation, print pagination, or
future-extension interpretation. The scope follows the Open XML
customWorkbookView
and
customSheetView
definitions. A clean packaged wheel emitted exactly FF060 and FFP060 for a
private Custom View filter-only change without exposing the filter or view name;
the validation record
has the exact artifact hash.

FormulaFence 0.60.0 makes Excel Table Style controls reviewable. A table can
keep its cells, formulas, range, and structured references while a
tableStyleInfo binding changes its headers, totals, banding, or emphasized
columns; a workbook-local custom style can change its differential formats; and
Table or TableColumn Dxf and named-cell-style bindings can change table-local
presentation. FormulaFence reads those raw styles.xml and table declarations
before ordinary readers flatten them, resolves Dxf-ID rewrites semantically, and
emits FF061; no_table_style_control_changes adds FFP061. Profiles expose
only structural counts: table identities, style names, differential formats,
colours, and XML remain private. The raw Table Style inventory accepts both
transitional and Strict SpreadsheetML parts, while malformed, unresolved,
unsupported, or bounded controls become explicit coverage evidence. This is
stored-declaration
comparison, not final Excel rendering, theme resolution, conditional-format
application, PivotTable-only style regions, or a claim about what a reviewer
will see on a particular client. A fresh packaged wheel emitted exactly FF061
and FFP061 for a private Table Style toggle-only change without exposing the
style name or Dxf colour; the validation record
has the external fixtures and clean-install evidence.

FormulaFence 0.61.0 makes legacy shared-workbook revision history reviewable.
Old shared workbooks can retain revision headers and logs outside the worksheet
grid: prior values, author identities, timestamps, comments, formatting records,
conflict-resolution material, and tracking/retention/protection controls.
FormulaFence follows workbook-to-header and header-to-log relationships and
compares complete bounded declarations privately. A material history,
relationship, control, or coverage change emits FF062; the
no_shared_workbook_revision_changes policy adds FFP062. Profiles retain
only structural header/log and control counts: historic values, locations,
authors, timestamps, comments, GUIDs, relationship IDs, and XML stay private.
Header-only and header-plus-log packages plus Strict SpreadsheetML are
supported; malformed, unsafe, duplicate, unresolved, unsupported, oversized,
or bounded material becomes visible coverage evidence. This is stored-audit
comparison, not revision application, historic-state reconstruction, conflict
resolution, identity validation, or Excel rendering. A fresh packaged wheel
emitted exactly FF062 and FFP062 for a private historic-log-only change
without exposing seeded historic values or the author identity; the
validation record
has the external fixture and clean-install evidence.

Install the exact 0.133.0 wheel with:

python -m pip install https://github.com/SybilGambleyyu/formulafence/releases/download/v0.133.0/formulafence-0.133.0-py3-none-any.whl
Enter fullscreen mode Exit fullscreen mode

For every changed cell, it follows statically visible A1-style, ordinary named-range, safely expandable formula-defined-name and named LAMBDA, direct dynamic-array spill anchors, fixed legacy-CSE outputs, currently observed dynamic-array output members, LET/inline-LAMBDA, supported table, and direct 3-D worksheet dependencies and reports downstream formula cells with deterministic shortest-path samples.

formulafence check approved.xlsx candidate.xlsx \
  --policy formulafence.yml \
  --format sarif --output results.sarif
Enter fullscreen mode Exit fullscreen mode

The policy is plain YAML. It can protect an output cell, limit routine edits to an input range, ban formula overrides, and cap downstream impact. It does not replace judgment; it makes a review expectation visible and repeatable.

version: 1
rules:
  no_formula_to_value: true
  no_new_external_links: true
  no_new_broken_references: true
  no_new_parser_warnings: true
  no_new_unresolved_references: true
  no_new_dynamic_references: true
  no_new_spill_references: true
  no_new_dynamic_array_output_references: true
  no_new_implicit_intersections: true
  no_array_formula_semantics_changes: true
  no_new_tokenization_failures: true
  no_table_definition_changes: true
  no_data_validation_changes: true
  no_conditional_formatting_changes: true
  no_protection_changes: true
  no_external_data_connection_changes: true
  no_external_link_package_changes: true
  no_xlm_macro_sheet_changes: true
  no_ribbon_customization_changes: true
  no_office_web_addin_changes: true
  no_chart_definition_changes: true
  no_pivot_table_definition_changes: true
  no_slicer_timeline_cache_changes: true
  no_power_pivot_data_model_changes: true
  no_what_if_data_table_changes: true
  no_scenario_manager_changes: true
  no_filter_visibility_changes: true
  no_formula_cached_result_changes: true
  no_rich_text_run_changes: true
  no_cell_hyperlink_changes: true
  no_worksheet_sparkline_changes: true
  no_xml_mapping_changes: true
  no_rich_data_changes: true
  no_legacy_comment_changes: true
  no_worksheet_drawing_shape_changes: true
  no_ignored_error_changes: true
  no_named_sheet_view_changes: true
  no_number_format_changes: true
  no_cell_font_changes: true
  no_cell_fill_changes: true
  no_workbook_theme_changes: true
  no_cell_alignment_changes: true
  no_cell_border_changes: true
  no_worksheet_display_control_changes: true
  no_worksheet_print_layout_changes: true
  no_worksheet_embedded_control_changes: true
  no_power_query_changes: true
  no_3d_reference_scope_changes: true
  max_downstream_impact: 100

protected_cells:
  - Dashboard!B12
Enter fullscreen mode Exit fullscreen mode

Fail closed when the analysis has a blind spot

FormulaFence 0.10.0 resolves ordinary workbook and sheet-local names with static A1 destinations, 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; no_table_definition_changes can make that a hard boundary.

It 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, a diff emits FF014; no_3d_reference_scope_changes can make that a hard boundary. This follows Excel's documented 3-D-reference behavior. External, malformed, endpoint-missing, and non-A1 variants remain explicit 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.

Version 0.8.0 removes a common modern false gap: LET names and inline LAMBDA parameters are local variables, not workbook names. FormulaFence follows their lexical scope—including nested lambdas supplied to functions such as REDUCE—and retains the actual A1, named, and table inputs around them. It follows Excel's documented LET scope and LAMBDA parameter syntax, without evaluating either.

Version 0.9.0 follows a named LAMBDA only when its whole definition is static and internal. A caller such as =ToCelsius(A2) retains its A2 edge and gains the function body's static inputs; nested named functions and formula-defined names that call one work the same way. It respects workbook and worksheet-local scope, and recognizes both ordinary Name Manager text and the _xlfn.LAMBDA/_xlpm./_xlop. OOXML spelling emitted by Excel-compatible writers. That is the same named-function model described in Excel's LAMBDA documentation. Recursive, dynamic, relative, external, 3-D, and tokenizer-unsupported definitions remain explicit coverage notes rather than guessed graph edges.

Version 0.10.0 traces the anchor behind a direct internal spill reference such as =SUM(A1#) or its stored OOXML form =SUM(_xlfn.ANCHORARRAY(A1)). The anchor edge captures changes to its formula and visible upstream inputs; profiles retain the spill token, and FF015/no_new_spill_references let CI make the partial boundary explicit. It does not invent a dynamic spill extent or dependency on every possible blocker, so external, 3-D, range, named, implicit-intersection, and malformed spill forms stay visible limits. A spill-bearing formula-defined name remains unresolved at its caller. This follows Excel's spilled-range operator, while recognizing the OOXML spelling used by Excel-compatible writers. A formula the underlying tokenizer cannot inspect is now listed by location and emits FF016; no_new_tokenization_failures can make that a CI boundary too.

Version 0.11.0 makes the adjacent implicit-intersection boundary visible. Excel’s @ operator can reduce a range or array to one value, and Excel-compatible files persist the unusual mixed cases as _xlfn.SINGLE(...). When that wrapper contains one direct static A1 cell or range with an unambiguous row/column intersection, FormulaFence adds only the selected cell edge: =_xlfn.SINGLE(Inputs!B2:B4) in row 3 depends on Inputs!B3, not every cell in the range. It records literal @A1:A3, @ applied to function results, and stored SINGLE() forms; a new use emits FF017, and no_new_implicit_intersections can require review. Function results retain their visible static inputs without being evaluated, and context-dependent formula-defined names remain unresolved. This is distinct from table [@Column] syntax. The scope follows Microsoft’s implicit-intersection guidance, its Formula versus Formula2 documentation, and XlsxWriter’s OOXML guidance.

Version 0.12.0 closes a quieter graph gap in old but still material models. A legacy CSE formula can declare a fixed result range such as B1:B3, while the formula itself is stored only at B1. A downstream =B2*10 or =SUM(B2:B3) therefore looks disconnected unless the tool understands the result members. FormulaFence keeps that fixed range compact and links its anchor to known formulas that read its members, so input changes reach those consumers without creating one virtual node per output cell. It identifies dynamic arrays through the workbook’s metadata rather than mistaking a current spill range for a fixed CSE range. Unrecognized metadata remains a coverage warning rather than a guess. The release also emits FF018 when array mode or a fixed CSE output range changes; no_array_formula_semantics_changes makes that a CI boundary. The behavior follows Microsoft’s distinction between dynamic and legacy arrays and XlsxWriter’s documented storage forms.

Version 0.13.0 closes the corresponding modern gap without pretending a dynamic spill is fixed. A dynamic-array anchor’s OOXML range is a current observation, not a promise about the next recalculation: Excel can grow, shrink, or block the spill. When a static formula currently reads a non-anchor member such as B2, FormulaFence now adds a compact anchor-to-consumer edge and records both the observed range and consumer in the profile. That lets an input change reach =B2*10 and =SUM(B2:B3) through the dynamic anchor, without materializing every spill cell. A new observed output-member relationship emits FF019; no_new_dynamic_array_output_references turns it into the fail-closed FFP019 policy boundary. The implementation was checked against an independently maintained XlsxWriter 3.2.9 workbook and keeps a declared B1:XFD1048576 range compact.

Version 0.14.0 makes data-entry controls reviewable too. A worksheet validation can restrict a whole column, source a list from another sheet, show an input prompt, or block an invalid entry; all of those can materially change a model’s operating boundary without altering a formula cell. FormulaFence now inventories compact target ranges and effective validation settings, normalizes equivalent OOXML defaults, criterion spelling, and target grouping so compatible writers do not create noise, and emits FF020 for a real change. Profiles redact validation formulas and prompt/error text; the local change report preserves full before/after evidence. Teams can make the surface fail closed with no_data_validation_changes (FFP020). It deliberately does not evaluate a validation formula or predict whether Excel will accept an entry. The behavior was checked against an independently maintained XlsxWriter workbook and follows Microsoft’s data-validation guidance.

Version 0.15.0 makes visual exception controls reviewable too. Conditional formatting determines the colors, badges, and warnings a reviewer sees; moving a rule in worksheet-wide precedence, toggling Stop If True, changing a threshold, or changing a data bar can alter that signal without changing an ordinary formula cell. FormulaFence reads those rules directly from OOXML, resolves differential styles instead of comparing unstable dxfId values, and retains opaque Excel-2010 extension fragments when a reader library cannot model them. It normalizes equivalent defaults, leading = spelling, priority-number gaps, and known extension-link GUIDs, then emits FF021 for a real change. Profiles redact criteria and raw visual XML; the local report keeps full before/after evidence. Teams can make the boundary fail closed with no_conditional_formatting_changes (FFP021). It does not evaluate a rule or predict Excel’s final rendered display. The behavior was checked against Microsoft’s public conditional-formatting examples and an independently maintained XlsxWriter Excel-2010 data-bar fixture.

Version 0.16.0 extends that review surface to operational protection. A workbook can keep its formulas intact while losing a structure lock, allowing a protected input to become editable, hiding a formula differently, or changing who may edit a protected range. FormulaFence now reads workbook, worksheet, dialog-sheet, and chart-sheet protection directly from OOXML; records protected-range target areas; and inventories sparse direct locked/hidden assignments without expanding whole rows or columns. It normalizes equivalent worksheet-action defaults and emits FF022 for a real change; no_protection_changes makes that a fail-closed FFP022 boundary. Password verifiers, hashes, salts, protected-range names, and security descriptors never enter a profile or report—only safe presence metadata and private change fingerprints do. This is deliberately not a claim that Excel protection is encryption, authentication, or a full access-control system. The behavior was checked against Excel-produced public protection examples and controlled protected-range and chart-sheet fixtures.

Version 0.17.0 closes another quiet merge-boundary gap: a workbook can change its inputs when it opens or while it is in use, without a normal formula edit. FormulaFence now reads workbook-wide link and refresh flags, connection schedules and background/cache/credential settings, linked query-table refresh behavior, and pivot-cache source/refresh controls directly from OOXML. A changed control emits FF023; no_external_data_connection_changes makes it fail closed as FFP023. The tool does not connect or refresh anything. It also never serializes connection names, paths, URLs, connection strings, commands, parameter values, SSO identifiers, cached records, or opaque extension XML: safe metadata and private change fingerprints let a review see that the boundary moved without leaking the source.

Version 0.18.0 brings the embedded Power Query package into that same boundary. A workbook’s Data Mashup can contain M formula documents and metadata that determine how query output reaches a worksheet or data model. FormulaFence reads the package structure directly, keeps M code and raw package values behind private fingerprints, and exposes only safe structural counts and control flags in profiles. A changed query formula or semantic control emits FF024; no_power_query_changes makes it fail closed as FFP024. It does not evaluate M, connect to a source, or expose query text, source locations, credentials, identifiers, or raw embedded data.

Version 0.19.0 closes the adjacent raw-package gap. A workbook can redirect an external workbook, DDE, or OLE link through xl/externalLinks/externalLink*.xml without changing a formula. FormulaFence inventories those parts, privately binds the workbook declaration to its package part, and fingerprints source targets, definitions, cache material, item behavior, and opaque extensions. A material change emits FF025; no_external_link_package_changes makes it fail closed as FFP025. It never follows or executes a link, and it never emits a target, workbook sheet or defined name, DDE service/item, OLE program, or cached value.

Version 0.20.0 closes a different macro blind spot that a VBA hash cannot see. Excel 4.0/XLM automation lives in raw Macro Sheet XML rather than vbaProject.bin, and ordinary workbook readers can retain the tab while omitting its executable cells. FormulaFence now reads the documented Macro Sheet and International Macro Sheet package parts before that happens. It privately compares macro XML, workbook bindings, and related relationships, emits FF026 for a material change, and lets CI make the boundary fail closed with no_xlm_macro_sheet_changes (FFP026). Commands, values, targets, and embedded-object contents stay out of profiles and reports; FormulaFence does not execute, emulate, or resolve any of them.

Version 0.21.0 closes the next package gap inside that boundary. An XLM macro sheet can keep the same relationship target while the bytes of its internal OLE object or embedded package change. FormulaFence now streams each direct, safe internal target into a private fingerprint without parsing its payload or following external targets. A byte-only change still emits FF026; the profile exposes only how many internal parts were fingerprinted or left uninspected. The scan is deliberately bounded to 32 MiB per part, 64 MiB per workbook, and 256 parts. Hitting a bound is a visible coverage warning, not a quiet pass.

Version 0.22.0 closes an adjacent UI-automation blind spot. Office RibbonX custom UI can bind a tab, button, or menu to a workbook callback while ordinary cells and vbaProject.bin stay unchanged. FormulaFence now reads the documented Ribbon Extensibility and Ribbon/Backstage root-package forms and customUI XML, privately fingerprints the complete control/callback definition and direct relationships, and emits FF027 for a material change. no_ribbon_customization_changes turns that into the fail-closed FFP027 CI boundary. Relationship-ID-only rewrites and equivalent internal target spellings are normalized away; malformed, unbound, oversized, or over-budget parts become visible coverage warnings. Callback names, IDs, labels, XML, and image targets remain private. The tool does not execute callbacks, follow external relationships, or parse image payloads.

Version 0.23.0 closes a document-linked add-in blind spot. A workbook can declare an Office Web Add-in task pane, bind it to a Web Extension definition, and request auto-show behavior without changing an ordinary cell, vbaProject.bin, or RibbonX control. FormulaFence follows the workbook-to-task-pane-to-extension OOXML chain, privately fingerprints configuration, references, auto-show properties, bindings, snapshots, and direct relationship semantics, and emits FF028 for a material change. no_office_web_addin_changes makes that a fail-closed FFP028 boundary. Add-in IDs, store references, property and binding values, XML, snapshots, and relationship targets remain private; relationship-ID-only rewrites and equivalent internal target spellings are normalized away. The tool does not install, fetch, or execute an add-in or manifest, and task-pane and Web Extension XML reads are bounded to 16 MiB per part, 32 MiB per workbook, and 64 parts. Unrecognized worksheet-extension and graphic-frame forms remain explicit coverage limits.

Version 0.24.0 closes the adjacent worksheet-control blind spot. A worksheet can bind <control> and <oleObject> markup to persisted ActiveX state, form-control properties, an embedded OLE/package payload, or an external OLE link without changing a normal cell or vbaProject.bin. FormulaFence follows that relationship-backed chain, privately fingerprints control/OLE declarations, ActiveX ocx persistence, form-control bindings, related relationships, and bounded direct ActiveX/OLE/package payload hashes, then emits FF029 for a material change. no_worksheet_embedded_control_changes makes that a fail-closed FFP029 boundary. Control names, class IDs, licenses, macros, formulas/ranges, OLE identities, targets, XML, and payload bytes remain private; relationship-ID rewrites and equivalent internal target spellings are normalized away. The tool never initializes ActiveX, deserializes an OLE object/package, follows an external target, or infers control event dispatch. Relevant XML is bounded to 16 MiB per part, 64 MiB per workbook, and 512 parts; direct payload hashes are bounded to 32 MiB per part, 64 MiB per workbook, and 512 parts. VML/drawing layout and embedded payload formats remain explicit coverage limits. The package boundary follows Microsoft's sheet ActiveX guidance and ocx persistence schema.

Version 0.25.0 closes the legacy VML form-control gap within that same FF029 boundary. A worksheet can point to a VML drawing whose non-comment ClientData records carry a macro assignment, linked cell, source range, camera range, or a relationship to presentation material—even while ordinary cells, vbaProject.bin, and modern <controls> markup remain unchanged. FormulaFence follows standard vmlDrawing relationships, privately fingerprints those non-Note controls and directly referenced relationship semantics, and reports only safe counts. FmlaMacro, FmlaLink, FmlaGroup, FmlaTxbx, FmlaRange, and FmlaPict values, captions, target paths, and VML XML stay private. Relationship-ID rewrites and equivalent internal target spellings are normalized away; malformed, missing, orphaned, oversized, and over-budget material becomes a visible coverage warning. Ordinary VML comment notes are intentionally excluded. The scanner never renders VML, reads a note into the control profile, evaluates a binding, executes a macro, or decodes a related image. The boundary follows Microsoft's ClientData documentation and its notes on FmlaMacro and FmlaRange.

Version 0.26.0 closes the report-layer chart gap. A workbook can leave its cell grid untouched while a worksheet or chartsheet drawing points to a changed chart type, series definition, cached series value, title, formatting, or userShapes overlay. FormulaFence follows the standard drawing → c:chart → overlay chain, privately compares non-cache chart definitions separately from numeric/string caches, and fingerprints bounded direct related payloads without exposing formulas, labels, cached values, shape text, targets, XML, or payload bytes. A material change emits FF030; no_chart_definition_changes makes it a fail-closed FFP030 boundary. Writer-chosen relationship IDs and equivalent internal target spellings are normalized away, while malformed, orphaned, unbound, oversized, and over-budget material becomes a visible coverage warning. The scanner does not calculate a series, render a chart, map a chart into the cell-impact graph, follow an external target, parse media/package formats, or interpret chartEx/nested-chart semantics. The boundary follows the OOXML Chart Part, the documented number-reference cache, and the chart user-shapes relationship.

Version 0.27.0 closes the adjacent PivotTable report-definition gap. A workbook can retain its ordinary grid while a PivotTable changes its binding, field layout, filter, cache schema, shared items, cache-record relationship, or cached record material. FormulaFence follows the workbook → PivotTable → cache-definition → cache-records chain directly from OOXML, privately fingerprints those surfaces, and emits FF031 for a material change. no_pivot_table_definition_changes makes that a fail-closed FFP031 boundary. Cache source and refresh controls intentionally remain under FF023, so a refresh-only edit stays distinguishable from a report-definition change. Relationship IDs, equivalent internal targets, and cache-ID renumbering are normalized away; malformed, missing, oversized, or over-budget parts become visible coverage warnings. The reader uses a temporary sanitized copy only when necessary so raw cache records are not eagerly materialized by the workbook library; the supplied workbook is never modified. It does not refresh a cache, calculate a PivotTable, render a report, follow an external target, or expose source data, field names, cached values, XML, targets, or payload bytes. The boundary follows the OOXML Pivot Table Part, Pivot Cache Definition, and Cache Records package forms.

Version 0.28.0 closes the adjacent interactive-filter gap. A Slicer can alter a cached item selection that filters a PivotTable or Excel table, and a Timeline can alter a cached date state for a PivotTable, without changing an ordinary cell. FormulaFence follows documented workbook extension declarations through explicit workbook relationships to bounded cache XML, privately comparing cache definitions, item selections, Timeline state/filter material, source bindings, filtered-PivotTable bindings, and unexpected direct cache relationships. A material change emits FF032; no_slicer_timeline_cache_changes makes it a fail-closed FFP032 boundary. Relationship IDs, equivalent internal target spellings, coordinated Slicer/Timeline PivotCache extension-ID renumbering, known optional Slicer defaults, Boolean spellings, and Timeline GUIDs are normalized away. Names, selected values, date ranges, targets, and XML stay private. The scanner does not apply a filter, calculate or render a PivotTable/table, infer downstream impact, fetch an external target, or model worksheet/drawing Slicer or Timeline view geometry and styles. The boundary follows Microsoft's Slicer Cache part, Timeline Cache part, and Slicer-to-PivotCache relationship.

Version 0.29.0 closes the adjacent embedded-model gap. An Excel workbook can carry a Power Pivot/Data Model with tables, relationships, calculations, and stored data outside the cell grid. FormulaFence follows the explicit powerPivotData workbook relationship and x15:dataModel declaration, privately fingerprints that declaration and bounded raw xl/model/*.data bytes, and emits FF033 for a material change. no_power_pivot_data_model_changes makes it a fail-closed FFP033 boundary. Table/column/relationship names, connection details, DAX, stored values, targets, XML, and payload bytes remain private. Relationship IDs, equivalent internal target spellings, and writer GUIDs are normalized; malformed, orphaned, external, direct-related, oversized, or over-budget material produces a coverage warning. Raw payload reads are bounded to 512 MiB per part, 512 MiB per workbook, and 16 parts. FormulaFence does not deserialize the Analysis Services payload, evaluate DAX, refresh a model, calculate or render a report, infer model-to-cell impact, or fetch an external target. The boundary follows Microsoft's PowerPivot Model guidance and the Open XML x15:dataModel declaration.

Version 0.30.0 makes Excel What-If Data Tables reviewable. A Data Table stores an f t="dataTable" master declaration that defines its output range, one- or two-variable form, orientation, input cells, and recalculation request. FormulaFence reads that OOXML directly, compares its structural semantics behind a private fingerprint, and emits FF034 for a material change. no_what_if_data_table_changes makes it a fail-closed FFP034 boundary. It also treats generated Data Table cells as a stable =TABLE() form, avoiding false self-diffs from an underlying reader object’s memory address. Output ranges and input references stay private; malformed, deleted-input, or overlapping declarations become visible coverage warnings. FormulaFence does not calculate scenarios or infer their results: ordinary cached scenario cells remain ordinary cell diffs. The boundary follows the OOXML Data Table Formula form and Excel’s What-If Analysis guidance.

Version 0.31.0 protects the adjacent Scenario Manager boundary. A Scenario Manager worksheet stores alternate named input-value sets that Excel can apply later, outside ordinary cells. FormulaFence reads raw OOXML <scenarios> declarations before the workbook reader can omit them, then privately compares selected/shown state, result-summary references, names, locked/hidden flags, comments/users, stored changing-cell values and references, deleted/undone state, and display formats. A material change emits FF035; no_scenario_manager_changes makes it a fail-closed FFP035 boundary. Equivalent local A1, Boolean, unsigned-integer, and default-flag spellings stay quiet, while malformed or unsupported declarations become visible coverage warnings. Scenario names, comments, users, values, and references never leave the local private signature. FormulaFence does not show or apply a scenario, calculate its result, or infer a scenario-to-formula dependency. The boundary follows OOXML scenarios and Excel’s Scenario Manager guidance.

Version 0.32.0 closes a common report-view blind spot. An AutoFilter criterion, table filter, sort state, manually hidden row, outline/collapsed row, or sheet-level zeroHeight default can change which records a reviewer sees—and which values a vertical SUBTOTAL includes—without an ordinary cell or formula diff. FormulaFence reads those raw OOXML controls, privately fingerprints their semantics, and emits FF036; no_filter_visibility_changes makes it a fail-closed FFP036 boundary. The public profile exposes structural counts only: criteria, selected values, custom sort lists, table names, sort keys, and row/range references remain private. Equivalent local A1, Boolean/default, and unsigned-integer spellings, plus filter-member ordering, are normalized; malformed or unsupported controls and unsafe table relationships become coverage warnings. FormulaFence does not apply a filter, calculate a result, infer formula visibility sensitivity, render a report, or model column widths, styles, or outline-display settings. The boundary follows Microsoft’s SUBTOTAL documentation and the OOXML autoFilter, filterColumn, and sortState forms.

Version 0.33.0 makes Excel’s hidden warning decisions reviewable. A per-range ignored-error declaration can silence evaluation, inconsistent-formula, omitted-range, unlocked-formula, empty-reference, list-validation, calculated-column, text-number, or two-digit-year warnings without an ordinary cell or formula diff. FormulaFence reads standard <ignoredErrors> and Office 2010 x14:ignoredErrors controls from raw OOXML, privately compares target ranges and enabled flags, and emits FF037; no_ignored_error_changes makes it a fail-closed FFP037 boundary. Profiles expose only container, target, and warning-kind counts, while target ranges and exact suppressions stay private. Equivalent local A1, Boolean, and target-order spellings are normalized; malformed or unsupported controls become coverage warnings. FormulaFence does not determine whether Excel would display a warning, calculate or repair an error, change application-level error checking, or infer downstream impact. The boundary follows the OOXML ignoredError form and Microsoft’s ignoredErrors extension documentation.

Version 0.34.0 closes the next report-view gap: modern Excel Named Sheet Views are relationship-backed saved views that can retain alternate AutoFilter criteria and sort rules without changing an ordinary cell, formula, or the active filter. FormulaFence follows those parts, privately compares saved-view material, and reconciles each filter using Excel’s documented AutoFilter UID, table-ID, then worksheet-owned fallback sequence. A material change emits FF038; no_named_sheet_view_changes makes it a fail-closed FFP038 boundary. Profiles expose only parts, views, alternate-filter, column-filter, criterion, sort, and coverage counts—never view names, IDs, criteria, target ranges, table bindings, table-column IDs, or sort keys. Equivalent GUID, local A1, Boolean/default, and unsigned-integer spellings remain quiet; missing, ambiguous, mismatched, malformed, unsafe, oversized, or unsupported metadata is a visible coverage warning. FormulaFence does not activate or render a saved view, calculate a filtered result, repair metadata, infer formula visibility sensitivity, or interpret differential-format, extension, or rich-sort semantics. The boundary follows Microsoft’s Named Sheet Views part, CT_NsvFilter, and reconciliation definitions.

Version 0.35.0 closes the remaining report-view gap: a hidden, outlined, or collapsed column can alter what a reviewer sees without changing an ordinary cell or formula. FormulaFence reads raw <cols>/<col> controls, applies overlapping declarations in file order so only later present visibility attributes override prior state, and privately fingerprints the effective result. A material change stays under FF036; no_filter_visibility_changes remains the fail-closed FFP036 boundary. Profiles expose only hidden/outlined/collapsed column counts—not column positions, ranges, XML, criteria, sort keys, or other private report structure. Equivalent Boolean/default, unsigned-integer, and range-segmentation spellings stay quiet; malformed, unsupported, or over-budget controls become coverage warnings. FormulaFence does not render outlines, apply filters, calculate a result, track widths/styles, or model outline-display settings. The boundary follows the OOXML cols and col definitions.

Version 0.36.0 closes a display-control gap: a number format can make an unchanged value appear blank with ;;;, scale it with commas, or show it as a percentage, date, text, or private literal. FormulaFence reads raw custom <numFmt> records, base and effective style XFs (including xfId/applyNumberFormat inheritance), direct cell styles, customFormat=1 rows, and column style defaults. A material change emits FF039; no_number_format_changes makes it a fail-closed FFP039 boundary. Codes, style IDs, and targets stay private; custom ID remapping, Boolean spelling, base-XF inheritance, and equivalent column-range splitting stay quiet. Missing definitions, invalid references, and malformed controls become coverage warnings. This is static declaration comparison, not locale-specific rendering, format validation, value calculation, width/overflow behavior, or arbitrary visual-style tracking. The boundary follows Excel’s custom number-format guidance and the OOXML xf form.

Version 0.37.0 closes the next display-control gap: a font can make an unchanged value or warning less visible, for example with white text against a matching background, or materially alter its face, size, emphasis, underline, or effects. FormulaFence reads raw <fonts> definitions, base and effective style XFs (including xfId/applyFont inheritance), direct cell styles, customFormat=1 rows, and column style defaults. A material change emits FF040; no_cell_font_changes makes it a fail-closed FFP040 boundary. Font names, colour values, effects, style IDs, and targets stay private; font-ID remapping, common Boolean spellings, font-child ordering, base-XF inheritance, and equivalent column-range splitting stay quiet. Missing definitions, invalid references, and malformed controls become coverage warnings. This is static declaration comparison, not theme-colour resolution, rendering, background/fill contrast, rich-text behavior, table styling, width/overflow, or arbitrary visual-style tracking. The boundary follows the OOXML xf form and the ICAEW’s spreadsheet-review guidance, which explicitly calls out white-on-white font use.

Version 0.38.0 closes the other half of that review-surface gap: a fill can make unchanged text, a warning, or an input/output cue less visible, for example by changing a matching background to a solid, patterned, or gradient fill. FormulaFence reads raw <fills> definitions, including patternFill and gradientFill stops, base and effective style XFs (including xfId/applyFill inheritance), direct cell styles, customFormat=1 rows, and column style defaults. A material change emits FF041; no_cell_fill_changes makes it a fail-closed FFP041 boundary. Fill colours, pattern and gradient material, style IDs, and targets stay private; fill-ID remapping, valid pattern-child ordering, common Boolean spellings, semantically inert no-fill and solid-background declarations, base-XF inheritance, and equivalent column-range splitting stay quiet. Missing definitions, invalid references, and malformed controls become coverage warnings. This is static declaration comparison, not theme-colour resolution, pattern or gradient rendering, text/background contrast, conditional-format differential styles, table styling, width/overflow, or arbitrary visual-style tracking. The boundary follows the OOXML xf, patternFill, and gradientFill forms, alongside the ICAEW’s spreadsheet-review guidance to reveal text hidden by formatting.

Version 0.39.0 closes a more direct visibility gap: Excel documents a row or column dimension of zero as hidden, so a workbook can remove unchanged cells from a reviewer’s surface with row@ht="0" or col@width="0", even without a hidden flag. FormulaFence extends its existing filter, sort, and visibility boundary to raw row height, column width, and sheetFormatPr default row/column dimensions when they are zero. It resolves layered col declarations in file order, including a later positive width that reveals an inherited zero-width column. A material change still emits FF036; the existing no_filter_visibility_changes policy gives a fail-closed FFP036 boundary. Profiles expose only zero-height-row, zero-width-column, and default-zero counts: raw dimensions, targets, and declarations stay private. Equivalent zero spellings and inherited zeros stay quiet; ordinary positive resizing stays out of scope. Malformed, negative, non-finite, and out-of-range dimensions become coverage warnings. This follows Microsoft’s documented zero-size-as-hidden behavior and OOXML’s row, col, and sheetFormatPr forms. It does not render worksheets, decide whether near-zero dimensions obscure content, model overflow or print layout, or track arbitrary positive layout changes.

Version 0.40.0 closes a result-integrity gap that is easy to miss in manual-calculation workbooks: SpreadsheetML can store the last displayed result beside unchanged formula text. FormulaFence reads raw <f> and <v> elements together, privately fingerprints numeric, string, Boolean, and error results, and emits FF042 when a saved result changes without a changed formula at that cell or a statically visible ordinary-cell precedent. no_formula_cached_result_changes makes that a fail-closed FFP042 policy boundary. Profiles expose only formula/cached/missing/type/malformed counts; result values, error text, digests, and locations stay private. Equivalent finite numeric and Boolean spellings stay quiet, while malformed metadata becomes a coverage warning. This is review evidence, not an attempt to calculate Excel or decide whether a result is stale, tampered, volatile, dynamic, external, or mathematically wrong. The scope follows Microsoft's formula OOXML guidance, the Open XML c cell, and Excel's calculation-mode guidance.

Version 0.41.0 closes a character-level display gap: SpreadsheetML can hold a cell's text in shared or inline rich-text runs, so the same concatenated text can hide a warning when a run's presentation changes. FormulaFence compares raw <r>/<rPr> controls and phonetic metadata from both storage forms, emits FF043 for a material change, and lets no_rich_text_run_changes make it a fail-closed FFP043 boundary. Profiles expose only rich-text cell/run/phonetic counts; text, colours, locations, shared-string indexes, and raw XML remain private. Equivalent property ordering, Boolean spelling, and shared-versus-inline storage do not create a finding; malformed or unsupported metadata becomes a coverage warning. This is static declaration comparison, not Excel rendering, theme resolution, contrast, visibility, font/fill interaction, or phonetic display. The boundary follows Microsoft's shared-string guidance and the Open XML rich-text run model.

Version 0.42.0 closes the parallel non-cell display gap: an Excel worksheet can keep a warning in a DrawingML xdr:sp text box whose text, anchors, geometry, visual declaration, macro/text link, or click/hover relationship changes independently of any cell. FormulaFence follows the worksheet-to-drawing relationship, compares regular shapes and nested xdr:grpSp groups privately, and emits FF044 for a material control change; no_worksheet_drawing_shape_changes makes it a fail-closed FFP044 boundary. Profiles expose only structural worksheet/drawing/anchor, shape/text/group, text paragraph/run, macro/text-link/hyperlink, relationship, and malformed-control counts. Text, colours, geometry, descriptions, macro names, formulas, relationship IDs, targets, and XML stay private; non-visual IDs, relationship-ID rewrites, and colour-case noise stay quiet. The scope follows the Open XML xdr:sp shape model and the documented XlsxWriter text-box surface.

It now 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.

One practical safeguard is coverage visibility. When the workbook parser encounters an OOXML extension it cannot fully interpret, FormulaFence records a coverage note. A candidate that adds one can be rejected with no_new_parser_warnings. Profiles also list unresolved range tokens, dynamic reference functions, spill references, explicit implicit intersection, dynamic and unclassified array anchors, observed dynamic output-member relationships, What-If Data Table, Scenario Manager, filter/sort/row and column visibility, ignored-error, Named Sheet View, cell number-format, cell-font, cell-fill, effective cell-alignment, effective cell-border, material worksheet-display and worksheet print-layout, worksheet-cell hyperlink, and worksheet-sparkline control counts, and formulas the tokenizer could not inspect; a change can be rejected with no_new_unresolved_references, no_new_dynamic_references, no_new_spill_references, no_new_dynamic_array_output_references, no_new_implicit_intersections, no_array_formula_semantics_changes, no_data_validation_changes, no_conditional_formatting_changes, no_protection_changes, no_external_data_connection_changes, no_external_link_package_changes, no_xlm_macro_sheet_changes, no_ribbon_customization_changes, no_office_web_addin_changes, no_chart_definition_changes, no_pivot_table_definition_changes, no_slicer_timeline_cache_changes, no_power_pivot_data_model_changes, no_what_if_data_table_changes, no_scenario_manager_changes, no_filter_visibility_changes, no_ignored_error_changes, no_named_sheet_view_changes, no_number_format_changes, no_cell_font_changes, no_cell_fill_changes, no_workbook_theme_changes, no_cell_alignment_changes, no_cell_border_changes, no_worksheet_display_control_changes, no_worksheet_print_layout_changes, no_cell_hyperlink_changes, no_worksheet_sparkline_changes, no_worksheet_embedded_control_changes, no_power_query_changes, or no_new_tokenization_failures.

Formula-cache profiles also expose only formula/cached/missing/type/malformed counts. A cache-only change yields FF042, while no_formula_cached_result_changes adds FFP042 in CI; raw results, error text, digests, and locations stay out of Markdown, JSON, and SARIF.

Rich-text profiles expose only shared/inline rich-text cell and run counts plus phonetic metadata counts. A material presentation-control change yields FF043, while no_rich_text_run_changes adds FFP043 in CI; raw text, colours, locations, indexes, and XML stay out of Markdown, JSON, and SARIF.

Worksheet-cell hyperlink profiles expose only safe worksheet/hyperlink, location/display/ScreenTip, relationship/external-relationship, and malformed-metadata counts. A material stored declaration change yields FF047, while no_cell_hyperlink_changes adds FFP047; targets, references, locations, display strings, ScreenTips, identifiers, and XML remain out of Markdown, JSON, and SARIF.

Worksheet-sparkline profiles expose only safe worksheet/group/sparkline, source/date-axis-source, colour-control, and malformed-metadata counts. A material stored declaration change yields FF048, while no_worksheet_sparkline_changes adds FFP048; source formulas, destination cells, group controls, colours, and XML remain out of Markdown, JSON, and SARIF.

SpreadsheetML XML Map profiles expose only safe map/schema/data-binding, file/connection, mapped-table, single-cell, and malformed-metadata counts. A material stored declaration or relationship change yields FF049, while no_xml_mapping_changes adds FFP049; schemas, map names, XPath expressions, table identities, target cells, connection identities, relationship targets, and XML remain out of Markdown, JSON, and SARIF.

Digital-signature profiles expose only aggregate package-origin, XML-signature, reference, embedded-certificate, certificate-part, VBA-signature, and malformed-metadata counts. A material stored declaration or relationship change yields FF050, while no_digital_signature_changes adds FFP050; signature XML, references, digests, certificate payloads, relationship IDs, and binary signature data remain out of Markdown, JSON, and SARIF.

Worksheet DrawingML profiles expose only safe regular/connector/group shape, anchor, connector-attachment, text-run, macro/text-link/hyperlink, and relationship counts. A material supported shape or connector declaration change yields FF044, while no_worksheet_drawing_shape_changes adds FFP044; raw text, presentation, anchors, macro/formula material, identifiers, targets, and XML remain out of Markdown, JSON, and SARIF.

The visibility inventory includes zero-sized row and column controls alongside filters, sorts, manually hidden dimensions, and outline state; the existing fail-closed visibility policy covers all of those static declarations.

Rich-data profiles expose only safe part, rich-value, structure, linked-entity-structure, array, supporting-property-bag, metadata-binding, bound-cell, web-image, relationship, external-reference, and malformed-metadata counts. A material stored definition, value, binding, or relationship change yields FF051, while no_rich_data_changes adds FFP051; entity values, provider data, field names, identifiers, URLs, image references, relationship IDs, bound-cell locations, and XML remain out of Markdown, JSON, and SARIF. FormulaFence reads raw rich-data parts and their package/workbook relationships before normal readers normalize them. Equivalent relationship IDs/order and internal target spelling stay quiet; malformed, unsafe, unreadable, oversized, or over-budget metadata becomes visible coverage evidence. It compares stored controls only: it does not contact providers, refresh values, calculate formulas, fetch relationship targets, validate target content, or infer Excel client behavior.

Custom-data-store profiles expose only aggregate custom XML, custom XML-property, schema-reference, relationship, custom-data-property, custom-data, and document-custom-property counts. A material stored declaration, opaque payload, or relationship change yields FF052, while no_custom_data_store_changes adds FFP052; XML, schemas, property names and values, storage identities, binary payloads, relationship IDs, and targets remain out of Markdown, JSON, and SARIF. FormulaFence compares Custom XML item IDs and Custom Data IDs privately, while normalizing relationship IDs and document-property PIDs; malformed, unsafe, unbound, unreadable, oversized, or over-budget metadata becomes coverage evidence. It does not execute an add-in, refresh or interpret its state, fetch a target, or infer Excel client behavior.

Workbook Theme profiles expose only aggregate Theme-part, colour-scheme,
font-scheme, format-scheme, relationship/external-relationship, image, and
malformed-metadata counts. A material stored control or direct Theme-image
payload change yields FF053, while no_workbook_theme_changes adds
FFP053; Theme XML, scheme names, colour values, font names, image
payloads, relationship IDs, and targets remain out of Markdown, JSON, and
SARIF. FormulaFence reads raw workbook bindings, transitional and strict Theme
XML, and direct Theme-image relationships before ordinary readers flatten the
surface. Writer-selected relationship IDs/order and equivalent internal target
spelling stay quiet; malformed, unsafe, unbound, unreadable, oversized, or
over-budget metadata becomes visible coverage evidence. It compares stored
controls only: it does not resolve effective styles, render a workbook,
calculate contrast, decode images, fetch targets, or infer Excel client
behavior.

Cell-alignment profiles expose only aggregate default-definition, direct-cell,
row, effective-cell, column, and malformed-metadata counts. A material stored
effective control change yields FF054, while
no_cell_alignment_changes adds FFP054; alignment values,
style IDs, and locations remain out of Markdown, JSON, and SARIF. FormulaFence
resolves base/cell XF inheritance and direct cell, row, and column assignments
before comparison. Equivalent defaults, boolean/integer spelling, inert
mergeCell, inheritance, and column-range splitting stay quiet;
malformed metadata is visible coverage evidence. It compares stored declarations,
not rendered layout or visibility.

Test beyond toy files

Unit fixtures are necessary, but an Office-file reader also needs to meet real workbooks. I validated FormulaFence against the public Foresight Cap Table and Exit Waterfall Tool: 18 sheets, 6,623 non-empty cells, and 4,228 formula cells.

The inspection found an unsupported OOXML extension and recorded it as a structured coverage note instead of leaking a raw dependency warning into CI output. It also identified 36 cells using INDIRECT, making the model’s dynamic-reference surface explicit. On a local, non-distributed copy, replacing one exit-waterfall formula with a number traced 330 downstream formula cells; the starter policy rejected both the formula override and the impact limit.

I also checked 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.

For the Power Query path, I validated against a public .xlsm workbook with a real Data Mashup package and a query table reached through a table relationship. FormulaFence identified the embedded formula document, semantic metadata, refresh controls, and linked query table. On a local, non-distributed copy, changing only the M formula produced FF024; the test marker did not appear in profile, report, Markdown, or SARIF output.

For external-link packages, I profiled Apache POI’s public ref2-56737.xlsx fixture: two real external-workbook parts, five external sheets, four defined names, five cached sheets, seven cached cells, and one cached refresh error. On a local, non-distributed copy, changing only a relationship target produced FF025; the target marker did not appear in the profile, report, Markdown, or SARIF output.

For XLM macro sheets, I used a controlled, harmless OOXML package shaped from the documented macro-sheet relationships and content types. SheetJS Community Edition recognized the macro-sheet tab while the ordinary Python reader exposed zero formula cells. FormulaFence’s raw preflight reported the hidden macro sheet, its two formula cells, and related relationships without a VBA payload. Changing private command material, a related target, and visibility produced FF026; neither commands nor targets appeared in profile, Markdown, JSON, or SARIF output. This demonstrates static package comparison and redaction, not XLM execution or Excel runtime behavior.

For RibbonX, I used a controlled raw-OOXML package with a root custom-UI declaration, a tab/group/button, onLoad and onAction callbacks, and an image relationship. Changing only the private onAction callback produced FF027 even though the safe control and callback counts were unchanged. The synthetic callback, labels, XML, and target did not appear in profile, Markdown, JSON, or SARIF output. The suite also covers Office 2010-era roots, image-target changes, relationship-ID rewrites, malformed parts, and size/count budgets. This is static package comparison and redaction, not Office UI rendering or callback execution.

For Office Web Add-in task panes, I used a controlled raw-OOXML package shaped from the documented workbook relationship, task-pane binding, and Web Extension definition. Changing only Office.AutoShowTaskpaneWithDocument produced FF028 even though the safe task-pane counts were unchanged. Synthetic add-in IDs, store references, properties, bindings, snapshots, and relationship targets did not appear in profile, Markdown, JSON, or SARIF output. The suite also covers both task-pane reference spellings, relationship-ID rewrites, equivalent target spellings, malformed definitions, and size/count budgets. This is static package comparison and redaction, not add-in installation, manifest retrieval, Office.js execution, or task-pane rendering.

For worksheet embedded controls and OLE objects, I used a controlled raw-OOXML package with an ActiveX ocx part and raw binary target, a form-control-properties part, an embedded OLE payload, an external OLE link, and an ActiveX presentation-image relationship. A byte-only OLE payload change produced FF029 without leaking control names, macros, class/license material, formulas/ranges, targets, or payload markers into profile, Markdown, JSON, or SARIF output. The suite also covers mc:AlternateContent, relationship-ID and target-spelling noise, malformed ActiveX XML, and per-part/aggregate byte and part limits. This validates static package comparison and redaction, not control initialization, embedded-object deserialization, rendering, or event execution.

For legacy VML controls, I used a separate controlled package with a worksheet-to-vmlDrawing relationship, a Button macro, a Drop linked to a cell and range, a GBox group link, a Pict camera range, a presentation relationship, and an adjacent Note comment. Changing only the private VML control material emitted FF029; changing only the note emitted no control finding. Macro names, formulas/ranges, captions, note text, image targets, XML, Markdown, JSON, and SARIF all remained redacted. The suite also covers relationship-ID and target-spelling churn, malformed roots, and XML bounds. This is static package comparison and data minimisation—not VML rendering, comment interpretation, macro execution, formula evaluation, image decoding, or event behavior.

For DrawingML charts, I started with a generated bar chart and added controlled numeric/string cache data plus a userShapes overlay and image relationship. Definition, cache-only, overlay, direct payload, and external-target changes each produced FF030 without leaking formulas, cached values, titles, shape text, targets, XML, or payload markers. The suite also covers a chartsheet chain, relationship-ID and internal-path normalization, malformed chart XML, and XML/payload budgets. This validates static package comparison and redaction—not chart calculation, rendering, source retrieval, media decoding, or visual correctness.

For PivotTables, I used a controlled relationship-backed package with a table binding, layout fields, cache definition, shared items, cache-record relationship, and raw records. Binding, layout, cache-schema, shared-item, relationship, and record-only changes each produced FF031, while refresh-only changes remained under FF023; relationship-ID, equivalent-target, and cache-ID noise did not create a finding. The suite also proves the original package bytes remain unchanged and forces the workbook library’s raw-record parser to fail if called, demonstrating that inspection does not eagerly load the cache records. Private field data, record markers, targets, XML, Markdown, JSON, and SARIF stayed redacted.

For Slicer and Timeline caches, I used a controlled raw-OOXML package with both documented workbook Slicer-cache container forms, Pivot-backed and table-backed Slicers, and a Pivot-backed Timeline state. Changing only private selected-item and date-state material produced FF032 and FFP032, while relationship-ID rewrites, equivalent internal targets, optional-default spelling, Timeline GUIDs, and a coordinated PivotCache extension-ID renumbering stayed quiet. The packaged CLI also profiled the unmodified Slicer workbook generated by upstream Excelize: four real cache parts, 13 items, 11 selections, and no FormulaFence cache coverage warning. Names, source fields, selected values, date ranges, targets, XML, Markdown, JSON, and SARIF stayed redacted. This validates static package comparison and compatibility, not Excel filter application or rendering.

For embedded Power Pivot/Data Models, I used a controlled package with an x15:dataModel declaration, private table/relationship material, an explicit powerPivotData binding, and a harmless opaque item.data payload. Declaration-only and byte-only payload changes produced FF033 / FFP033, while relationship-ID rewrites, equivalent paths, and regenerated Data Model GUIDs stayed quiet. The packaged CLI also profiled Microsoft's public Customer Profitability Excel sample: one model payload and binding, nine model tables, eight model relationships, and no Data-Model coverage warning. Names, relationship details, DAX, stored values, targets, XML, Markdown, JSON, and SARIF stayed redacted. This validates static package comparison and compatibility—not Analysis Services deserialization, DAX evaluation, refresh, report rendering, or model-to-cell impact analysis.

For What-If Data Tables, the packaged CLI profiled the independently maintained Witan two-variable sensitivity fixture with a clean self-diff: one two-variable table, 25 declared outputs, and a requested recalculation. Controlled declaration, input-reference, and output-overlap changes produce FF034 / FFP034, while absolute-reference and case-only rewrites stay quiet. This validates static declaration comparison and the absence of a reader-memory-address artifact—not scenario calculation or output prediction.

For Scenario Manager, the packaged CLI profiled a public six-scenario workbook linked by the Journal of Accountancy: one Scenario Manager worksheet, 18 stored inputs, six locked scenarios, and no Scenario Manager coverage warning. A stored-value-only change in a local temporary copy emitted FF035 / FFP035 without exposing the replacement value. This validates static declaration comparison and redaction—not scenario application, formula calculation, or Excel rendering.

For filters and row visibility, the packaged CLI profiled the public XlsxWriter autofilter.py example generated locally with XlsxWriter 3.2.9: seven worksheet AutoFilters, seven filter columns and criterion groups, 163 explicitly hidden rows, and no visibility-control coverage warning. A controlled criterion-only change—one that FormulaFence 0.31.0 does not report—emitted only FF036 / FFP036 in 0.32.0, without exposing the criterion. This validates static declaration comparison and data minimisation, not filter application, recalculation, SUBTOTAL/AGGREGATE correctness, formula-sensitivity inference, or rendering.

For column visibility, the fresh packaged CLI profiled XlsxWriter 3.2.9’s public outline_collapsed.py example generated locally at commit cf3fe78d3eab5e4c7d825d4451af3a60e2a04011: six hidden columns, twelve outlined columns, one collapsed marker, and no visibility-control coverage warning. A controlled hidden-column-only change—one that FormulaFence 0.34.0 reports as zero changes—emitted exactly FF036 / FFP036 in 0.35.0, without exposing a column range or raw XML. The suite also checks overlapping width and visibility declarations, equivalent range splitting, malformed bounds, and output redaction. This validates static declaration comparison and data minimisation, not Excel rendering, formula recalculation, filter application, width/style handling, or outline-display behavior.

For number formats, the fresh packaged CLI profiled XlsxWriter 3.2.9’s public tutorial2.py example generated locally: five direct custom-format assignments and no coverage warning. Changing only the first money cell’s format to ;;;—an edit the public 0.35.0 wheel reports as zero changes—emitted exactly FF039 / FFP039 in 0.36.0 without exposing the code, style ID, or target. The suite also checks custom-ID remapping, XF inheritance, column-range splitting, malformed definitions, and output redaction. This validates static declaration comparison and data minimisation, not Excel’s locale-specific rendering, width/overflow, formula calculation, or arbitrary visual formatting.

For cell fonts, the fresh packaged CLI profiled XlsxWriter 3.2.9’s public tutorial2.py example generated locally: one default font definition, three direct font assignments, and no coverage warning. Changing only the first money cell’s font colour to white—an edit the public 0.36.0 wheel reports as zero changes—emitted exactly FF040 / FFP040 in 0.37.0 without exposing a font name, colour, style ID, or target. The suite also checks font-ID remapping, child ordering, XF inheritance, column-range splitting, malformed definitions, and output redaction. This validates static declaration comparison and data minimisation, not Excel’s theme-colour resolution, rendering, background/fill contrast, formula calculation, or arbitrary visual formatting.

For cell fills, the fresh packaged CLI profiled XlsxWriter 3.2.9’s public tutorial2.py example generated locally: no fill assignments and no coverage warning. Changing only the first money cell’s fill to black solid—an edit the public 0.37.0 wheel reports as zero changes—emitted exactly FF041 / FFP041 in 0.38.0 without exposing a fill colour, pattern, gradient, style ID, or target. The suite also checks fill-ID remapping, pattern-child ordering, semantically inert no-fill and solid-background declarations, gradient direction, XF inheritance, column-range splitting, malformed definitions, and output redaction. This validates static declaration comparison and data minimisation, not Excel’s theme-colour resolution, fill rendering, text/background contrast, conditional-format differential styles, formula calculation, or arbitrary visual formatting.

For ignored-error controls, the packaged CLI profiled XlsxWriter’s public ignore_errors.py example generated locally with XlsxWriter 3.2.9: one standard container, two suppression rules and target ranges, one evaluation-error and one text-number warning kind, and no ignored-error coverage warning. A controlled target-only change—one that FormulaFence 0.32.0 does not report—emitted FF037 / FFP037 in 0.33.0 without exposing the target. This validates static declaration comparison and data minimisation, not warning eligibility or display, error repair, application-level settings, recalculation, or rendering.

For Named Sheet Views, the fresh packaged CLI profiled LibreOffice’s public NamedSheetViews.xlsx fixture: one relationship-backed part, two saved views/two alternate filters, two column filters and criterion groups, two sort rules/conditions, and no coverage warning. A controlled saved-view criterion-only change—one that FormulaFence 0.33.0 reports as zero changes—emitted exactly FF038 / FFP038 in 0.34.0 without exposing the view name, criterion, target, binding, or sort key. The suite also exercises the table-ID reconciliation fallback. This validates static declaration comparison and data minimisation, not saved-view activation/rendering, filtered-result calculation, metadata repair, or differential-format/extension/rich-sort behavior.

For the 0.21 related-part boundary, the same controlled package included two internal payloads and one external linked object. A byte-only change to the internal OLE payload, with the macro XML and relationship target unchanged, produced FF026 with no payload bytes or paths in JSON, Markdown, or SARIF. The tests also force the per-part, aggregate-byte, and part-count limits to verify that each becomes a clear coverage warning rather than a hidden blind spot.

Those results are a compatibility demonstration, not a claim that the source model is correct. The full limits and validation record are in the FormulaFence repository.

For zero dimensions, the fresh packaged CLI profiled XlsxWriter 3.2.9’s public tutorial2.py example generated locally. Adding only an effective width="0" declaration to its money column—without a hidden flag or ordinary cell/formula edit—is a zero-change result in the public 0.38.0 wheel but emitted exactly FF036 / FFP036 in 0.39.0. The suite covers direct zero-height/zero-width declarations, zero worksheet defaults, layered positive overrides, ordinary positive resizes, equivalent zero spellings, malformed numeric declarations, and output redaction. This validates static zero-dimension comparison, not Excel rendering, near-zero visibility, overflow, print layout, formula calculation, or arbitrary layout tracking.

For stored formula results, I used the same public XlsxWriter 3.2.9 tutorial2.py workbook with manual calculation and full-recalculation-on-load/save disabled. A controlled raw-package mutation changed only the saved result beside an unchanged SUM(B2:B5) formula. The public 0.39.0 wheel returned zero changes; the fresh 0.40.0 wheel emitted exactly FF042, and the starter policy added FFP042. The report was checked not to reveal the result values, formula text, or formula-cell coordinate. This validates saved-cache comparison and data minimisation, not formula calculation, cache provenance, or volatile/dynamic/external behavior.

For rich-text runs, I built a controlled shared-string package where a warning's concatenated cell text stayed fixed while its second run changed from opaque black to opaque white. The public 0.40.0 wheel returned zero changes; the fresh 0.41.0 wheel emitted exactly FF043, and the starter policy added FFP043. The report was checked not to reveal the warning text, colours, shared-string index, or cell coordinate. The suite also checks property-order and Boolean normalization, equivalent shared/inline storage, styled-boundary movement with unchanged text, ordinary text-only edits, malformed metadata, and output redaction. This validates stored presentation-declaration comparison, not Excel rendering, theme resolution, contrast, visibility, or phonetic display.

For Worksheet DrawingML shapes, I generated a controlled XlsxWriter 3.2.9 text-box workbook where all cells and warning text stayed fixed while only the stored text-run colour in xl/drawings/drawing1.xml changed from black to white. The public 0.41.0 wheel returned zero changes; the fresh 0.42.0 wheel emitted exactly FF044, and the starter policy added FFP044. The report was checked not to reveal the text-box text, colours, non-visual name, or relationship ID. The suite also covers regular/group inventory, hyperlink-only changes, relationship and non-visual-ID normalization, malformed XML, budgets, and output redaction. This validates stored regular-shape declaration comparison, not DrawingML rendering, theme/contrast/visibility, text-link calculation, macro execution, target retrieval, media, or other drawing objects.

For modern threaded comments, I built a controlled standard SpreadsheetML pair from a clean workbook, keeping ordinary cells, person records, thread/reply structure, timestamps, and mention bindings fixed while changing only one reply body in the threaded-comment part. The public 0.42.0 wheel returned zero changes; the fresh 0.43.0 wheel emitted exactly FF045, and the starter policy added FFP045. The report was checked not to reveal comment text, a cell reference, a timestamp, an email-like identity, or raw GUIDs. The suite also checks schema-correct mentions, consistent ID rewrites, unsafe relationships, malformed parts, scan budgets, and output redaction. This validates static stored-declaration comparison and privacy boundaries, not collaboration behavior, notification delivery, rendering, account lookup, or cloud state.

For legacy Excel Notes and threaded placeholders, I built a controlled standard SpreadsheetML comments/VML pair, keeping ordinary cells and the rest of the Note declaration fixed while changing only the stored Note body. The public 0.43.0 wheel returned zero changes; the fresh 0.44.0 wheel emitted exactly FF046, and the starter policy added FFP046. Only xl/comments/comment1.xml changed in the package. The report was checked not to reveal the Note text, author, or cell reference. The suite also covers Note edits, VML visibility-only changes, threaded-placeholder reconciliation, regenerated identifiers, unsafe relationships, malformed XML, scan budgets, and output redaction. This validates private static declaration comparison, not Note/VML rendering, permission resolution, notification delivery, account lookup, or cloud state.

For worksheet-cell hyperlinks, I built a controlled workbook pair where the friendly value, style, and ScreenTip stayed fixed and only the external target in xl/worksheets/_rels/sheet1.xml.rels changed. The public 0.44.0 wheel returned zero changes; the fresh 0.45.1 wheel emitted exactly FF047, and the policy added FFP047. The report was checked not to reveal either target, the ScreenTip, a cell reference, or a relationship ID. The suite also covers standard and Office 2016 revision declarations, target/location/display/ScreenTip-only edits, identifier rewrites, unbound relationships, malformed references, XML budgets, and reader-overlay composition. This validates private static declaration comparison, not target retrieval, redirect following, link availability, reputation, or client rendering.

For worksheet sparklines, I built a controlled XlsxWriter 3.2.9 workbook pair with a real Office 2010 line sparkline, marker, axis, custom min/max, date-axis, and colour controls. Ordinary cells stayed fixed and only xl/worksheets/sheet1.xml changed: one stored source range moved to another row. The public 0.45.1 wheel reported zero changes; the fresh 0.46.0 wheel emitted exactly FF048, and the policy added FFP048. The report was checked not to reveal either source, the date-axis source, output cell, or colour. The suite also covers source/style-only changes, normalization, malformed metadata, XML budgets, redaction, and composition with hyperlink reader isolation. This validates private static declaration comparison, not Excel calculation, sparkline rendering, source retrieval, or accessibility assessment.

For XML Maps, I built a controlled OpenPyXL 3.1.5 and raw-ZIP workbook pair with a real map/schema/data binding, XML table-column property, single-cell XML table, and the required workbook/worksheet relationships. Ordinary cells, formulas, and every uncompressed package member except the mapped table field stayed fixed. The public 0.46.0 wheel returned zero changes; the fresh 0.47.0 wheel emitted exactly FF049, and the policy added FFP049. The report was checked not to reveal the schema, map name, XPath, table identity, target cell, connection identity, or relationship target. The suite also covers mapping and refresh changes, relationship rebinding, equivalent spelling, unsafe/malformed metadata, bounded reads, redaction, and ordinary-table isolation. This validates private static declarations, not XML import/export, schema/data validation, binding access, refresh, or Excel rendering.

For digital-signature controls, I built an independent raw-ZIP workbook pair with a package signature origin, XMLDSIG envelope, linked certificate part, and classic, Agile, and V3 VBA signature blobs. Ordinary cells and formulas stayed fixed; only the linked certificate payload changed. The public 0.47.0 wheel returned zero changes; the fresh 0.48.0 wheel emitted exactly FF050, and policy added FFP050. The report was checked not to reveal a digest, signature value, certificate payload, relationship ID, or reference URI. The suite also covers package/VBA add, remove, and change cases; relationship rebinding; normalization; duplicate members; malformed or unsafe metadata; bounded reads; and profile redaction. This validates private static signature-control comparison, not any cryptographic, certificate, trust, or signer assertion.

For rich data, I profiled the independently maintained openxlsx-data richData_datatypes.xlsx fixture. It exposed one each of the data, structure, type, array, supporting-property-bag, supporting-property-bag-structure, style, and web-image parts; 362 rich values; 10 structures; 6 linked-entity structures; 20 arrays; 4 property bags; 12 value-metadata bindings and bound cells; 6 web images; 12 external web-image relationship references; and no coverage warning. In a local raw-ZIP candidate, I changed only xl/richData/rdrichvalue.xml. The public 0.48.0 wheel returned zero changes; the fresh 0.49.0 wheel emitted exactly FF051, and the policy added FFP051. The report was checked not to reveal an entity value, provider field, URL, image reference, relationship ID, or bound-cell location. The suite also covers value, metadata-binding, and web-image-relationship changes; equivalent relationship spelling; malformed metadata; bounded reads; redaction; and ordinary-workbook isolation. This validates private static stored-control comparison, not provider contact, refresh, target retrieval, content validation, or Excel client behavior.

For custom data stores, I profiled the independently maintained Open XML SDK NoExtDataE6.xlsx fixture: two custom XML items, two property parts, six schema references, two relationships, and two document custom properties, with no coverage warning. In a local raw-ZIP candidate, ordinary cells stayed equal and only customXml/item2.xml changed. A fresh 0.50.0 wheel emitted exactly FF052, and policy added FFP052, without exposing custom XML, schema material, property values, storage IDs, or relationship details. The suite also covers custom binary payloads, identity changes, relation-ID/PID normalization, malformed metadata, bounded reads, Power Query isolation, and output redaction. This validates private static stored-control comparison, not add-in execution, refresh, target retrieval, payload interpretation, or Excel client behavior.

For workbook Themes, I profiled the independently maintained Open XML SDK
Blank.xlsx fixture:
one workbook-bound Theme with one colour, font, and format scheme and no
coverage warning. In a local raw-ZIP candidate, ordinary cells and formulas
stayed fixed and only xl/theme/theme1.xml changed. A fresh 0.51.0
wheel emitted exactly FF053, and the starter policy added FFP053,
without exposing either Theme colour, the member name, or a relationship ID.
The same wheel recognized the Open XML SDK’s strict-OOXML
2D Rotation-O12-XL-OartEffects.xlsx fixture
and emitted the same finding for its Theme-only mutation. The suite separately
covers direct image payloads, relationship-ID normalization, malformed
metadata, bounded reads, redaction, and policy enforcement. This validates
private static stored-control comparison, not effective-style resolution or
rendering.

For effective cell alignment, I profiled the independently maintained Open XML
SDK Styles.xlsx fixture.
In a local raw-ZIP candidate, ordinary cells and formulas stayed fixed and only
xl/styles.xml changed. A fresh 0.52.0 wheel emitted exactly
FF054, and the starter policy added FFP054, without
exposing changed alignment values, style IDs, or target cells. The suite covers
direct, row, column, and default styles; xfId/applyAlignment;
equivalent spelling; inert mergeCell; malformed metadata; redaction;
and policy enforcement. This validates stored effective-declaration comparison,
not rendered layout or visibility.

For worksheet-display controls, I used the same Open XML SDK
Styles.xlsx fixture: a raw-ZIP candidate kept ordinary cells and formulas fixed and changed only one sheetView declaration to hide zeroes. The fresh 0.53.0 wheel emitted exactly FF055, and the policy added FFP055, without exposing a sheet name, view target, or raw control. The same packaged wheel repeated that result on the Open XML SDK’s strict-OOXML 2D Rotation-O12-XL-OartEffects.xlsx fixture. The suite covers hidden zeroes/formulas/gridlines, custom gridline colours, headers, outline symbols, rulers, page whitespace, direction, view modes, panes, equivalent spelling/navigation noise, malformed metadata, redaction, and policy enforcement. This validates private stored-view declarations, not rendered Excel layout or client behavior.

For worksheet print layout, I again used the Open XML SDK
Styles.xlsx fixture
and the strict-OOXML
2D Rotation-O12-XL-OartEffects.xlsx fixture.
In each raw-ZIP candidate, ordinary cells and formulas stayed fixed and exactly
one worksheet XML member changed: a stored left page margin. The fresh 0.54.0
wheel emitted exactly FF056, and the policy added FFP056, without exposing
the margin value, raw control name, worksheet member, or printer relationship
ID. The transitional file already carried a printer-settings coverage gap; the
release keeps that distinct from a genuine layout change. The suite also covers
areas/titles, print options, margins, page setup/fit-to-page, headers/footers,
manual breaks, equivalent spelling, no-op normalization, malformed controls,
redaction, and policy enforcement. This validates private stored print
declarations, not rendered pagination.

For effective cell borders, I used the independently maintained Open XML SDK
Styles.xlsx fixture
and the strict-OOXML
2D Rotation-O12-XL-OartEffects.xlsx fixture.
In each raw-ZIP candidate, ordinary cells and formulas stayed fixed and only
xl/styles.xml changed. The fresh 0.55.0 wheel emitted exactly FF057, and
the policy added FFP057, without exposing a changed border colour, border
index, line style, target, or package-member name. The suite covers direct,
row, column, default, inherited, logical-side, diagonal, outline, equivalent
spelling, malformed, redaction, and policy cases. This validates private stored
border declarations, not final Excel border rendering.

For worksheet dimensions, I generated the transitional baseline from
XlsxWriter's public autofit.py example
and used the Open XML SDK's strict-OOXML
2D Rotation-O12-XL-OartEffects.xlsx fixture.
For each, a local raw-ZIP candidate kept ordinary cells, formulas, and every
other package member fixed while changing one stored worksheet dimension. A
fresh 0.56.0 wheel emitted exactly FF058, and the policy added FFP058,
without exposing a dimension, target, raw member name, baseline adjustment, or
source cell value. The XlsxWriter profile recognized its four effective
AutoFit/width columns; the strict profile recognized its three Office 2010
baseline-adjustment sheets without a coverage gap. The suite also covers
default/direct sizing, layered ranges, strict namespaces, automatic
thick-border adjustments, equivalent spelling, malformed metadata, reader
isolation, redaction, and policy enforcement. This validates private stored
dimension declarations, not final Excel layout or pagination.

What it does not claim

FormulaFence does not calculate a saved formula result, decide whether it is
stale or tampered, or establish its mathematical correctness.

FormulaFence does not calculate Excel or prove a financial model correct. Material models still need a qualified owner, recalculation in the approved spreadsheet engine, and independent review. Relative, cyclic, external, and tokenizer-unsupported name definitions, unsupported or ambiguous table syntax, spill extents and blocking cells, non-static named LAMBDAs, arbitrary custom functions, complex implicit-intersection expressions, XLM execution and runtime dependencies, RibbonX callback execution and image payloads, Office Web Add-in manifest retrieval, runtime behavior, and unrecognized worksheet-extension and graphic-frame forms, chart series calculation/rendering/input-impact mapping, external-target retrieval, media/package parsing, chartEx/nested-chart semantics, PivotTable refresh/calculation/rendering and downstream cell-impact mapping, AutoFilter application, visibility-sensitive formula inference, final rendered row/column layout, AutoFit calculation, wrapped/merged-cell overflow, automatic page geometry, final border rendering/theme or palette resolution/adjacent-cell precedence, alignment, rich-text runs, final Table Style rendering and its conditional-format interaction, and outline-display settings, number-format rendering/validation/locale semantics, cell-font rendering/theme-colour/contrast semantics, cell-fill rendering/theme-colour/contrast semantics, ignored-error warning eligibility/display, error repair, application-level error checking, Named Sheet View activation/rendering, filtered-result calculation, metadata repair, differential-format/extension/rich-sort semantics, Slicer/Timeline filter application and worksheet/drawing view geometry/styles, Power Pivot/Data Model deserialization, DAX evaluation, refresh, report rendering, and model-to-cell impact, What-If Data Table calculation and scenario-output prediction, Scenario Manager application/result calculation/dependency inference, worksheet ActiveX/OLE initialization, embedded-package deserialization, form-control event dispatch, VML/Note rendering and image payloads, and event behavior beyond the static Note/VML declarations, Power Query evaluation and downstream dependencies beyond its formula/control boundary, external-link execution and downstream dependencies, and features such as INDIRECT remain coverage limits rather than guessed graph edges.

The 0.41 boundary narrows the stored rich-text part of that list: it compares character-level run presentation and phonetic declarations, but it does not render the workbook, resolve themes, determine contrast or visibility, or decide how Excel will display phonetic text.

The 0.42 boundary narrows the worksheet-drawing part of that list: it compares stored regular xdr:sp and nested xdr:grpSp declarations, but does not render DrawingML, resolve theme/contrast/visibility, calculate a text link, execute a macro assignment, retrieve a target, or inspect pictures, connectors, graphic frames, SmartArt, media, or other non-regular drawing objects.

The 0.43 boundary narrows the modern-comment part of that list: it compares stored threaded-comment/person package declarations privately, but does not render comments, resolve people or permissions, send notifications, retrieve cloud state, or decide client visibility. At that release, it did not yet inspect legacy Notes or placeholders.

The 0.44 boundary closes that stored legacy-annotation gap: it compares Note/comments/VML and threaded-placeholder declarations privately, emitting FF046 and FFP046 under no_legacy_comment_changes, but does not render Notes or VML, decide visual visibility in an Excel client, resolve people or permissions, send notifications, retrieve cloud state, or determine whether a placeholder is rendered.

The 0.45 boundary adds ordinary worksheet-cell hyperlink declarations and their selected relationship semantics, emitting FF047 and FFP047 under no_cell_hyperlink_changes, but it does not render, resolve, fetch, follow, or reputation-check a target; inspect linked content; or infer redirect, trust-zone, or client behavior.

The 0.46 boundary adds Office 2010 worksheet-sparkline declarations, emitting FF048 and FFP048 under no_worksheet_sparkline_changes, but it does not calculate, render, resolve, fetch, or assess the accessibility of a sparkline source.

The 0.47 boundary adds SpreadsheetML XML Map declarations, emitting FF049 and FFP049 under no_xml_mapping_changes, but it does not import/export XML, validate XML data or schemas, open bindings, fetch data, calculate refresh results, or infer Excel client behavior.

But a review process should at least make it hard to silently replace a formula with a number. That is the narrow, useful boundary FormulaFence is built to enforce.

The 0.48 boundary adds package and VBA digital-signature-control declarations, emitting FF050 and FFP050 under no_digital_signature_changes, but it does not cryptographically validate XMLDSIG references, transforms, digests, signatures, certificate trust/identity/validity, timestamps, signed-content coverage, or VBA-code coverage.

The 0.49 boundary adds Excel rich-data declarations, emitting FF051 and FFP051 under no_rich_data_changes, but it does not contact a provider, refresh a value, calculate a formula, fetch or validate a relationship target, resolve an entity, or infer Excel client behavior.

The 0.50 boundary adds persisted custom workbook data stores, emitting FF052 and FFP052 under no_custom_data_store_changes, but it does not execute add-ins, refresh or interpret their state, fetch a target, or infer Excel client behavior.

The 0.51 boundary adds stored workbook Theme controls, emitting FF053 and
FFP053 under no_workbook_theme_changes, but it does not resolve
effective styles, render cells/charts/drawings, calculate contrast, decode an
image, fetch a target, or infer Excel client behavior.

The 0.52 boundary adds effective cell-alignment controls, emitting FF054
and FFP054 under no_cell_alignment_changes; it resolves
stored alignment inheritance but does not calculate width/height, merged layout,
overflow, final visibility, visual-control composition, or Excel rendering.

The 0.53 boundary adds material worksheet-display controls, emitting FF055
and FFP055 under no_worksheet_display_control_changes; it compares stored
views privately but does not render a workbook, resolve an effective palette
colour, calculate viewport geometry/final visibility, or infer Excel client
behavior.

The 0.54 boundary adds material worksheet print-layout controls, emitting
FF056 and FFP056 under no_worksheet_print_layout_changes; it compares
stored print declarations privately but does not render or preview Excel,
calculate page geometry/counts or automatic pagination, resolve printer/client
defaults or printer-specific devMode settings, or cover custom/legacy
sheet-view and extension print controls.

The 0.55 boundary adds effective cell-border controls, emitting FF057 and
FFP057 under no_cell_border_changes; it compares stored border declarations
privately but does not resolve theme/palette colours, choose adjacent-cell
precedence, render final styles, apply conditional/table/differential borders,
calculate print output, or infer Excel client behavior.

The 0.56 boundary adds material worksheet-dimension controls, emitting FF058
and FFP058 under no_worksheet_dimension_changes; it compares saved sizing,
AutoFit, baseline-adjustment, and automatic-thick-border declarations privately
but does not calculate final layout, AutoFit, wrapping/merged overflow, or
automatic pagination.

The 0.57 boundary adds native worksheet pictures, backgrounds, and header/footer watermarks, emitting FF059 and FFP059 under no_worksheet_image_changes; it compares stored bindings, declarations, and bounded payload hashes privately but does not decode/render media, fetch a target, or calculate final layout or pagination.

The 0.58 boundary extends FF044 / FFP044 to stored xdr:cxnSp connector declarations and endpoint attachments, including strict DrawingML and group-contained connectors; it does not route or render a connector or resolve final visual behavior.

The 0.59 boundary adds legacy Custom View declarations, emitting FF060 and
FFP060 under no_custom_workbook_view_changes; it compares saved alternate
display/filter/print state privately but does not activate or render the view,
calculate its filtered result, or determine final print output.

The 0.60 boundary adds stored Excel Table Style declarations, emitting FF061
and FFP061 under no_table_style_control_changes; it compares applied
bindings, custom definitions, Dxf references, and named cell-style bindings
privately but does not render a final Table appearance, resolve themes, or apply
conditional formatting.

The 0.61 boundary adds legacy shared-workbook revision headers and logs,
emitting FF062 and FFP062 under no_shared_workbook_revision_changes; it
compares historic audit declarations, relationship bindings, and
tracking/retention/protection controls privately but does not apply revisions,
reconstruct historic state, resolve conflicts, or render Excel.

The 0.62 boundary extends FF044 / FFP044 to bounded non-chart Worksheet DrawingML SmartArt graphic frames. It compares the private diagram data, layout, quick-style, colour, and direct rendering parts referenced by dgm:relIds, while chart frames stay in FF030 and native pictures stay in FF059. Unknown frame types, malformed component bindings or roots, and component-side relationships outside the bounded scan become coverage evidence rather than silent omissions. It supports transitional and Strict DrawingML, compares stored declarations rather than rendering SmartArt, and keeps diagram content, anchors, IDs, and relationship targets out of reports.

The 0.63 boundary extends FF030 / FFP030 to Office 2016+ ChartEx worksheet charts. It recognizes cx:chart graphic frames in Excel mc:AlternateContent, keeps the older-client fallback from becoming a second worksheet control, and privately fingerprints ChartEx XML plus bounded direct style, colour-style, drawing, image, theme-override, and embedded-package targets. Profiles expose only aggregate counts; relationship-ID rewrites and equivalent internal target spellings stay quiet. Malformed, unsupported, external, unsafe, orphaned, oversized, and over-budget material becomes coverage evidence. It compares stored package material, not formula calculation, chart rendering, visual truthfulness, payload formats, or second-hop relationships.

The 0.64 boundary extends FF044 / FFP044 to direct internal Image targets from a SmartArt Diagram Data part. It fingerprints bounded stored image bytes—32 MiB per image, 64 MiB per workbook, and 512 images—while profiles retain only safe counts. It does not decode or render media, fetch a target, or follow hyperlinks, second-hop targets, or relationships from the other SmartArt component kinds.

The 0.65 boundary extends FF028 / FFP028 to documented worksheet x15:webExtensions bindings and active in-content DrawingML we:webextensionref frames. It verifies local appRef bindings and direct definition relationships while keeping formulas, IDs, and XML private. An inactive native-picture fallback remains a normal bounded worksheet-image boundary (FF059) rather than being double-counted or lost. Malformed, unbound, oversized, and over-budget evidence stays visible; the tool does not retrieve manifests, execute Office.js, or render an add-in.

The 0.66 boundary adds a package-wide external-relationship ledger. It checks every canonical OPC root or part-level .rels member for TargetMode="External", including opaque remote endpoints outside a known workbook feature. A material endpoint, type, source, or coverage change emits FF063; no_external_relationship_changes adds FFP063. Profiles retain only relationship part/source/target and hyperlink/image/other counts, while source paths, types, identifiers, targets, and raw XML remain private. Relationship-ID-only rewrites stay quiet; malformed, orphaned, duplicate, unreadable, oversized, and over-budget metadata remains visible coverage evidence. The scanner is bounded to 16 MiB per part, 64 MiB per workbook, and 512 parts, and it never resolves, opens, fetches, or trusts a target. A fresh wheel was exercised against independent Open XML SDK hyperlink and external-link workbooks; the validation record has the checksums and redaction evidence.

The 0.67 boundary closes a different gap: an action can live entirely in a formula, with no worksheet hyperlink declaration or external relationship. FormulaFence now inventories stored HYPERLINK, WEBSERVICE, IMAGE, and RTD calls, including Excel’s _xlfn. compatibility spelling. A material call, argument, provider, or same-count destination change emits FF064; no_formula_external_action_changes adds FFP064. It also raises the boundary when an ordinary source-cell edit reaches an action formula through the static dependency graph, so =HYPERLINK(A9, ...) cannot silently retarget through A9. The action-ledger comparison signature stays private; its public ledger and FF064 detail expose only action-cell and function counts. The ordinary semantic diff retains changed formulas and values by design. HYPERLINK calls are kept in the ledger even when they look internal because their destination can be calculated. The tool does not evaluate a formula, follow a link, fetch a web service or image, or start an RTD provider. The scope follows Microsoft’s link guidance, WEBSERVICE reference, IMAGE reference, and RTD reference.

The 0.68 boundary handles a different kind of executable surface: Python in Excel keeps Python source in a workbook package part while a PY formula binds it into the grid. FormulaFence recognizes stored PY spellings and privately fingerprints the documented Python part, its environment and scripts, and the formula binding. A code, environment, package, binding, or statically visible source-cell change emits FF065; no_python_in_excel_changes adds FFP065. The public profile reports only counts. Source code, environment identifiers, script indexes, formula arguments, locations, and raw XML remain private; relationship-ID-only rewrites stay quiet and malformed or unbound metadata becomes a coverage event. The tool does not parse or run Python, evaluate PY, resolve a result, or contact Microsoft Cloud. A clean wheel was checked against an independently maintained public Python-part template and controlled code-only and static-input changes; the validation record has the checksums and redaction evidence.

The 0.69 boundary covers a related but separate executable surface: Office Add-in custom functions. A workbook can store a namespaced call such as =CONTOSO.ADD(...), while its manifest, JavaScript or TypeScript, and remote runtime live elsewhere. FormulaFence conservatively inventories stored namespaced candidates, excludes known native dotted Excel functions and compatibility spellings, and propagates candidates through formula-defined names and named LAMBDAs without treating the stored call as proof that an add-in is installed or runnable. A material candidate change or a statically visible input change emits FF066; no_office_custom_function_changes adds FFP066. The dedicated ledger exposes only aggregate formula-cell, call, and namespace counts; it never loads a manifest or add-in, evaluates the formula, or contacts a runtime. Ordinary formula-defined-name review context remains outside that redacted ledger. The validation record includes a clean-wheel check against the documented Office custom-function shape.

The 0.70 boundary covers stored worksheet and formula-defined REGISTER.ID calls. Excel documents this function as registering a DLL or code resource and returning its identifier. FormulaFence recognizes native REGISTER.ID calls, propagates them through formula-defined names and named LAMBDAs, and raises FF067 for a direct registration call, named-definition, or statically visible source-input change. no_worksheet_code_resource_registration_changes adds the fail-closed FFP067 boundary. The dedicated ledger exposes only aggregate registration-formula-cell, REGISTER.ID-call, and formula-defined-name counts; module paths, procedure names, arguments, identifiers, and cell locations remain private. It never loads a DLL/XLL or other resource, evaluates a formula, or establishes that registration will succeed. This is separate from the XLM macro-sheet CALL surface. The validation record includes a clean-wheel check of direct, named-definition, and static-input changes.

The 0.71 release closes an indirection gap in the existing formula external-action boundary. A stored HYPERLINK, WEBSERVICE, IMAGE, or RTD call can now be reached through a formula-defined name or named LAMBDA without escaping FF064. FormulaFence propagates the known stored calls through nested and recursive name chains, keeps relevant name-definition material in a separate private signature, and retains an uninvoked stored definition as review evidence. A same-count name-definition change or a statically visible input change can therefore emit FF064; no_formula_external_action_changes continues to make it FFP064. Profiles expose only formula-cell, function, and formula-defined-name counts. Names, formulas, arguments, destinations, provider strings, and locations remain private; the scanner never evaluates a formula, follows a link, fetches content, or starts an RTD provider. The validation record includes a clean-wheel check of direct, nested, static-input, and redaction paths.

The 0.72 release closes the remaining stored-definition REGISTER gap without expanding into a speculative catch-all for XLM syntax. Microsoft's xlfRegister Form 1 reference documents that legacy XLM REGISTER can register DLL functions or commands and that its macro types can be called from a defined-name definition; Form 2 can load and activate an XLL. FormulaFence now records REGISTER only when it appears in a formula-defined name or named LAMBDA, propagates that private marker through nested and sheet-local names to invoking cells, and raises FF068 for a same-count definition/invocation change or a statically visible input change. no_formula_defined_xlm_registration_changes adds fail-closed FFP068. Profiles expose only invocation-cell, call, and relevant definition counts; module paths, procedure names, type strings, formulas, arguments, cells, and name identities remain private. It does not evaluate a formula, execute a macro, resolve a path, load a DLL/XLL, or inspect trust settings. Direct worksheet REGISTER formulas and raw XLM macro-sheet parts remain separate, intentionally narrow boundaries. The validation record includes clean-wheel checks for nested definitions, static inputs, policy failure, and redaction.

The 0.73 release closes a separate stored-definition blind spot: legacy XLM EVALUATE. Microsoft's expression-evaluation reference identifies it as an XLM function that reduces a valid character string to a worksheet value. FormulaFence now inventories EVALUATE only when it is stored in a formula-defined name or named LAMBDA, propagates that private marker through nested and sheet-local names to invoking cells, and raises FF069 for definition, invocation, or statically visible argument-input changes. no_formula_defined_xlm_evaluation_changes adds fail-closed FFP069. Profiles expose only invocation-cell, call, and relevant definition counts; expression text, formulas, arguments, cells, and names remain private. The scanner does not evaluate text, parse a runtime-generated expression, or infer dependencies inside it: only the stored call's own visible static argument edge is traced. Direct worksheet EVALUATE calls and raw XLM macro sheets remain deliberately separate boundaries. The validation record includes controlled definition, static-input, redaction, clean-wheel, scope, cycle, shadowing, and runtime-text-coverage checks.

The 0.74 release covers another stored-definition blind spot: legacy XLM GET.CELL. Microsoft's Excel C API reference identifies GET.CELL / xlfGetCell as an XLM information function. FormulaFence inventories GET.CELL only when it is stored in a formula-defined name or named LAMBDA, propagates that private marker through nested and sheet-local names to invoking cells, and raises FF070 for definition, invocation, or statically visible argument-input changes. no_formula_defined_xlm_get_cell_changes adds fail-closed FFP070. Profiles expose only invocation-cell, call, and relevant definition counts; information types, references, formulas, arguments, cells, and names remain private. The scanner does not evaluate a call, determine its information type, resolve dynamic references, or simulate display, formatting, comments, protection, or other Excel state; only the stored call's own visible static argument edge is traced. Direct worksheet GET.CELL calls and raw XLM macro sheets remain deliberately separate boundaries. The validation record includes controlled definition and static-input changes, redaction, clean-wheel, scope, cycle, and shadowing checks.

The 0.75 release covers selected legacy XLM environment-information calls: GET.WORKBOOK, GET.WORKSPACE, and GET.DOCUMENT. Microsoft's Excel C API reference identifies workspace information functions such as GET.CELL and GET.WORKBOOK; its xlfFree example shows GET.WORKSPACE returning platform information, and its expression-evaluation reference identifies GET.DOCUMENT as an XLM information function. FormulaFence inventories the selected calls only when stored in a formula-defined name or named LAMBDA, propagates their private marker through nested and sheet-local names to invoking cells, and raises FF071 for definition, invocation, or statically visible argument-input changes. no_formula_defined_xlm_environment_information_changes adds fail-closed FFP071. Profiles expose only invocation-cell, call, and relevant definition counts; information types, references, formulas, arguments, cells, and names remain private. The scanner does not evaluate a call, determine its information type, resolve dynamic references, or simulate workbook, workspace, document, client, add-in, or printer state. It deliberately does not assert that a state-only workbook change alters a stored call. Direct worksheet calls and raw XLM macro sheets remain deliberately separate boundaries. The validation record includes controlled definition, static-input, state-only, redaction, clean-wheel, scope, cycle, and shadowing checks.

The 0.76 release closes the adjacent ordinary-formula gap: native CELL and INFO calls can depend on file, folder, client, workspace, or selected-cell state even when visible precedents do not change. FormulaFence inventories direct worksheet calls as well as formula-defined names and named LAMBDAs, while Microsoft’s CELL reference explains why the optional reference matters and its INFO reference documents current operating-environment values. FF072 records material call, definition, invocation, or statically visible input changes; no_formula_environment_information_changes promotes it to FFP072. The ledger exposes only formula-cell, call, relevant-definition, and omitted-CELL-reference counts; it never evaluates a formula, identifies an information type, infers a selected cell, or simulates workbook/client state. A state-only workbook change is deliberately not asserted to alter a call. The validation record includes independent fixtures, redaction checks, and a fresh-wheel control.

The 0.77 release extends the existing FF064 / FFP064 formula external-action boundary to provider-backed STOCKHISTORY and the full documented Cube family: CUBEKPIMEMBER, CUBEMEMBER, CUBEMEMBERPROPERTY, CUBERANKEDMEMBER, CUBESET, CUBESETCOUNT, and CUBEVALUE. A workbook can therefore change a market symbol, stored Cube connection, MDX expression, set, or property selection while retaining the same visible call count. FormulaFence keeps those signatures private, propagates the boundary through formula-defined names and named LAMBDAs, and flags a statically visible source-input change without evaluating a formula. Profiles expose only formula-cell, relevant-name, STOCKHISTORY, and aggregate Cube-call counts; symbols, connections, queries, locations, and results stay out of the dedicated ledger. This follows Microsoft’s STOCKHISTORY reference and CUBESET reference. It does not contact a market provider, resolve a connection, query a cube, or infer an external result. The validation record includes controlled definition, source-input, state-only, redaction, and fresh-wheel checks.

The 0.78 release closes a workbook-structure blind spot around native SHEET and SHEETS. Microsoft’s SHEET reference documents sheet-number behavior, while its SHEETS reference documents that an omitted reference counts sheets in the containing workbook; both include hidden, very-hidden, macro, chart, and dialog sheets. FormulaFence now inventories those calls alongside CELL and INFO in worksheet formulas, formula-defined names, and named LAMBDAs. When a complete raw OOXML tab catalog changes, stored SHEET and omitted-reference SHEETS() calls raise FF072; no_formula_environment_information_changes adds fail-closed FFP072. The catalog remains private, visibility-only edits are not treated as catalog changes, and explicit SHEETS(reference) calls are inventoried without guessing whether their runtime reference spans one sheet or a 3-D range. FormulaFence never evaluates the call, resolves a dynamic reference, or simulates a workbook result. The validation record includes chart-sheet, visibility-only, explicit-reference, redaction, and clean-wheel checks.

The 0.79 release closes a different stored-definition gap: selected legacy XLM action and event-dispatch calls can hide in a formula-defined name or named LAMBDA without appearing in raw macro-sheet XML. FormulaFence now inventories the finite set CALL, EXEC, EXECUTE, RUN, SEND.KEYS, and selected ON.* calls, then carries a private marker through nested, recursive, and sheet-local names to invoking cells. Microsoft’s Excel C API reference describes XLM command-equivalent functions and event traps, while its DLL-access guidance documents CALL and REGISTER as XLM macro-sheet routes to DLL functions or commands. A stored definition, invocation, or statically visible input change emits FF073; no_formula_defined_xlm_action_changes adds fail-closed FFP073. Targets, handler names, formulas, arguments, locations, and name identities stay private. The tool does not evaluate a formula, resolve a target, load a DLL, send DDE, run a macro or program, or interpret arbitrary XLM commands; direct worksheet calls and raw macro sheets remain deliberately separate boundaries. The validation record includes clean-wheel, static-input, state-only, privacy, scope, cycle, and shadowing checks.

The 0.80 release closes a direct-formula DDE blind spot. Excel’s documented DDE syntax such as ='Quote'|'NYSE'!ZAXX can bind an application, topic, and item directly in worksheet formulas, formula-defined names, or named LAMBDAs; it is distinct from OOXML externalLink DDE/OLE metadata. FormulaFence now scans stored syntax conservatively before the ordinary formula tokenizer, propagates private markers from names to their invoking cells, and raises FF074 for material link, stored-definition, invocation, or statically visible input changes. no_formula_dde_link_changes adds the fail-closed FFP074 boundary. Public profiles expose only aggregate formula-cell, link, and relevant-definition counts; application, topic, item, formulas, names, and cell identities remain in private comparison signatures. The scanner ignores pipes inside ordinary string literals and local quoted sheet names, does not evaluate a formula or resolve, launch, contact, or send commands to a DDE server, and retains raw OOXML external-link DDE/OLE metadata under the separate FF025 boundary. The scope follows Microsoft’s DDE overview and Excel DDE security-setting guidance; clean-wheel validation exercises direct, named, input, policy, and redaction paths.

The 0.81 release makes the merge boundary easier to adopt: FormulaFence now ships as a root composite GitHub Action. A workflow supplies an approved baseline, candidate, and optional policy; the Action writes a local report, adds Markdown evidence to the job summary, uploads the report before re-emitting a policy failure, and exposes deterministic report-path and exit-code outputs. It confines reports, workbooks, and policy files to the checked-out workspace, refuses to overwrite an input, and still never evaluates a formula, runs a macro, comments on a pull request, or sends workbook contents to a FormulaFence service. The CI guide has the complete workflow, SARIF, artifact, and pinning details.

The 0.82 release closes an unqualified worksheet-function gap. A bare call can bind to a VBA user-defined function, COM/Automation add-in, XLL, or another registered runtime even though the stored formula cannot identify a provider. FormulaFence keeps a stable native Excel catalogue and treats only unknown bare calls as candidates, while suppressing built-in functions, workbook-defined names, local LET/LAMBDA bindings, qualified calls, and dedicated legacy XLM spellings. It propagates candidates through formula-defined names, named LAMBDAs, recursive groups, and sheet-local definitions; direct, stored-definition, invocation, and statically visible input changes emit FF075, while no_unqualified_runtime_function_changes adds fail-closed FFP075. The dedicated ledger exposes only aggregate counts—never candidates, formulas, arguments, cells, providers, or host details—and FormulaFence never evaluates a formula, loads a provider, or infers that one is installed or runnable. The scope follows Microsoft’s installed UDF reference, custom-function guidance, and XLL guidance; the validation record includes a fresh-wheel policy and redaction check.

The 0.83 release closes a separate XLM execution-path gap. A macro-sheet program can remain unchanged while a special workbook automatic-macro name adds, removes, or retargets the path from an open, close, activate, or deactivate event to a macro-sheet cell. FormulaFence reads raw workbook defined names, normalizes the optional _xlnm. built-in prefix, and recognizes only the four documented workbook-scoped names—Auto_Open, Auto_Close, Auto_Activate, and Auto_Deactivate—when a direct internal single-cell A1 definition points to a declared XLM macro sheet. A material routing change emits FF076; no_xlm_automatic_macro_binding_changes adds high-severity FFP076. The dedicated profile and finding expose only per-event counts, leaving names, targets, and stored definitions in a private signature; the ordinary defined-name diff stays readable for normal review. FormulaFence does not evaluate or resolve a name, parse or execute XLM, rely on the reserved definedName@xlm attribute, or infer whether Excel security settings will run it. The scope follows Microsoft's automatic-macro API and enumeration; the validation record includes static-routing, ambiguity, redaction, policy, and fresh-wheel checks.

The 0.84 release closes a compatibility blind spot in the existing Python-in-Excel boundary. Current workbooks can carry a separately stored 2022 pythonScripts.xml package part beside—or instead of—the documented 2023 python.xml part. FormulaFence now recognizes the 2022 root, content type, workbook relationship, and script records, then privately fingerprints every stored Python part independently. A code/package change in either representation emits FF065; no_python_in_excel_changes still adds FFP065. Public output remains aggregate-only, and FormulaFence does not decide which representation an Excel runtime will execute or assume that the two agree. Conflicting, malformed, missing, unbound, or unreadable declarations remain explicit coverage evidence. The release was exercised against public Python-in-Excel workbooks, including a compatibility-only file and a dual-representation file; a fresh wheel produced a redacted FF065 for a code-only compatibility-part change. The validation record has the reproducible details.

The 0.85 release adds a bounded portfolio change-control boundary for teams that maintain related models. formulafence portfolio BASELINE_DIRECTORY CANDIDATE_DIRECTORY recursively matches supported .xlsx and .xlsm workbooks by portable relative path, applies existing policies independently to each matched file, and creates one JSON, Markdown, or SARIF artifact. An added or removed path emits high-severity FF077; no_portfolio_membership_changes adds FFP077. A move deliberately remains a removal plus addition rather than an inferred rename. Portfolio SARIF uses only relative workbook artifact paths, and new or removed workbook contents remain out of public evidence.

The scan is bounded at 512 supported workbooks per directory by default and fails closed for symlinks, unsupported spreadsheet formats, case-colliding or control-character paths, over-limit inventories, and two empty directories; transient Office lock files are skipped. An unreadable workbook produces redacted critical FF078 evidence, preserves the other results, and returns exit code 2. If that unreadable file is newly added or removed, its known FF077 / FFP077 membership evidence remains visible too. The composite GitHub Action now detects paired directory inputs, exposes the bound with max-workbooks, and refuses an output path inside either inspected portfolio, so a report cannot alter the inventory it is reviewing. FormulaFence does not execute formulas or macros. A fresh wheel reproduced this contract against the public Python-in-Excel course workbooks; the validation record contains the real-corpus and clean-install evidence.

The 0.86 release follows direct, static external A1 references across a portfolio only when an exact, relative candidate workbook and sheet match. A changed source cell with downstream formula impacts emits FF079; no_cross_workbook_impacts makes the same evidence a fail-closed FFP079 policy boundary. The graph is deliberately not a link resolver: absolute, URI, escaping, basename-only, external-name, 3-D, malformed, and dynamic forms are left unresolved rather than guessed. Reports retain only portable workbook paths and logical Excel cell addresses; raw link spelling stays private.

This is static reachability evidence, not a recalculation claim. FormulaFence does not open, fetch, evaluate, or follow an external target. The traversal has a global --max-link-impact bound (also exposed by the composite Action); if the bound is reached, critical FF080 evidence marks the portfolio report incomplete and returns exit code 2. The validation record covers exact-relative resolution, transitive impact, policy and SARIF output, redaction, the cap, a clean-wheel check, and a public external-link fixture that is intentionally not guessed.

The 0.87 release adds the direct workbook-scoped name form that Excel documents for workbook links: a formula such as =[Inputs.xlsx]InputRange can contribute to the same candidate-only graph, but only when its exact relative source is already in the portfolio and that source name expands completely to static internal A1 destinations. Safely resolvable global formula-name aliases work too; FormulaFence never evaluates a name or a formula to make this happen.

The 0.88 release adds Excel's package-indexed external-name form, such as =[1]!InputRange, including a direct workbook-scoped consumer alias whose stored definition is [1]!InputRange. The 1 is a declaration position, not a filename. FormulaFence uses it only after the raw package proves one document-order externalReference, one externalLink part, one externalBook, and one external externalLinkPath relationship. That target must still normalize to an already-inspected relative candidate, and the source name must fully expand to static internal A1 destinations.

The boundary remains deliberately strict. Raw external source spellings, package relationship targets, and indexed source-name spellings stay private in portfolio evidence; ordinary source and consumer defined-name declarations remain normal review context. FormulaFence never opens, fetches, evaluates, or trusts an external-link cache. Absolute, URI, escaping, malformed or ambiguous package shapes, DDE/OLE/non-workbook links, package external-A1 forms, sheet-scoped or formula-defined consumer aliases, and missing, dynamic, relative, cyclic, external, 3-D, malformed, or otherwise non-static source names remain unresolved rather than guessed. Changing a name declaration is still the ordinary defined-name review event, not an FF079 source root. The validation record covers declaration-order resolution, aliases, rejected unsafe and malformed forms, report redaction, a public link-package fixture, and a clean-wheel run.

The 0.89 release extends that same package-backed path to direct external A1 references such as =[1]Data!$B$2:$B$4, including a direct workbook-scoped consumer alias whose stored definition is exactly [1]Data!$B$2:$B$4. The index is still not a filename: FormulaFence resolves it only through the validated declaration order and only to an already-inspected relative candidate. Static cells, ranges, whole rows, and whole columns are eligible; 3-D spans, structured references, formula aliases, sheet-scoped aliases, unsafe paths, and malformed or ambiguous package shapes remain unresolved. It never opens, follows, evaluates, or trusts an external target or cache. The boundary was checked against a temporary copy of an independently maintained OpenPyExcel fixture containing a real [1]Sheet1!$A$1 alias, and the 0.89 validation record includes the public-fixture, redaction, CI, and clean-wheel evidence.

The 0.90 release closes the next static link gap: external sheet-local names such as =[Inputs.xlsx]Data!LocalInput and =[1]Data!LocalInput. The source sheet is an explicit local-name scope, not a hint to search for a similarly named global or another sheet-local name. FormulaFence follows only a fixed internal destination (including a safely expanded local alias) in an already-inspected candidate workbook; direct and package-indexed workbook-scoped consumer aliases can retain the exact local-name spelling. Dynamic, missing, wrong-scope, 3-D, structured, unsafe, malformed, or ambiguous forms remain unresolved. The checked boundary never opens, fetches, refreshes, evaluates, or trusts a target/cache, and raw source paths, indexes, sheet/name identities, and aliases stay out of portfolio evidence. It was exercised against an independently maintained workbook with a real worksheet-scoped range and a quoted sheet name; the exact packaged wheel emitted FF079 and FFP079 while preserving redaction. The 0.90 validation record has the reproducibility details.

The 0.91 release closes the remaining direct-alias gap in that graph. A workbook-scoped consumer name can now retain one exact direct external A1 range or workbook-scoped external name, including the canonical literal spelling with one leading =. The bridge is still deliberately narrow: formula aliases and sheet-scoped aliases are not inferred, and a same-named local consumer name shadows the workbook alias exactly as Excel does. FormulaFence still resolves only an already-inspected relative candidate, never opens or fetches a link, and keeps raw link paths and source/alias identities out of portfolio evidence. The release passed 614 tests and an isolated wheel/CLI check against an independently maintained external workbook; the 0.91 validation record has the reproducibility details.

The 0.92 release closes the next blind spot: finite chains of exact workbook-scoped name aliases can now reach a previously validated external A1, workbook-name, or sheet-local-name endpoint. Every bridge must be only one unqualified, non-A1 name identity, with or without its leading =. Formula expressions, ranges, sheet-scoped aliases, missing targets, and cycles remain unresolved; a local consumer name still shadows the workbook alias. The release passed 616 tests and an isolated wheel/CLI validation against an independently maintained external-data workbook, yielding FF079 and FFP079 without exposing raw link or alias identities. The 0.92 validation record has the reproducibility details.

The 0.93 release adds candidate-only resolution for static external 3-D A1 spans such as =[Inputs.xlsx]Jan:Mar!$B$2 and their validated package-indexed equivalents. It expands a span only when the source’s complete raw tab catalog and ordinary-worksheet order exactly match the inspected workbook, endpoints are unique forward worksheet tabs, and every included tab remains an inspected worksheet. That keeps the result static: FormulaFence neither opens, fetches, refreshes, evaluates, nor trusts an external target or cache. Unsupported paths, incomplete or inconsistent tab catalogs, reversed or malformed spans, formula wrappers, local alias shadowing, cycles, and other dynamic forms stay unresolved rather than guessed. Finite workbook-scoped alias chains can retain the same terminal form under the existing strict bridge rules. The release passed 620 tests, an isolated final-wheel CLI check, and a temporary-copy validation against an independently maintained external-data workbook; a changed middle-sheet cell produced FF079 and FFP079 without exposing raw source paths, endpoint tabs, or alias identities. The 0.93 validation record has the reproducibility details.

The 0.94 release closes the next static link gap: external Excel-table selectors such as ='..\inputs\source.xlsx'!Sales[#Data] and their validated package-indexed forms can now contribute to the same candidate-only portfolio graph. FormulaFence resolves a selector only when its exact relative source is already inspected and exactly one case-insensitive source table matches; it then follows a static #All, #Data, #Headers, #Totals, single-column, or contiguous-column selection to concrete source cells. Finite workbook-scoped alias chains can preserve that terminal form. Row-relative @/#This Row, source-sheet-qualified, bare, missing, ambiguous, malformed, unsafe, and otherwise non-static forms remain unresolved rather than guessed. It never opens, fetches, refreshes, evaluates, or trusts an external target or cache; raw table selectors and alias identities remain out of portfolio evidence. The release passed 624 tests, an isolated final-wheel CLI check, and a temporary-copy validation against the independently maintained XlsxWriter table fixture: one changed source cell produced four FF079/FFP079 impacts without leaking the selector or alias. The scope follows Microsoft’s structured-reference grammar; the 0.94 validation record has the reproducibility details.

The 0.95 release extends that same graph through a safely eligible,
workbook-scoped, non-LAMBDA formula-defined name. A definition such as
=SUM(ExternalInput) or
=SUM('..\inputs\source.xlsx'!Table1[Column2]) can retain its static external
input endpoint to calling worksheet formulas, including through another
eligible formula name. This extracts static input edges rather than calculating
the name: every external token must already be a static direct or
package-validated endpoint, while broken, unresolved, tokenizer-failed,
dynamic, relative, local-3-D, spilled, explicitly intersected, local, and
LAMBDA definitions remain unresolved. FormulaFence still never opens,
fetches, refreshes, evaluates, or trusts an external target or cache, and raw
paths, selectors, and bridge identities stay private in portfolio evidence.
The release passed 624 tests, an isolated final-wheel CLI check, and a
temporary-copy validation against the independently maintained XlsxWriter
table fixture: one changed source cell reached exactly two formula-name callers with FF079/FFP079 without leaking the names or selector. The 0.95 validation record has the reproducibility details.

The 0.96 release closes the next real-world indirection gap: a workbook-scoped named LAMBDA can hide a static external endpoint inside a reusable function body. FormulaFence now retains that endpoint only at an actual function call, including a nested named LAMBDA or an eligible formula-defined wrapper. It also carries fixed internal inputs alongside the endpoint, so =LAMBDA(value,SUM(value,Inputs!$B$2,ExternalInput)) preserves both sides of the dependency rather than dropping the ordinary workbook input. A bare LAMBDA name creates no edge. Dynamic, relative, recursive, local/shadowed, broken, unresolved, local-3-D, spill, explicit-intersection, and tokenizer-failed forms remain out of the graph; FormulaFence still does not calculate the formula or open, fetch, refresh, or trust an external target. The release passed 626 tests and an isolated final-wheel CLI validation against XlsxWriter's independent table fixture: changing one source cell reached the three real invocations with FF079/FFP079, while a bare-LAMBDA control and the private names, selector, and relative path stayed out of JSON. The 0.96 validation record has the reproducibility details.

The 0.97 release protects a different but equally quiet boundary: an external source or target can change while the existing worksheet formula stays at exactly the same cell. Literal static external endpoints can also live in workbook or sheet-local names, data-validation criteria, and chart formulas—not only worksheet cells. FormulaFence now keeps a private ledger across those persisted surfaces. A material change emits FF081; no_external_workbook_link_surface_changes turns it into FFP081. The new evidence exposes only surface and endpoint counts, while the ledger retains source paths, formulas, ranges, and chart-part identities privately. If a chart formula part cannot be read, the guard fails closed rather than pretending coverage. This is still static inspection: FormulaFence does not evaluate a formula, open or resolve a source workbook, refresh external data, trust a cache, or infer a text-built reference. The release passed 632 tests and an isolated-wheel integration run with an unchanged external-formula location plus private name and validation targets. The 0.97 validation record has the exact scope and evidence.

The 0.98 release adds a deliberate sharing boundary around that local-review evidence. Generic JSON and SARIF reports can normally include changed formulas, defined names, and validation criteria; that is useful in a trusted review, but it can expose a literal external-workbook path or endpoint in a broadly shared CI artifact. --redact-external-workbook-links now gives diff, check, and portfolio an opt-in render-time mode: it replaces a whole serialized value containing a recognized static external-workbook reference with a stable marker, while leaving the comparison, policy decision, and exit code unchanged. It also conservatively hides plainly visible bracketed dynamic literals without evaluating a formula or reconstructing text-built references. The default remains detailed local evidence; this is not a general secret scrubber. The public GitHub Action exposes the same choice as redact-external-workbook-links: 'true'. The release passed 636 tests, a clean isolated-wheel check, and the public Action contract. The 0.98 validation record has the exact scope and artifacts.

The 0.99 release closes a related sharing leak around formula actions and direct DDE links. A changed HYPERLINK, WEBSERVICE, IMAGE, RTD, STOCKHISTORY, Cube call, or DDE formula can carry a URL, provider, connection/query, service, topic, or item in ordinary review evidence; a bare cell such as A9 can carry the same material when an action formula reads it. --redact-formula-external-actions gives diff, check, and portfolio an opt-in, output-only boundary that hides direct action/DDE formula text, exact statically tracked input evidence, and conservative changed name-chain evidence without changing findings, policy decisions, or exit codes. It neither evaluates formulas nor contacts a provider/DDE server, and it is not a general secret scrubber. The GitHub Action exposes the same switch as redact-formula-external-actions: 'true'. The release passed 642 tests, hosted CI, the public Action contract, and isolated wheel checks across JSON, Markdown, SARIF, policy, and portfolio output. The 0.99 validation record has the exact scope and artifact hash.

The 0.100 release adds the matching sharing boundary for Python in Excel. Microsoft documents static source in PY(python_code, return_type), so ordinary JSON review evidence can contain code or an xl() reference even though FormulaFence’s FF065 ledger keeps workbook Python-package source private. --redact-python-in-excel now gives diff, check, and portfolio an opt-in, output-only mode that replaces direct stored PY material and before/after evidence for a changed PY cell or an exact statically tracked input that reaches one. It leaves findings, policy decisions, and exit codes intact; it does not parse or execute Python, calculate formulas, contact Microsoft Cloud, or claim to scrub arbitrary workbook material. The composite Action exposes the same switch as redact-python-in-excel: 'true'. The release passed 647 tests, hosted CI, the public Action contract, and isolated final-wheel checks across JSON, Markdown, SARIF, policy, and portfolio output. The 0.100 validation record has the exact scope and artifact hash.

The 0.101 release adds the matching sharing boundary for namespaced Office custom functions. Microsoft documents these JavaScript or TypeScript functions as namespaced Excel formulas, and a call can carry proprietary arguments to an add-in or service. Generic review output can therefore reveal direct function material, an exact statically tracked input that reaches a changed call, or a changed formula-defined-name wrapper. --redact-office-custom-functions gives diff, check, and portfolio an opt-in, output-only mode that replaces that material with a stable marker while leaving findings, policy decisions, and exit codes intact. It does not load an add-in or manifest, execute JavaScript, resolve dynamic arguments, calculate formulas, or contact a network service. The composite Action exposes the same switch as redact-office-custom-functions: 'true'. The release passed 653 tests, the public Action contract, and isolated final-wheel checks across JSON, Markdown, SARIF, policy, and portfolio output. The scope follows Microsoft’s Office custom-functions overview; the 0.101 validation record has the exact scope and artifact hash.

The 0.102 release adds the matching sharing boundary for bare unqualified runtime-function candidates. Microsoft documents installed UDFs, bare VBA custom-function calls, and XLL code exposed to Excel; a stored call can therefore carry a private name, argument, or ordinary static input without proving that a provider is installed or runnable. --redact-unqualified-runtime-functions gives diff, check, and portfolio an opt-in, output-only boundary that replaces direct FF075 candidate material, exact statically tracked inputs, and conservatively changed formula-defined-name-chain evidence with a stable marker. Findings, policy decisions, and exit codes stay intact. It does not evaluate formulas, resolve/load VBA, COM/Automation, XLL, or another runtime, execute code, contact a provider, or reconstruct dynamic arguments. The composite Action exposes the same switch as redact-unqualified-runtime-functions: 'true'. The release passed 659 tests, the public Action contract, and isolated final-wheel checks across JSON, Markdown, SARIF, policy, and portfolio output; the 0.102 validation record has the exact scope and artifact hash.

The 0.103 release adds the matching sharing boundary for worksheet code-resource registrations. Microsoft’s REGISTER.ID reference documents a worksheet-capable function that returns a DLL/code-resource registration ID and registers an unregistered resource. A generic report can therefore reveal a module, procedure, type string, static input, or changed named wrapper even though FormulaFence’s FF067 ledger publishes only counts. --redact-worksheet-code-resource-registrations gives diff, check, and portfolio an opt-in, output-only boundary that replaces that direct material, exact statically tracked inputs, and conservatively changed formula-defined-name-chain evidence with a stable marker. Findings, policy decisions, and exit codes stay intact. It does not calculate formulas, resolve a module path, load a DLL/XLL, inspect host trust settings, execute code, contact a provider, or reconstruct dynamic arguments. The composite Action exposes the same switch as redact-worksheet-code-resource-registrations: 'true'. The release passed 665 tests, the public Action contract, and isolated final-wheel checks across JSON, Markdown, SARIF, policy, and portfolio output; the 0.103 validation record has the exact scope and artifact hash.

The 0.104 release adds the matching sharing boundary for legacy XLM REGISTER calls stored in formula-defined names and named LAMBDAs. Microsoft’s xlfRegister Form 1 reference documents DLL-function and command registration from this XLM primitive, while Form 2 documents XLL activation. That means a generic change artifact can disclose a module, procedure, type string, static input, or a dotted named wrapper even though the FF068 ledger publishes only counts. --redact-formula-defined-xlm-registrations gives diff, check, and portfolio an opt-in, output-only boundary that replaces direct stored registration material, exact statically tracked inputs, and conservatively changed formula-defined-name-chain evidence with a stable marker. Findings, policy decisions, and exit codes stay intact. It does not calculate formulas, execute a macro, resolve a module path, load a DLL/XLL, inspect trust settings, contact a provider, or reconstruct dynamic arguments. The composite Action exposes the same switch as redact-formula-defined-xlm-registrations: 'true'. The release passed 671 tests, the public Action contract, and isolated final-wheel checks across JSON, Markdown, SARIF, policy, and portfolio output; the 0.104 validation record has the exact scope and artifact hash.

The 0.105 release adds the matching sharing boundary for legacy XLM EVALUATE calls stored in formula-defined names and named LAMBDAs. Microsoft’s Excel expression-evaluation reference documents EVALUATE reducing a character string to an Excel value. A generic change artifact can therefore disclose stored expression text, a static input, or a dotted named wrapper even though the FF069 ledger publishes only counts. --redact-formula-defined-xlm-evaluations gives diff, check, and portfolio an opt-in, output-only boundary that replaces direct stored evaluation material, exact statically tracked inputs, and conservatively changed formula-defined-name-chain evidence with a stable marker. Findings, policy decisions, and exit codes stay intact. It does not calculate formulas or text arguments, parse runtime-generated expression text, execute a macro, or reconstruct dynamic text; dependencies visible only inside runtime text remain a coverage limit. The composite Action exposes the same switch as redact-formula-defined-xlm-evaluations: 'true'. The release passed 677 tests, the public Action contract, and isolated final-wheel checks across JSON, Markdown, SARIF, policy, and portfolio output; the 0.105 validation record has the exact scope and artifact hash.

The 0.106 release adds the matching sharing boundary for selected legacy XLM action and event-dispatch calls stored in formula-defined names and named LAMBDAs. Microsoft’s Excel C API reference documents XLM command-equivalent functions and event traps, while its DLL-access guidance documents CALL as an XLM DLL-access route. A generic change artifact can therefore disclose a target, handler, static input, or dotted named wrapper even though the FF073 ledger publishes only counts. --redact-formula-defined-xlm-actions gives diff, check, and portfolio an opt-in, output-only boundary that replaces direct stored selected-action material, exact statically tracked inputs, and conservatively changed formula-defined-name-chain evidence with a stable marker. Findings, policy decisions, and exit codes stay intact. It does not calculate formulas, resolve a target or event handler, load a DLL, send DDE, execute a macro or program, or reconstruct a dynamic action. The composite Action exposes the same switch as redact-formula-defined-xlm-actions: 'true'. The release passed 683 tests, the public Action contract, and isolated final-wheel checks across JSON, Markdown, SARIF, policy, and portfolio output; the 0.106 validation record has the exact scope and artifact hash.

The 0.107 release adds the matching sharing boundary for legacy XLM GET.CELL calls stored in formula-defined names and named LAMBDAs. Microsoft’s Excel C API reference identifies GET.CELL as xlfGetCell. A generic change artifact can therefore disclose an information type, reference, static input, or dotted named wrapper even though the FF070 ledger publishes only counts. --redact-formula-defined-xlm-get-cell-calls gives diff, check, and portfolio an opt-in, output-only boundary that replaces direct stored GET.CELL material, exact statically tracked inputs, and conservatively changed formula-defined-name-chain evidence with a stable marker. Findings, policy decisions, and exit codes stay intact. It does not calculate formulas, determine an information type, resolve a dynamic reference, simulate display/formatting or other Excel state, or reconstruct a runtime value. The composite Action exposes the same switch as redact-formula-defined-xlm-get-cell-calls: 'true'. The release passed 689 tests, the public Action contract, and isolated final-wheel checks across JSON, Markdown, SARIF, policy, and portfolio output; the 0.107 validation record has the exact scope and artifact hash.

The 0.108 release adds the matching sharing boundary for selected legacy XLM environment-information calls stored in formula-defined names and named LAMBDAs. Microsoft's Excel C API reference identifies workspace information functions such as GET.WORKBOOK; its xlfFree example demonstrates GET.WORKSPACE returning platform information, and its expression-evaluation reference identifies GET.DOCUMENT as an XLM information function. A generic change artifact can therefore disclose an information code, reference, static input, or dotted named wrapper even though the FF071 ledger publishes only counts. --redact-formula-defined-xlm-environment-information-calls gives diff, check, and portfolio an opt-in, output-only boundary that replaces direct stored selected-call material, exact statically tracked inputs, and conservatively changed formula-defined-name-chain evidence with a stable marker. Findings, policy decisions, and exit codes stay intact. It does not calculate formulas, determine an information type, resolve a dynamic reference, simulate workbook/workspace/document state, or reconstruct a runtime value. The composite Action exposes the same switch as redact-formula-defined-xlm-environment-information-calls: 'true'. The release passed 695 tests, the public Action contract, and isolated final-wheel checks across JSON, Markdown, SARIF, policy, and portfolio output; the 0.108 validation record has the exact scope and artifact hash.

The 0.109 release adds the matching sharing boundary for native CELL, INFO, SHEET, and SHEETS calls. Those ordinary-formula calls can retain a private information code, reference, tab name, static input, or dotted named wrapper in a generic change artifact even though FF072 exposes only aggregate counts. --redact-formula-environment-information gives diff, check, and portfolio an opt-in, output-only boundary that replaces direct stored native-call material, exact statically tracked inputs, and conservatively changed formula-defined-name-chain evidence with a stable marker. It preserves findings, policy decisions, and exit codes, including the existing private tab-catalog comparison for stored SHEET and omitted-reference SHEETS() calls. It does not calculate formulas, determine an information type, resolve a dynamic reference, infer a selected cell, simulate workbook/client/workspace state, or reconstruct a runtime value. The composite Action exposes the same switch as redact-formula-environment-information: 'true'. The release passed 701 tests, the public Action contract, and isolated final-wheel checks across JSON, Markdown, SARIF, policy, and portfolio output; the 0.109 validation record has the exact scope and artifact digest.

The 0.110 release makes the review artifact itself easier to use: --format html
now gives diff, check, and portfolio a self-contained browser page with
local text/severity filters and expandable complete evidence. The page contains
only inline styles and a fixed filtering script; it loads no remote assets or
sends workbook content anywhere. Every workbook-derived value is HTML-escaped,
so report evidence cannot become markup or script. The existing output-only
sharing boundaries apply before HTML rendering and remain explicit in the page.
The composite Action accepts format: html, uploads the page as its configured
artifact, and links to it from the job summary rather than embedding it. The
release passed 703 tests, the public Action contract, package checks, and a
fresh final-wheel install that retained default local evidence while redacted
HTML preserved FF072 / FFP072. The 0.110 validation record has the exact scope and final artifact digests.

The 0.111 release hardens the boundary before semantic review begins. Every
source workbook now passes a bounded ZIP inventory before FormulaFence reads raw
OOXML or calls openpyxl; it does not extract the package. The gate accepts
canonical single-disk stored/deflated packages, validates central and local
headers, and caps source bytes (1 GiB), central-directory metadata (32 MiB),
parts (4,096), expanded member bytes (512 MiB), aggregate expansion (768 MiB),
and compression ratio (1,000:1). It rejects duplicate/case-colliding or unsafe
paths, ZIP Unicode-path aliases, encrypted/special members, malformed ZIP64
declarations, and overlapping payloads. Valid ZIP64 workbooks remain supported.
This is a resource and reader-consistency boundary, not a malware classifier or
a substitute for isolated CI runners. The release passed 719 tests, hosted CI
and the public Action contract, package checks, and a fresh Python 3.12 wheel
install. The 0.111 validation record has the exact limits and artifact digests.

The 0.112 release closes the next resource boundary after ZIP validation.
FormulaFence now requires defusedxml for raw OOXML parsing, which enables
openpyxl's defused XML path in the supported install. After the header-only
archive inventory—and before downstream scanners or the complete workbook
reader—it caps XML and relationship parts at 64 MiB, aggregate XML material at
256 MiB, follows bounded workbook sheet relationships, and streams selected
worksheet XML to reject more than 500,000 populated SpreadsheetML cells without
retaining them. This deliberately protects the in-memory semantic reader while
preserving explicit coverage warnings for malformed unrelated extension parts.
VBA payloads are hashed in a stream, and malformed openpyxl TypeError or
IndexError failures now return the normal unreadable-workbook exit status
instead of a traceback. The release passed 727 tests, hosted CI and the public
Action contract, package checks, and a fresh Python 3.12 wheel install that
confirmed the defused parser and early entity rejection. The 0.112 validation
record

has the exact limits and evidence.

The 0.113 release closes the remaining cardinality gap inside that reader
boundary. Before FormulaFence starts downstream scanners or openpyxl, it
streams the package manifest, workbook metadata, canonical styles,
shared-string table, and workbook-selected sheets. Each streamed part is
limited to 4,000,000 XML elements and 256 nesting levels; the gate also caps
populated cells and shared-string entries at 500,000 each, cellXfs styles at
Excel's 65,490-style limit, cell text at 32,767 characters, and stored formula
or defined-name text at 8,192 characters. Shared strings follow the first
manifest target openpyxl would load, then a sole workbook relationship or
canonical fallback. This specifically stops a small compressed workbook from
making the reader allocate an unbounded string list while preserving the
coverage-warning route for malformed unrelated extension XML. The release
passed 737 tests in 115.64 seconds, hosted CI and the public Action contract,
package checks, and a clean Python 3.12 wheel install that confirmed the
defused parser, a normal profile, and a real 500,001-item shared-string input
error. The 0.113 validation record
has the exact limits and evidence.

The 0.114 release closes the bootstrap catalog gap before the workbook reader
starts. The ZIP member limit alone cannot limit package manifest declarations,
workbook relationships, or duplicate <sheet> declarations that repeatedly
point at one already-safe target. FormulaFence now streams and caps those
reader-materialized catalogs at 4,096 content-type declarations, 4,096 workbook
relationships, and 512 workbook sheet declarations. The last limit counts
declarations rather than unique parts, so a compact package cannot multiply one
sheet relationship into a large in-memory workbook or repeated raw-sheet scan.
The release passed 741 tests in 121.54 seconds, hosted CI and the public Action
contract, package checks, and a clean Python 3.12 wheel install that profiled a
normal workbook and rejected a 516-declaration repeated-sheet fixture before
loading it. The 0.114 validation record
has the exact limits and artifact evidence.

The 0.115 release closes the adjacent workbook defined-name allocation path.
SpreadsheetML permits an unbounded <definedName> sequence, and Excel permits
vastly more names than a CI worker should model blindly. FormulaFence now
streams and caps direct workbook defined-name declarations at 100,000 before
raw scanners or openpyxl build their complete models. The same local-name
semantics used by the reader now govern both the name and sheet catalog
counters, so an alternate-namespace entry cannot bypass the 100,000-name or
512-sheet boundary. A generated 100,000-name package remained supported; its
100,001st name failed before reader construction. The release passed 745 tests
in 116.02 seconds, hosted CI and the public Action contract, package checks,
and a clean Python 3.12 wheel install. The 0.115 validation record
has the exact boundary and artifact evidence.

The 0.116 release closes another repetition path in the workbook catalog. An
external reference or PivotCache selects a workbook relationship, but a
relationship-count limit alone cannot stop a compact XML part from repeating one
valid relationship thousands of times. FormulaFence now streams and caps direct
<externalReferences> and <pivotCaches> catalog entries at 4,096 each before
its raw scanners or openpyxl reader start. The counters use the reader's
local-name behavior, so alternate-namespace entries cannot bypass the bound. A
10,000-declaration pivot fixture previously cost 7.35 seconds; these catalog
fixtures now fail before reader work. The release passed 751 tests in 131.91
seconds, hosted CI and the public Action contract, package checks, and a clean
Python 3.12 wheel install. The 0.116 validation record
has the exact limits and artifact evidence.

The 0.117 release closes the remaining repeated-child paths in the workbook
package and legacy Custom View scanner. SpreadsheetML's book-view format
permits an unlimited collection, while the reader builds an object for every
book view, function group, smart-tag type, and web-publish object. FormulaFence
now caps those direct workbook catalogs, plus raw-scanned custom workbook views,
at 4,096 entries each; direct custom sheet views are capped at 4,096 in
aggregate across the selected sheet parts. The counts follow the reader/scanner's
local-name behavior, so alternate namespaces cannot bypass them. Controlled
10,000-entry fixtures previously reached 87 MiB and 2.37 seconds; all six now
fail before downstream work in 0.013–0.022 seconds, while exact-limit fixtures
remain accepted. The release passed 769 tests in 130.23 seconds, hosted CI and
the public Action contract, package checks, and a clean Python 3.12 wheel
install. The 0.117 validation record
has the exact limits and artifact evidence.

The 0.118 release closes a compact allocation path before ordinary workbook analysis. SpreadsheetML's mergeCells interoperability notes permit an unbounded mergeCell sequence, while openpyxl expands every coordinate in a merge range into an in-memory MergedCell. FormulaFence now streams direct merge declarations before raw scanners or the complete reader: 4,096 declarations across selected ordinary worksheet parts, 100,000 coordinates for every range and in aggregate, and a 256-character reference bound. The measurement follows direct local-name children and the reader's sheet-qualified range grammar, so namespace decoration or range spelling cannot bypass it. A 4,889-byte package with a 100,000-coordinate range remains accepted; 101,000-coordinate and full-grid A1:XFD1048576 fixtures reject in 0.002 seconds before grid expansion. The release passed 779 tests in 127.54 seconds, hosted CI and the public Action contract, package checks, and a clean Python 3.12 wheel install. The 0.118 validation record has the exact boundary and artifact evidence.

The 0.119 release closes compact allocation paths in worksheet control catalogs. SpreadsheetML permits up to 65,534 data-validation records and 32,767 sqref targets; the direct conditional-formatting and Scenario Manager collections can also make a small package allocate a large reader catalog. FormulaFence now streams those direct worksheet children before raw scanners or the complete reader. Data validations, conditional-formatting declarations and rules, and Scenario Manager containers, scenarios, and input-cell records are each capped at 4,096 across selected ordinary sheets. Every sqref is limited to 128 KiB and 4,096 whitespace-separated ranges, with 8,192 ranges in each catalog; data-validation and conditional-formatting formulas share the 8,192-character stored-formula limit. Counts follow the reader's direct local-name behavior, including alternate-namespace children. A 100,000-validation fixture previously reached 172 MiB and 9.64 seconds; the new gate rejects oversized catalogs in 0.008–0.044 seconds, while exact-limit fixtures remain accepted. The release passed 807 tests in 120.15 seconds, hosted CI and the public Action contract, package checks, and a clean Python 3.12 wheel install. The 0.119 validation record has the exact boundary and artifact evidence.

The 0.120 release closes compact allocation paths in styles.xml. Excel's published limits list 512 fonts per workbook, 256 fill styles, roughly 200–250 number formats, and 65,490 unique cell styles; openpyxl nevertheless dispatches stylesheet containers by local name and builds records for every direct child of its nested style lists. FormulaFence now streams this table before raw scanners or the complete reader: repeated known containers are capped at 4,096 in aggregate, and number-format, font, fill, fill-child, gradient-stop, border, base-XF, named-style, differential-style, palette, table-style, table-style-element, and extension lists each at 4,096. The existing 65,490 effective cellXfs ceiling remains and now follows the same local-name behavior, so an alternate namespace or unexpected child cannot bypass it. A 285,934-byte workbook with 100,000 valid font records (13.8 MiB of style XML) previously took 18.321 seconds and 336 MiB; the gate rejects it in 0.068 seconds at baseline memory, while an exact 4,096-font fixture remains accepted. The release passed 828 tests in 122.30 seconds, hosted CI and the public Action contract, package checks, and a clean Python 3.12 wheel install. The 0.120 validation record has the exact boundary and artifact evidence.

The 0.121 release closes a compact formatted-row allocation path. Excel permits a grid with 1,048,576 rows, but a populated-cell limit does not constrain empty row formatting. When a SpreadsheetML <row> carries an unqualified attribute other than r or spans, openpyxl retains it for later RowDimension construction. FormulaFence now caps those declarations at 16,384 in aggregate across ordinary worksheet parts before raw scanners or the complete reader run. Coordinate-only rows and namespace-qualified extension attributes remain compatible; unknown unqualified attributes are safely bounded. A 259,597-byte workbook with 100,000 empty height-formatted rows previously took 8.580 seconds and 142 MiB; it now rejects in 0.074 seconds at 36 MiB, while a 10,000-row fixture remains accepted. The release passed 836 tests in 122.58 seconds, hosted CI and the public Action contract, package checks, and an isolated Python 3.12 wheel install. The 0.121 validation record has the exact boundary and artifact evidence.

The 0.122 release closes two compact column-catalog paths. Excel’s published limits list 16,384 columns, yet a workbook can repeat valid <col> declarations without widening its visible grid. FormulaFence now limits reader-visible col records to 16,384 and direct cols containers to 4,096 across ordinary worksheet parts before raw scanners or the complete reader run. This constrains both reader allocations and the raw dimension-metadata lists; Strict SpreadsheetML receives the same guard where FormulaFence reads it, while foreign namespaces remain compatible. A 20,680-byte package with 100,000 repeated column declarations previously took 9.028 seconds and 82.8 MiB; it now rejects in 0.074 seconds at 35.8 MiB. A 7,246-byte package with 100,000 empty containers previously took 4.226 seconds and 72.3 MiB; it now rejects in 0.014 seconds at 34.7 MiB. Exact 16,384-declaration and 4,096-container fixtures remain accepted. The release passed 847 tests in 120.57 seconds, hosted CI and the public Action contract, package checks, and an isolated Python 3.12 wheel install. The 0.122 validation record has the exact boundary and artifact evidence.

The 0.123 release bounds two compact page-break catalogs. Excel’s published limits allow 1,026 horizontal and 1,026 vertical breaks, while openpyxl materializes direct <brk> records and FormulaFence’s raw print-layout scanner retains every direct child of rowBreaks and colBreaks. FormulaFence now limits those children to 2,052 in aggregate—one complete published row-plus-column allowance—and direct containers to 4,096 before either path runs. Strict SpreadsheetML receives the same guard where FormulaFence reads it; foreign-namespace containers remain compatible. A 17,127-byte package with 100,000 valid row-break records previously took 9.762 seconds and 70.9 MiB; it now rejects in 0.012 seconds at 33.7 MiB. A 7,166-byte package with 100,000 empty containers previously took 4.077 seconds and 39.0 MiB; it now rejects in 0.015 seconds at 33.8 MiB. Exact published capacity remains accepted. The release passed 859 tests in 122.86 seconds, hosted CI and the public Action contract, package checks, and an isolated Python 3.12 wheel install. The 0.123 validation record has the exact boundary and artifact evidence.

The 0.124 release closes the matching Custom View bypass. FormulaFence’s legacy Custom View scanner separately traverses direct page-break catalogs below a supported customSheetViews/customSheetView hierarchy, including Strict SpreadsheetML and opaque alternate-namespace paths; the preflight now shares the same 2,052-record and 4,096-container aggregate budgets with that scanner. A 19,396-byte Custom View package carrying 100,000 row-break records loaded through the exact 0.123.0 wheel in 9.043 seconds at 109.6 MiB, but 0.124 rejects it in 0.012 seconds at 33.3 MiB. A 9,138-byte package with 100,000 empty containers fell from 3.981 seconds and 74.3 MiB to a 0.023-second, 33.9-MiB rejection. The release passed 869 tests in 121.67 seconds, package checks, and a fresh Python 3.12 wheel install. The 0.124 validation record has the exact boundary and artifact evidence.

The 0.125 release closes the remaining opaque Custom View subtree path. FormulaFence’s raw scanner creates a recursive private signature for unknown XML below a supported Custom View, so the preflight now limits every descendant below direct views to 4,096 in aggregate, separately from the existing direct-view count. It covers standard, Strict, and alternate-namespace view paths while preserving the published 2,052 row-plus-column page-break allowance. An 8,978-byte package with 100,000 direct opaque children took 4.197 seconds and 111.7 MiB in the exact 0.124.0 wheel; 0.125 rejects it in 0.018 seconds at 33.7 MiB. An 8,798-byte package holding the same entries under one opaque nested child fell from 3.773 seconds and 100.2 MiB to a 0.019-second, 33.7-MiB rejection. The release passed 879 tests in 123.22 seconds, package checks, and a fresh Python 3.12 wheel install. The 0.125 validation record has the exact boundary and artifact evidence.

The 0.126 release closes the matching RibbonX allocation path. FormulaFence’s private RibbonX scanner can recursively canonicalize unsupported customUI controls, so it now streams every bounded customization part before materializing it and allows at most 4,096 XML elements per part, alongside the existing byte and part-count limits. An over-budget part becomes explicit incomplete-coverage evidence rather than a fully built XML tree. A 10,158-byte package with 100,000 direct opaque controls took 0.559 seconds and 93.6 MiB in the exact 0.125.0 wheel; 0.126 marks it in 0.137 seconds at 33.4 MiB. A 9,976-byte package holding the same entries below one opaque nested control fell from 0.578 seconds and 92.9 MiB to 0.137 seconds and 34.1 MiB. The release passed 885 tests in 123.47 seconds, package checks, and a fresh Python 3.12 wheel install; both hostile fixtures produce a visible FF010 coverage finding that can fail CI at the chosen threshold. The 0.126 validation record has the exact boundary and artifact evidence.

The 0.127 release closes the matching Office Web Add-in metadata paths. FormulaFence privately canonicalizes task-pane and webextension definition XML, so it now streams them before materializing a tree and permits 4,096 elements per part with a 16,384-element aggregate across the task-pane-plus-definition scan. An over-budget part becomes explicit incomplete-coverage evidence rather than a fully built fragment. An 11,021-byte package with 100,000 direct opaque task-pane children took 0.586 seconds and 97.8 MiB in the exact 0.126.0 wheel; 0.127 marks it in 0.135 seconds at 34.1 MiB. An 11,041-byte package with 100,000 opaque definition children fell from 0.579 seconds and 98.1 MiB to 0.135 seconds and 34.1 MiB. The release passed 892 tests in 123.09 seconds, package checks, and a fresh Python 3.12 wheel install; direct and nested variants of both parts produce visible FF010 coverage findings that can fail CI at the chosen threshold. The 0.127 validation record has the exact boundary and artifact evidence.

The 0.128 release closes the shared package-relationship path beneath those specialist scanners. FormulaFence now streams every OOXML .rels part before raw metadata or openpyxl can materialize it, allowing 4,096 XML elements per part and 16,384 in aggregate. The bound counts roots and opaque nested descendants, not only direct Relationship records; malformed optional parts still surface their established coverage warning. A 13,334-byte definition fixture and 13,325-byte task-pane fixture, each with 100,000 empty relationship records, took 1.094 seconds/84.0 MiB and 1.127 seconds/82.0 MiB in the exact 0.127.0 wheel without an unrecognized relationship marker. Version 0.128 returns the stable input error before a scanner starts in about 0.009 seconds at 34.8 MiB. The release passed 898 tests, package checks, and a fresh Python 3.12 wheel install; both fixtures return CLI status 2. The 0.128 validation record has the exact boundary and artifact evidence.

The 0.129 release closes the chart XML allocation path. FormulaFence privately canonicalizes legacy chart, ChartEx, chart-host DrawingML, and chart-overlay XML, so it now streams each part before materializing it and allows 32,768 elements per part with a 65,536-element aggregate across the chart package scan. This leaves room for real cached series, whose Excel data-point capacity is memory-limited, while turning a larger well-formed tree into explicit coverage evidence. A 12,453-byte package with 100,000 opaque chart children took 0.936 seconds and 84.9 MiB in the exact 0.128.0 wheel; 0.129 records one coverage gap in 0.374 seconds at 46.8 MiB. The release passed 905 tests, package checks, and a fresh Python 3.12 wheel install; the installed diff emits FF030 and returns status 1 at --fail-on medium. The 0.129 validation record has the exact boundary and artifact evidence.

The 0.130 release closes the matching PivotTable XML allocation paths. FormulaFence privately canonicalizes PivotTable view and cache-definition XML, then the ordinary workbook reader can otherwise follow those same bindings. It now streams each part before materializing it and allows 32,768 elements per part with a 65,536-element aggregate across the PivotTable package scan. Excel allows up to 1,048,576 unique items per PivotTable field, so a larger valid catalog becomes explicit coverage evidence rather than an unbounded CI allocation. After raw inspection, FormulaFence removes only PivotTable cache and view bindings from its temporary reader copy, retaining ordinary cell analysis without reparsing the package. An 11,968-byte workbook with 100,000 opaque view children fell from 1.115 seconds and 88.6 MiB to 0.507 seconds and 41.3 MiB; an 11,766-byte workbook with 100,000 valid cached items fell from 7.137 seconds and 207.2 MiB to 0.583 seconds and 44.9 MiB. The release passed 913 tests, package checks, and a fresh Python 3.12 wheel install; the installed diff emits FF031 and returns status 1 at --fail-on medium. The 0.130 validation record has the exact boundary and artifact evidence.

The 0.131 release closes the matching Slicer and Timeline cache XML allocation path. FormulaFence privately canonicalizes cache definitions and filter state, so it now streams every cache member before materializing the tree and allows 16,384 elements per part with a 32,768-element aggregate across the Slicer/Timeline cache scan. Excel documents 10,000 items displayed in a filter drop-down list; that is not a cache-validity rule, so the capacity leaves room above it while a larger well-formed tree becomes explicit FF032 coverage evidence instead of an unbounded CI allocation. A 13,633-byte workbook with 100,000 opaque direct Slicer-cache children fell from 0.738 seconds and 85.5 MiB in the exact 0.130.0 wheel to 0.165 seconds and 34.8 MiB in the final 0.131.0 wheel. The release passed 920 tests, hosted CI and the public Action contract, package checks, and a fresh Python 3.12 wheel install; the installed diff emits FF032 and returns status 1 at --fail-on medium. The 0.131 validation record has the exact boundary and artifact evidence.

The 0.132 release closes the matching generic Custom XML allocation path. Microsoft documents Custom XML as arbitrary stored XML, so FormulaFence treats its 32,768-elements-per-part and 65,536-elements-per-scan limits as CI allocation and coverage boundaries rather than workbook-validity limits. It streams generic Custom XML, property, Custom Data-property, custom document-property, and relevant relationship XML before private canonicalization; opaque binary Custom Data remains byte-bounded. A safely classified DataMashup handoff also prevents Power Query discovery from reparsing an over-budget generic tree. An 11,493-byte workbook with 100,000 opaque Custom XML children fell from 0.853 seconds / 94.8 MiB in the exact 0.131.0 wheel to 0.186 seconds / 36.0 MiB in 0.132.0. The release passed 929 tests, hosted CI, package checks, and a fresh Python 3.12 wheel install; the installed hostile diff emits FF010 and FF052. The 0.132 validation record has the exact boundary and artifact evidence.

The 0.133 release closes the matching workbook Theme XML allocation path. Microsoft documents a SpreadsheetML Theme as a workbook-bound colour, font, and format control, so FormulaFence treats its 32,768-elements-per-part and 65,536-elements-per-scan limits as CI allocation and coverage boundaries rather than workbook-validity limits. It streams Theme and Theme-relationship XML before private canonicalization; direct Theme-image payloads remain byte-bounded. A 9,591-byte workbook with 100,000 opaque direct Theme XML children fell from 1.090 seconds / 112.2 MiB in the exact 0.132.0 wheel to 0.683 seconds / 41.5 MiB in 0.133.0. The release passed 937 tests, hosted CI, package checks, and a fresh Python 3.12 wheel install; the installed hostile diff emits FF010 and FF053. The 0.133 validation record has the exact boundary and artifact evidence.

The 0.134 release closes the matching modern Threaded Comments and Persons XML allocation path. Microsoft specifies a Threaded Comments part associated with a worksheet and a Persons part associated with the workbook, so FormulaFence streams both before private canonicalization and permits 32,768 XML elements per part with 65,536 across the scan. After raw inspection it removes only those package relationships from the temporary ordinary-reader copy, preserving evidence while preventing a later reader from rebuilding a rejected tree. A 10,565-byte workbook with 100,000 opaque comment children fell from 1.264 seconds / 115.3 MiB in the exact 0.133.0 wheel to 0.672 seconds / 39.9 MiB in 0.134.0. The release passed 947 tests, hosted CI, package checks, and a fresh Python 3.12 wheel install; the installed hostile diff emits FF010 and FF045. The 0.134 validation record has the exact boundary and artifact evidence.

The 0.135 release closes a shared Worksheet DrawingML allocation gap. A compact workbook can point a worksheet at one xdr:wsDr tree that shape, native-image, in-content Office Web Add-in, and chart scanners each need to inspect. Previously a later chart guard could notice an expansive tree only after the earlier shape/image readers had materialized it. FormulaFence now follows direct internal transitional or Strict worksheet drawing relationships in the semantic preflight, deduplicates their XML targets, and streams them before any of those readers or the ordinary workbook reader start: 32,768 elements per target and 65,536 across the selected targets. This is a CI allocation boundary, not a workbook-validity limit. A well-formed overage returns the stable status-2 safety error rather than an incomplete report; malformed, missing, or non-XML optional targets keep their coverage diagnostics, and orphan DrawingML parts remain out of scope.

The release was measured with a 35,274-byte workbook containing 1,000,000 opaque direct children below a worksheet DrawingML root. The exact 0.134.0 wheel completed a normal diff in 9.914 seconds at 143.4 MiB after the early readers had handled the tree. The exact 0.135.0 wheel rejects it before any metadata reader in 0.584 seconds at 39.5 MiB. The full 955-test suite, hosted CI, Action contract, package checks, and a fresh Python 3.12 wheel install are recorded in the 0.135 validation record; Microsoft documents the worksheet DrawingML root as xdr:wsDr.

The 0.136 release closes the matching raw XML allocation paths for traditional Excel Notes and worksheet embedded controls. Conventional Note comments and their VML layout trees are privately compared outside ordinary cells; the control inventory independently traverses VML so it can distinguish Notes from form controls. FormulaFence now streams those raw trees before either private parser materializes them: 32,768 elements per part and 65,536 per scan. A well-formed overage becomes visible FF010/FF046 Note coverage evidence or FF010/FF029 control coverage evidence, not a hidden allocation spike. A 5 MB Note Comments part with one million opaque children fell from 8.586 seconds / 739 MiB in 0.135.0 to 0.706 seconds / 40 MiB in the final wheel; the shared VML case fell from 3.738 seconds / 209 MiB to 0.737 seconds / 41 MiB. The release passed 966 tests, the Action contract, package checks, and a fresh Python 3.12 install. The 0.136 validation record has the reproducible boundary and artifact evidence.

The 0.137 release closes four compact raw-XML allocation paths: SpreadsheetML XML Maps and table bindings, OPC package-signature envelopes, Python-in-Excel package XML, and Excel Rich Data package XML. FormulaFence streams every materialized package XML part before private canonicalization, with a 32,768-element per-part and 65,536-element inventory ceiling. A well-formed overage becomes visible FF010 coverage evidence together with FF049, FF050, FF051, or FF065, rather than a fully allocated private tree. Certificate and VBA-signature binaries remain byte-bounded rather than XML parsed; Rich Data streams only the worksheet vm/r bindings it needs, avoiding a second full worksheet tree. Four five-megabyte fixtures with one million opaque children completed in 0.431–0.535 seconds at about 36 MiB in the exact wheel, versus 3.262–7.227 seconds and 383–660 MiB before the guard. The release passed 985 tests, hosted CI, package checks, and a fresh Python 3.12 install. The 0.137 validation record has the exact fixtures and artifact hashes.

The 0.138 release closes the matching inner-XML allocation path in Power Query DataMashup Custom XML. The outer item was already bounded, but its Base64 length-prefixed stream can carry independent metadata and formula-firewall permission documents. FormulaFence now streams each decoded document before private parsing: 32,768 elements per document and 65,536 across the Power Query scan. A well-formed overage becomes visible FF010/FF024 coverage evidence, with a private content fingerprint so same-size hostile changes remain diff-visible. Two one-million-child inner documents expanded to five megabytes inside a 6.7 MB Custom XML item compressed to about 18 KB. The exact wheel completed them in 0.314 and 0.310 seconds at 62 and 57 MiB, versus 13.967 seconds / 416 MiB and 11.693 seconds / 403 MiB before the guard. The release passed 995 tests, hosted CI, package checks, and a fresh Python 3.12 install. The 0.138 validation record has the reproducible fixture and artifact evidence.

The 0.139 release closes the equivalent compact-allocation path in legacy shared-workbook revision history. revisionHeaders and revisionLog are raw audit XML outside ordinary cells; FormulaFence now streams each revision XML part before private history canonicalization, with a 32,768-element per-part and 65,536-element scan limit. A well-formed overage becomes explicit FF010/FF062 coverage evidence with a private streamed content fingerprint, so same-size hostile histories remain diff-visible without exposing old cells, authors, or XML. One-million-record log and header fixtures were only 6.0 MB / 8.9 KB and 9.0 MB / 17.6 KB after compression. The exact wheel completed them in 0.205 and 0.247 seconds at about 40 MiB, versus 4.212 seconds / 450 MiB and 5.087 seconds / 454 MiB before the guard. The release passed 1,005 tests, hosted CI, package checks, and a fresh Python 3.12 install. The 0.139 validation record has the reproducible fixture and artifact evidence.

The 0.140 release closes a compact-allocation path in raw external-data Connections XML. FormulaFence already compared refresh controls, but a highly repetitive xl/connections*.xml part could still be privately materialized first. It now bounds each Connections part to 16 MiB and 32,768 XML elements, and each Connections scan to 64 MiB, 512 parts, and 65,536 structural elements. A well-formed structural overage becomes visible FF010/FF023 opaque coverage evidence with a private streamed content fingerprint, so same-size hostile parts remain diff-visible without exposing connection material or raw XML. A 13,001,347-byte Connections part with 1,000,009 XML elements compressed to 25,951 bytes; the exact wheel completed it in 0.227 seconds / 39,280 KiB instead of allocating the hostile tree. The release passed 1,015 tests, hosted CI, package checks, and fresh Python 3.12 wheel/sdist installs. The 0.140 validation record has the full boundary and artifact hashes.

The 0.141 release closes the matching compact-allocation path in raw external-link package XML. FormulaFence already compared external-workbook, DDE, and OLE definitions, but a repetitive externalLink part or its direct relationship XML could still be privately materialized first; inventory and package-indexed workbook resolution could also reach the same part. It now shares one cached boundary across those readers: 16 MiB and 32,768 XML elements per selected externalLink*.xml or direct .rels part, plus 64 MiB, 512 parts, and 65,536 elements across the complete external-link scan. A well-formed structural overage becomes visible FF010/FF025 opaque package evidence with a private streamed content fingerprint, so same-size hostile content remains diff-visible without exposing targets, names, DDE/OLE metadata, cache values, or raw XML. A 17,577-byte workbook with a 6,000,645-byte external-link part and 1,000,000 injected children fell from 8.562 seconds / 714.8 MiB in 0.140.0 to 0.255 seconds / 42.3 MiB in the 0.141.0 source comparison. The release passed 1,026 tests, hosted CI, package checks, and fresh Python 3.12 wheel/sdist installs. The 0.141 validation record has the full boundary and artifact hashes.

The 0.142 release closes the matching compact-allocation path in selected raw query-table XML targets. FormulaFence already inspected refresh controls reached directly from worksheets or through Excel tables, but a byte-permitted queryTable part could still materialize repetitive private refresh-field, sort, or extension trees. It now streams each selected relationship target before private parsing: 16 MiB and 32,768 XML elements per part, plus 64 MiB, 512 parts, and 65,536 elements across the query-table scan. The reader shares a cached boundary and a sheet-neutral private template across direct and table-mediated references, so a reused part is neither reparsed nor recursively canonicalized once per worksheet binding. A well-formed structural overage becomes visible FF010/FF023 opaque coverage evidence with a private streamed content fingerprint, so same-size hostile XML remains diff-visible without exposing query-table names, connection material, field data, sort state, extensions, or raw XML. A 17,431-byte workbook with a 6,000,506-byte query-table part and 1,000,000 injected children fell from 6.686 seconds / 536.8 MiB in 0.141.0 to 0.261 seconds / 44.3 MiB in the 0.142.0 source comparison. The release passed 1,037 tests, hosted CI, package checks, and fresh Python 3.12 wheel/sdist installs. The 0.142 validation record has the full boundary and artifact hashes.

The 0.143 release closes the adjacent compact-allocation path in raw Excel 4.0/XLM Macro Sheet XML. FormulaFence already scanned macro formulas before ordinary workbook loading, but a byte-permitted program tree could still be privately materialized by macro canonicalization, a Custom View path, or the underlying worksheet reader. It now streams each selected macro-sheet part before private parsing: 16 MiB and 32,768 XML elements per part, plus 64 MiB, 512 parts, and 65,536 elements across the macro-sheet scan. A well-formed structural overage becomes visible FF010/FF026 opaque macro evidence with a private streamed content fingerprint, so same-size hostile programs remain diff-visible without exposing commands, values, relationships, or raw XML. After the raw scan, the temporary ordinary reader receives an empty worksheet replacement for selected XLM targets; Custom View and generic-sheet paths exclude those targets too, including an invalid ordinary relationship alias, which remains visible as coverage evidence. A 16,685-byte workbook carrying a 6,000,635-byte macro-sheet XML part with 1,000,000 injected children fell from 7.090 seconds / 528.9 MiB in 0.142.0 to 0.263 seconds / 44.8 MiB in 0.143.0. The release passed 1,051 tests, hosted CI, package checks, and fresh Python 3.12 wheel/sdist installs. The 0.143 validation record has the full boundary and artifact hashes.

The 0.144 release closes a compact-allocation path in the canonical xl/metadata.xml catalog used to distinguish legacy CSE formulas from dynamic arrays. FormulaFence now bounds that raw metadata reader at 16 MiB and 32,768 XML elements before materializing it, and streams direct worksheet c/f bindings instead of building a second worksheet tree. An oversized or structurally over-budget catalog becomes explicit incomplete-coverage evidence: observed arrays remain unclassified, fixed-CSE and observed-spill aliases are not guessed, and FF010 plus private FF018 evidence keeps opaque changes reviewable without exposing metadata, XML, or workbook values. A 15,291-byte workbook with a 6,000,781-byte metadata part and 1,000,000 ignored foreign children fell from 1.301 seconds / 134.6 MiB in 0.143.0 to 0.284 seconds / 47.4 MiB in 0.144.0. The release passed 1,058 tests, package checks, and fresh wheel/sdist installs. The 0.144 validation record has the boundary and artifact evidence.

The 0.145 release closes a shared compact-allocation path in Excel Table Definition XML. FormulaFence now streams every canonical xl/tables/*.xml part—including canonical orphan parts the Table Style scanner inventories—and every safe direct internal worksheet table relationship target before raw filter, Named Sheet View, external-data, XML Mapping, Table Style, or ordinary workbook readers can materialize it. Transitional and Strict relationship forms plus noncanonical safe targets are covered at 32,768 XML elements per part and 65,536 in aggregate. A well-formed overage stops at the stable semantic-reader preflight instead of producing a partial profile; malformed, missing, and non-XML optional targets retain their coverage diagnostics. A 26,997-byte workbook carrying an 11,000,385-byte table XML part with 1,000,000 ignored foreign children fell from 5.728 seconds / 261.2 MiB in 0.144.0 to a 0.060-second / 37.5 MiB hard rejection in 0.145.0. The release passed 1,065 tests, hosted CI, package checks, and fresh wheel/sdist installs. The 0.145 validation record has the full boundary and artifact evidence.

The 0.146 release closes a compact-allocation path in SpreadsheetML shared strings used by both FormulaFence’s raw rich-text scanner and the ordinary workbook reader. A shared-string table can legitimately contain many simple values, so FormulaFence preserves the 500,000-entry allowance while bounding the materialized shapes: 32,768 XML elements for one complete si item, 65,536 across complex/rich items, and 32,768 opaque direct sst-child elements per table / 65,536 in aggregate. The raw scanner now streams one direct item at a time and releases irrelevant root children; the preflight covers both the manifest-selected ordinary-reader table and any distinct relationship-selected raw table, so malformed selection disagreement cannot bypass the boundary. A well-formed overage stops with the stable status-2 safety error before a profile is produced. A controlled 85,611-byte workbook carrying 500,000 ignored shared-string root children expanded to 27.5 MB; the exact 0.145.0 wheel completed successfully in 2.928 seconds / 110.2 MiB with no warning, while 0.146 rejects it in 0.425 seconds / 38.0 MiB. The release passed 1,074 tests, hosted CI, package checks, and fresh wheel/sdist installs. The 0.146 validation record has the full boundary and artifact evidence.

The 0.147 release closes the matching compact-allocation path in worksheet-root XML. FormulaFence now matches the documented base Worksheet root-child grammar in selected transitional and Strict worksheet parts before raw worksheet scanners or the ordinary reader retain the tree. A complete direct root subtree outside that grammar is limited to 32,768 XML elements per worksheet and 65,536 in aggregate, while ordinary sheetData and named base controls keep their existing specialist budgets. A well-formed overage returns the stable status-2 safety-preflight error before a profile is produced. A controlled 87,143-byte workbook carrying 500,000 ignored foreign direct worksheet-root children expanded to 28.0 MB; the exact 0.146.0 wheel completed successfully in 18.774 seconds / 133.9 MiB with no warning, while 0.147 rejects it in 0.459 seconds / 37.6 MiB. The release passed 1,081 tests, hosted CI, package checks, and fresh wheel/sdist installs. The 0.147 validation record has the full boundary and artifact evidence.

The 0.148 release closes the adjacent compact-allocation path inside Worksheet extension lists. extLst is a documented Worksheet child, but it can carry arbitrary extension XML; FormulaFence now streams every SpreadsheetML extension-list subtree in selected transitional and Strict worksheets before raw worksheet scanners or the ordinary reader retain it. Direct or nested extension lists allow 32,768 XML elements per worksheet and 65,536 in aggregate, while ordinary sheetData and other named controls keep their specialist budgets. A well-formed overage returns the stable status-2 safety-preflight error before a profile is produced. A controlled 18,225-byte workbook carrying 500,000 foreign extension children expanded to 6.0 MB; the exact 0.147.0 wheel completed successfully in 16.513 seconds / 126.2 MiB, while 0.148 rejects it in 0.478 seconds / 37.4 MiB. The release passed 1,088 tests, hosted CI, package checks, and fresh wheel/sdist installs. The 0.148 validation record has the full boundary and artifact evidence.

The 0.149 release closes the equivalent compact-allocation path in chart-sheet and dialog-sheet XML. Those relationship-selected non-grid sheet parts hold controls rather than a cell grid, but both allow a documented extLst and raw protection/Custom View scans can materialize their complete trees; the workbook reader eagerly loads chart sheets and routes dialog sheets through its worksheet path. FormulaFence now streams every selected chart sheet and dialog sheet before either path starts, allowing 32,768 XML elements per part and 65,536 in aggregate while leaving chart DrawingML under its separate boundary. A well-formed overage returns the stable status-2 safety-preflight error before a profile is produced. Controlled 500,000-child fixtures expanded to 6.0 MB from 18,858-byte chart-sheet and 18,576-byte dialog-sheet packages; the exact 0.148.0 wheel completed successfully in 3.551 seconds / 90.3 MiB and 3.682 seconds / 90.7 MiB, while 0.149 rejects them in 0.386 seconds / 38.0 MiB and 0.407 seconds / 37.6 MiB. The release passed 1,099 tests, hosted CI, package checks, and fresh wheel/sdist installs. The 0.149 validation record has the full boundary and artifact evidence.

The 0.150 release closes the adjacent compact-allocation path in the bootstrap xl/workbook.xml part. The ordinary reader constructs that package tree all at once, and FormulaFence has raw tab and legacy Custom View scanners alongside it. FormulaFence now streams every local-name extLst subtree—including nested workbook-view and alternate-namespace forms—and a foreign direct root subtree before either reader starts, allowing 32,768 XML elements for each narrow boundary while named Workbook catalogs keep their established limits. A controlled 16,653-byte workbook with 500,000 extension children expanded to 6.0 MB; the exact 0.149.0 wheel accepted it in 28.430 seconds / 139.6 MiB, while 0.150 rejects it in 0.437 seconds / 37.1 MiB with the stable status-2 safety error. The release passed 1,112 tests, hosted CI, package checks, and fresh wheel/sdist installs. The 0.150 validation record has the boundary, fixture, and artifact evidence.

The 0.151 release closes the remaining compact-allocation paths in xl/styles.xml. The stylesheet reader constructs a complete tree before it decides whether a child is a style record, ignored foreign content, or an extension, so FormulaFence now streams every local-name extLst, foreign root subtree or root local name, ignored named-catalog child, and materialized style record first. Each narrow subtree allows 32,768 elements; materialized record descendants also share a 262,144-element budget while the existing named catalog limits, including Excel's 65,490 effective styles, remain intact. Seven controlled 500,000-child fixtures exposed the difference: the exact 0.150 wheel accepted all of them, peaking at 416.5 MiB RSS for repeated font children, while 0.151 rejects every fixture before profile output in 0.508–0.589 seconds / 37.9–38.6 MiB. The release passed 1,133 tests, hosted CI, package checks, and fresh wheel/sdist installs. The 0.151 validation record has the full fixture and artifact evidence.

The 0.152 release closes the parser-frontier allocation gap that remains before
an XML element-count callback: ElementTree has already built an element's
complete attribute map when it emits a start event. FormulaFence now lexically
limits every physical XML opening tag to 128 KiB before reader-visible streams,
bounded raw structure scans, or in-memory OOXML root parsing reach the XML
parser. The streaming check handles UTF-8/ASCII-compatible parts plus UTF-16
and UTF-32, while respecting quoted delimiters, comments, CDATA, processing
instructions, and declarations. A compact Styles root or nested cell-format
record carrying 500,000 distinct attributes previously reached 211–213 MiB and
over six seconds in the exact 0.151 wheel; 0.152 rejects both before parser
entry in about 0.01 seconds at about 34 MiB, with the stable status-2 safety
error. The release passed 1,147 tests, hosted CI, package checks, and fresh
wheel/sdist installs. The 0.152 validation record
has the exact fixtures and artifact evidence.

The 0.153 release closes the adjacent XML text-node allocation gap. ElementTree
supplies character data while parsing, but its ordinary tree builder joins chunks
for one text or tail node before an end-event check can inspect it. FormulaFence
now caps each decoded node at 1 MiB before a chunk reaches that builder, across
the semantic-reader preflight, raw XML scans, and in-memory root reads. A
controlled Styles payload with one ignored 20,000,000-character opaque node was
accepted by the exact 0.152 wheel in 1.50 seconds / 88.8 MiB; 0.153 rejects it
in 0.088 seconds / 35.5 MiB with the same status-2 safety boundary. The release
passed 1,153 tests, hosted CI, package checks, and fresh wheel/sdist installs.
The 0.153 validation record
has the fixture and artifact evidence.

The 0.154 release closes the remaining XML parser-frontier gap for ignored lexical tokens. Before an XML parser starts, FormulaFence now caps each comment, processing instruction, declaration, end tag, and entity reference at 128 KiB; opening tags retain their separate boundary, CDATA stays under the 1 MiB decoded-data guard, and the shared defused parser explicitly rejects document-type declarations. Controlled 20,000,000-character Styles comment, processing-instruction, and document-type-comment fixtures were accepted by the exact 0.153 wheel at 130.8, 149.5, and 130.9 MiB RSS; 0.154 rejects them before parser entry in about 0.32 seconds at roughly 38 MiB. The release passed 1,193 source tests, hosted CI and the public Action contract, package checks, and fresh wheel/sdist installs. The 0.154 validation record has the exact fixtures, boundaries, and artifact evidence.

The 0.155 release closes a compact nested-archive expansion path in Power Query DataMashup packages. The outer Custom XML can be small while its embedded logical ZIP declares much more material to inflate and hash. FormulaFence now checks that nested ZIP before any member read: 768 KiB of package source, stored/deflated entries only, 512 entries across the scan, 16 MiB per member, 64 MiB aggregate expansion, and a 1,000:1 maximum ratio. An unsafe package remains a private opaque fingerprint plus an explicit coverage warning, so a candidate diff emits FF010/FF024 rather than quietly treating uninspected package material as equivalent. A 537,768-byte inner ZIP with 128 zero-filled 4 MiB entries declared 536,870,947 bytes; the public 0.154 wheel accepted it in 2.74 seconds, while 0.155 returned coverage evidence in 0.49 seconds, essentially matching the 0.47-second normal control. The release passed 1,197 source tests, package checks, and fresh wheel/sdist installs. The 0.155 validation record has the reproducible boundary and artifact evidence.

The 0.156 release closes the remaining nested-ZIP catalog path in Power Query DataMashup metadata. Counting embedded content with ZipFile.infolist() creates a Python entry object for every central-directory record even when no content is read. FormulaFence now scans bounded central-directory metadata first, shares the 512-part ceiling across logical packages and metadata catalogs, caps raw names at 1 KiB, and keeps ZIP64, multi-disk, malformed, or filename-rewriting metadata as explicit opaque coverage evidence. A 769,622-byte metadata ZIP with 7,400 empty entries made the public 0.155 wheel report 7,401 parts with no warning at 42.6 MiB RSS; 0.156 stops at the boundary in about 0.16 seconds / 39.0 MiB and retains FF010/FF024 visibility. The release passed 1,207 source tests, hosted CI, package checks, and fresh wheel/sdist installs. The 0.156 validation record has the fixture, exact measurements, and artifact evidence.

The 0.157 release treats policy-as-code as a bounded CI input too. It accepts one strict UTF-8 YAML document with a 1 MiB source ceiling, 4,096 nodes, 64 nesting levels, 4,096-character scalars, and 512 selectors per selector list. Duplicate keys, anchors, aliases, and merge keys now fail closed instead of allowing a last-wins or inherited setting to quietly weaken review; check loads the policy before either workbook. A 20,971,553-byte hostile policy took the public 0.156 wheel 7.25 seconds and 98.8 MiB RSS before its schema error, while 0.157 rejects it in 0.34 seconds at 38.7 MiB before YAML construction or workbook inspection; the 46-byte normal control remains about 0.45 seconds. The release passed 1,219 source tests, hosted CI, package checks, and fresh wheel/sdist installs. The 0.157 validation record has the fixture, exact measurements, and artifact evidence.

The 0.158 release makes every workbook snapshot inspect one bounded private source copy: archive preflight, semantic-reader gating, raw OOXML scans, the ordinary reader, and its snapshot hash now all use the same bytes while reports retain the requested path. That closes the source-path replacement window where a preflighted file could be swapped before later evidence. The existing 1 GiB source ceiling is enforced during copying; non-regular sources fail closed and private copies are removed on success or failure. A controlled replacement after the public 0.157 preflight made its snapshot report the replacement; changing the public path just after 0.158 made its private copy left the snapshot on the original bytes. Ten complete normal snapshots took 0.710387 seconds with 0.157 and 0.716368 seconds with 0.158. The release passed 1,221 source tests, hosted CI and Action-contract checks, plus clean public wheel/sdist installs. The 0.158 validation record has the exact boundary, measurements, and artifact evidence.

The 0.159 release closes a policy-file pathname race before workbook inspection. FormulaFence now opens one descriptor, requests nonblocking mode where the host provides it, verifies it is regular, and reads its bounded policy source from that descriptor rather than reopening a path. On a host with POSIX nonblocking descriptor opens, replacing a valid policy with a FIFO made the public 0.158 wheel block for the 1.000191-second control alarm; 0.159 rejected it as non-regular in 0.000094 seconds without YAML construction. A 46-byte normal policy remained inexpensive: 250 parses took 0.068762 seconds in 0.158 and 0.060042 seconds in 0.159. The release passed 1,222 source tests, hosted CI and Action-contract checks, plus clean public wheel/sdist installs that exercised the FIFO control. The 0.159 validation record has the exact boundary, measurements, and artifact evidence.

The 0.160 release closes the final output-path race for CLI reports and init starter policies. FormulaFence writes a private temporary file in the destination directory and atomically replaces the final pathname, so a final-component symlink or hard link substituted after input checks is replaced rather than followed into an inspected workbook or policy. In the controlled check, 0.159 exited successfully after changing the workbook and leaving the report as a symlink; 0.160 preserved the workbook and published a regular report. Ten JSON profiles of the 5,301-byte normal workbook took 0.744942 seconds in 0.159 and 0.744002 seconds in 0.160. The release passed 1,225 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean public wheel/sdist installs that exercised both late-swap controls. The 0.160 validation record has the precise boundary, measurements, and artifact evidence.

The 0.161 release makes the default init no-overwrite promise atomic. Earlier code checked that a starter-policy pathname was absent, then atomically replaced that pathname; a regular file, symlink, or hard-link directory entry created in the gap could still be replaced without --force. FormulaFence now writes the complete policy privately and atomically claims the final name without replacement, so a competing entry is preserved and the command exits 2 with the existing --force guidance. Against the public 0.160 wheel, the controlled normal-file race exited 0 with concurrent_preserved=False; 0.161 exited 2 with concurrent_preserved=True. One thousand ordinary initializations took 1.423195 seconds in 0.160 and 1.439989 seconds in 0.161. The release passed 1,229 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean public wheel/sdist installs covering report output, normal-file and hard-link races, and forced-symlink publication. The 0.161 validation record has the precise boundary and evidence.

The 0.162 release bounds every recursive portfolio directory inventory before FormulaFence retains or sorts paths. The default cap is 32,768 filesystem entries per supplied directory, independently tunable through --max-inventory-entries or the matching GitHub Action input; it includes ordinary non-workbook files, directories, Office lock files, symlinks, and supported workbooks. That closes a CI resource gap where the workbook limit could be small while a vast non-workbook directory was still collected and sorted. In the controlled 10,000-text-file case, the public 0.161 wheel finished its uncapped inventory in 0.203777 seconds at 41,620 KiB RSS; 0.162 rejected the 128-entry boundary in 0.004794 seconds at 38,636 KiB. The release passed 1,234 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean public wheel/sdist installs covering normal and hostile policies, exact and overflow inventory limits, and output-safety controls. The 0.162 validation record has the reproducible boundary and artifact evidence.

The 0.163 release binds a portfolio workbook read to the regular-file identity and state observed while FormulaFence inventories its relative path. Before the existing private snapshot is made, it requests a no-follow final component where supported and verifies the opened descriptor’s device, inode, change timestamp, and size. A late in-place rewrite, new regular file, or symlink replacement now becomes redacted FF078 incomplete evidence and exit code 2 instead of silently inspecting a different workbook. In the controlled replacement, the public 0.162 wheel reported incomplete=False, status=changed; 0.163 reported incomplete=True, status=unreadable, findings=FF078. Twenty ordinary portfolio comparisons took 2.100858 seconds at 37,504 KiB RSS in 0.162 and 2.148245 seconds at 37,540 KiB in 0.163. The release passed 1,238 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean public wheel/sdist installs covering policy, inventory, and late-source-change controls. The 0.163 validation record has the precise boundary and artifact evidence.

The 0.164 release makes recursive portfolio traversal fail closed when a directory cannot be read. FormulaFence now uses a bounded direct scandir walk rather than relying on recursive glob behavior that can suppress a filesystem error as an empty subtree; any directory-read or entry-type error stops comparison before a report can imply complete coverage. The existing raw-entry ceiling, symlink refusal, workbook limits, and source-identity checks remain in force. With a controlled unreadable nested directory, the public 0.163 wheel succeeded and retained model.xlsx; 0.164 returned Could not inventory baseline portfolio directory. with exit code 2. Five ordinary 10,000-entry inventories took 0.827227 seconds at 43,780 KiB RSS in 0.163 and 0.820001 seconds at 46,476 KiB in 0.164. The release passed 1,239 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean public wheel/sdist installs covering policy, inventory, and blocked-subtree controls. The 0.164 validation record has the exact boundary and artifact evidence.

The 0.165 release closes the next portfolio-scale resource gap. Each directory now has its own aggregate source-byte budget before FormulaFence opens any snapshot: the default is 4 GiB across inventoried regular .xlsx / .xlsm sources, configurable with --max-portfolio-source-bytes or the matching max-portfolio-source-bytes Action input. This prevents the 512-workbook ceiling from multiplying the existing 1 GiB per-workbook allowance into an impractical CI read set. The preflight uses the size captured with each already-inventoried file identity, so it does not reopen a pathname and the later guarded read retains the existing replacement protection. Exact-boundary and one-byte-overflow controls prove that both baseline and candidate sides stop before snapshot reads. Twenty complete 16-workbook comparisons took 73.563222 seconds / 40,632 KiB maximum RSS in the public 0.164 wheel and 73.085537 seconds / 40,444 KiB in 0.165. The release passed 1,246 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean wheel/sdist installs. The 0.165 validation record has the full boundary and artifact evidence.

The 0.166 release bounds semantic state retained during a directory comparison. Each baseline and candidate side has an independent 2,000,000-populated-cell budget, configurable with --max-portfolio-snapshot-cells or the matching max-portfolio-snapshot-cells Action input. Unlike the source-byte preflight, populated cells are only known once an individual bounded snapshot exists, so FormulaFence records that cost immediately and stops before opening later sources once a side has crossed its budget. This limits nested diff evidence and the candidate cross-workbook graph without conflating it with the 500,000-cell individual-reader cap or 4 GiB aggregate source-byte cap. In a six-workbook compact 20,000-cell control, the public 0.165 wheel completed all snapshots in 89.803903 seconds / 105,888 KiB; 0.166 with a 20,000-cell side budget stopped at 40,000 baseline cells in 47.647901 seconds / 82,872 KiB. The release passed 1,252 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean wheel/sdist installs with exact-limit and overflow controls. The 0.166 validation record has the full boundary and artifact evidence.

The 0.167 release bounds aggregate local dependency analysis independently from the reader, portfolio snapshot-cell, and cross-workbook graph budgets. One 100,000-state pool now spans every changed source in diff / check and every matched workbook in a portfolio; each changed source and reachable static local dependent consumes a state. Configure it with --max-change-analysis-states or the matching Action input. An overage returns exit code 2 before a partial impact report can imply complete evidence. Shortest paths are reconstructed lazily only for the fixed rendered sample, avoiding eager quadratic allocation of every prefix in a long chain. In a 250-input / 800-formula-chain control, the public 0.166 wheel completed 200,250 states in 12.662560 seconds and wrote a 2,310,669-byte report; the 0.167 candidate failed closed in 2.436415 seconds with a 10,000-state limit and in 3.358328 seconds at its default limit. The release passed 1,260 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean wheel/sdist installs with exact-limit and overage controls. The 0.167 validation record has the full boundary and artifact evidence.

The 0.168 release bounds final comparison artifacts independently from workbook input, local-impact, portfolio snapshot, and cross-workbook graph budgets. diff, check, and portfolio now default to a 32 MiB UTF-8 ceiling, configurable with --max-report-bytes or the matching Action input. JSON and SARIF count incremental encoder chunks, Markdown streams lines, and HTML accounts for escaped review entries; an overage returns exit code 2 before FormulaFence writes or replaces the requested output. In a control of two 78,679-byte / 78,683-byte packages with 1,000 changed 32,767-character cells, the public 0.167 wheel wrote a 66,287,419-byte JSON report in 32.719226 seconds. Version 0.168 rejected the same artifact at its default ceiling in 32.398541 seconds with no report, while an exact opt-up wrote the same 66,287,419 bytes in 32.726499 seconds. The release passed 1,269 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean wheel/sdist installs with normal and one-byte no-output rejection controls. The 0.168 validation record has the complete boundary evidence.

The 0.169 release completes the artifact boundary for profile. A profile deliberately publishes safe structural and coverage metadata, including one location/function record for each dynamic reference; a compressed workbook at the allowed cell count can therefore still become an impractical JSON or Markdown file. profile now accepts the same default 32 MiB --max-report-bytes ceiling and fails with exit code 2 before it publishes an over-limit artifact. This is not truncation: a reviewer can deliberately supply a known larger budget. In a valid 2,590,768-byte workbook with 500,000 repeated INDIRECT formulas, the public 0.168 wheel wrote a 65,429,579-byte profile in 6m44.010s. Version 0.169 rejected it in 7m06.956s with no report at the default ceiling; an exact 65,429,579-byte opt-up completed in 6m59.999s and was byte-for-byte identical. The release passed 1,272 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean wheel/sdist installs with normal JSON and one-byte Markdown no-output controls. The 0.169 validation record has the complete boundary evidence.

The 0.170 release closes the retained-state gap that comes before profile rendering. A profile can intentionally keep one safe location and function record for every dynamic formula, while the 32 MiB artifact ceiling is only checked after that public object has been built. profile now preflights every serialized profile-list item with a separate positive --max-profile-records budget, defaulting to 100,000. It covers top-level inventories and nested table columns, control ranges, parser warnings, token/function sequences, and dynamic-array references; an overage exits 2 before FormulaFence builds the profile or publishes output. This is not truncation: reviewers can deliberately supply a larger known count. On a valid 2,590,768-byte workbook with 500,000 repeated INDIRECT formulas, 0.170 rejected the default budget in 6m58.596s with no file. The complete profile has exactly 1,000,007 list records; that exact opt-up with the known 65,429,579-byte render budget completed in 7m14.598s and was byte-for-byte identical to the public 0.168 JSON. The release passed 1,274 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean public wheel/sdist installs. The 0.170 validation record has the complete boundary evidence.

The 0.171 release bounds the static local dependency graph retained during snapshot construction. A compact formula-defined name can resolve to many local inputs at every caller, so source-byte and populated-cell limits alone did not cap the reverse/range indexes used for impact review. profile, diff, check, and portfolio now accept a positive --max-dependency-edges limit of 2,000,000 by default, with the matching GitHub Action input. It counts retained direct/range records and additional fixed-CSE or observed dynamic-array output aliases while keeping ranges compact and separate from the candidate-only cross-workbook graph. Directory portfolios use independently shared baseline/candidate pools and stop before a later source read on overage; a CLI overage exits 2 before publication. In a 57,243-byte workbook whose 100-input Fanout name is called 10,000 times, 0.171 rejected 999,999 edges with no output and completed exactly 1,000,000 with byte-identical JSON. The release passed 1,282 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean public wheel/sdist installs. The 0.171 validation record has the full evidence.

The 0.172 release removes a reader-adjacent amplification path in formula-defined-name resolution. A compact workbook can carry a large valid name catalog; earlier versions rebuilt complete visible reference, named-LAMBDA, and sensitive-call lookup maps for every formula-valued name. FormulaFence now reuses scope-aware live overlays for visibility, preserving worksheet-local shadowing, qualified-name visibility, and explicit unresolved-call coverage instead of approximating them. On a 15,318-byte workbook with 4,000 formula-defined names, the public 0.171 wheel took 48.790797 seconds; 0.172 completed in 2.673606 seconds. The release passed 1,283 source tests, hosted CI and Action-contract checks, exact public-asset hash verification, and clean public wheel/sdist installs with byte-identical formula-name and named-LAMBDA profile fixtures. The 0.172 validation record has the full evidence.

The 0.173 release closes the complementary temporary-state gap in formula-defined-name safety ledgers. Separate action, DDE, custom-function, registration, XLM, and environment-information calls must remain distinct review evidence, but an acyclic chain formerly retained every inherited prefix with no dedicated ceiling. profile, diff, check, and portfolio now accept --max-formula-defined-name-states, defaulting to 1,000,000, with the matching GitHub Action input. The budget reserves direct sensitive-call entries, direct name-marker dependencies, and inherited component ledgers before they are materialized; portfolio sides each share an independent pool and stop before a later source is opened. On a valid 27,587-byte workbook with 4,000 chained action-bearing names, the public 0.172 wheel wrote a 131,806-byte profile in 5.742741037 seconds. Version 0.173 rejected its default limit in 3.171173740 seconds with no output; an explicit exact 8,009,999-state control produced byte-identical JSON. Hosted CI, the public Action contract, public asset hashes, and fresh wheel/sdist installs passed. The 0.173 validation record has the full boundary evidence.

The 0.174 release finishes compacting the private safety-marker path in that resolver. Even after the visibility overlays, a valid inactive catalog still retained eleven identity-to-marker maps and a reverse marker dictionary before a formula used any name. FormulaFence now generates a canonical private marker only on an actual lookup and recognizes only the bounded canonical form it generated, preserving deterministic propagation and the existing state accounting. On a valid 56,392-byte workbook with 20,000 static formula-defined names, the public 0.173 wheel produced its 519,802-byte JSON profile in 18.947417 seconds at 151,460 KiB; 0.174 produced byte-identical JSON in 18.548948 seconds at 115,436 KiB, a 23.8% resident-memory reduction. A 100-name action chain at its exact 5,249-state boundary remained byte-identical too. The 1,289-test suite, public assets, fresh wheel/sdist installs, hosted CI, and Action contract are all verified; the 0.174 validation record has the details.\n\nThe 0.175 release removes the remaining empty sensitive-ledger state from inactive formula-defined-name catalogs. FormulaFence now stores one sparse eleven-kind propagated ledger only for an affected component and presents it through a lazy scope-aware lookup; when no sensitive evidence exists it skips the empty propagation graph while retaining exact marker-dependency budget accounting. On the valid 20,000-name catalog, the public 0.174 wheel used 114,588 KiB; 0.175 produced the byte-identical profile at 68,284 KiB, 40.4% less. Action-bearing and benign exact-boundary chains, the full 1,290-test suite, fresh artifacts, hosted CI, and the Action contract were verified. The 0.175 validation record has the details.\n\nThe 0.176 release removes a remaining quadratic fixed-point cost in external formula-defined-name resolution. Static external endpoint definitions previously rebuilt and compared complete direct, derived, filtered, and alias-expanded maps after each newly resolved name. FormulaFence now exposes live filtered lookup views and follows aliases only for the requested name, while incremental reverse-alias wake-ups preserve direct/package alias chains and named-LAMBDA boundaries. On a valid 16,365-byte workbook with 4,000 static external formula-defined names, the public 0.175 wheel took 12.959426 seconds; 0.176 produced the byte-identical profile in 4.638464 seconds, 64.2% less elapsed time. The full 1,291-test suite, fresh public artifact install, hosted CI, and Action contract were verified. The 0.176 validation record has the details.

The 0.177 release removes a fixed visibility-reader cost that becomes material on ordinary multi-sheet workbooks. When raw worksheet XML has no <cols> declaration, FormulaFence no longer allocates four empty full-column state arrays and scans all 16,384 Excel columns merely to confirm that no column control exists. An all-columns-hidden defaultColWidth="0" declaration still produces the exact canonical control and 16,384-column count, while empty, malformed, and populated <cols> containers remain on the guarded parser path. On a controlled valid 512-sheet workbook with one formula per sheet, the public 0.176 wheel took 9.74 seconds; 0.177 produced the byte-identical profile in 7.72 seconds, about 20.7% less elapsed time. The 1,292-test source suite, hosted CI and Action contract, public asset hashes, and fresh wheel/sdist installs were verified. The 0.177 validation record has the exact fixture and artifact evidence.

The 0.178 release removes the same full-column scan from five style-control readers: number formats, fonts, fills, alignments, and borders. A worksheet without <cols> has no column-style override, so FormulaFence now keeps the workbook default implicit while retaining exact direct-cell and row-style comparisons. Populated and malformed column declarations remain on their existing guarded path. On the controlled 512-sheet workbook, the public 0.177 wheel took 7.74 seconds; 0.178 produced the byte-identical profile in 3.95 seconds, about 49.0% less elapsed time. Styled columnless profile and diff artifacts also remained byte-identical. The 1,293-test source suite, hosted CI and Action contract, public asset hashes, and fresh wheel/sdist installs were verified. The 0.178 validation record has the exact fixture and artifact evidence.

The 0.179 release makes one raw-OOXML safety boundary substantially faster without sharing mutable reader state. FormulaFence retains a small workbook, relationship, style, or worksheet payload only after the lexical markup gate accepts it, binds that cache to one private stable source, and reparses every hit with the character-data guard. The cache is capped at 128 KiB per part and 4 MiB in total; malformed, oversized, and unrelated archive parts never enter it. On the controlled 512-sheet workbook, the public 0.178 wheel had a 4.014589-second median profile time; 0.179 produced byte-identical JSON in 2.777051 seconds, about 30.8% less elapsed time. The 1,296-test suite, hosted CI and Action contract, public asset hashes, and fresh wheel/sdist installs were verified. The 0.179 validation record has the exact bounds and measurements.

The 0.180 release removes one remaining whole-column scan from worksheet-dimension inspection. When a visual worksheet has no <cols> declaration, FormulaFence keeps the all-default width and AutoFit state implicit rather than allocating and compressing 16,384 entries. Sheet defaults and row dimensions remain parsed, zero-width visibility stays in its separate control reader, and populated, empty, or malformed column declarations remain on the guarded path. On the controlled 512-sheet workbook, the public 0.179 wheel had a 2.396862-second median profile time; 0.180 produced the byte-identical JSON in 1.939342 seconds, about 19.1% less elapsed time. The 1,297-test suite, hosted CI and Action contract, public asset hashes, and fresh wheel/sdist installs were verified. The 0.180 validation record has the exact boundary and artifact evidence.

The 0.181 release reuses the next safe layer above XML payloads: immutable relationship and worksheet-part catalogs within one private stable workbook snapshot. Readers no longer repeatedly parse the same workbook relationship catalog or rebuild the same standard, visual, and display sheet maps. The cache only uses payloads already within the 128 KiB-per-part and 4 MiB-total boundary, caps relationship records at 2,048 and sheet records at 512, returns fresh mutable maps where needed, and clears derived entries if the character-data guard changes. On the controlled 512-sheet workbook, the public 0.180 wheel had a 1.942789-second median profile time; 0.181 produced the byte-identical JSON in 1.692732 seconds, about 12.9% less elapsed time. The 1,300-test suite, hosted CI and Action contract, public asset hashes, and fresh wheel/sdist installs were verified. The 0.181 validation record has the exact cache and artifact evidence.

The 0.182 release retains small parsed OOXML roots privately within one stable snapshot and deep-copies each result for raw readers, avoiding repeated parsing without shared mutable XML state. A root is eligible only after its source payload fits the existing 128 KiB-per-part and 4 MiB-total cache; it is further capped at 16 KiB, 2,048 elements per root, and 8,192 retained elements in total. Derived roots and catalogs reset if the character-data guard changes. On the controlled 512-sheet workbook, public 0.181 had a 1.687454-second median profile time; 0.182 emitted byte-identical JSON in 1.179587 seconds, about 30.1% less elapsed time. The 1,302-test suite, hosted CI and Action contract, public asset hashes, and fresh wheel/sdist installs were verified. The 0.182 validation record has the exact boundary and artifact evidence.

The 0.183 release lets four worksheet scanners reuse a small private root only after each independently applies its byte, part-count, and aggregate-reader budgets. Office Web Add-in, cell-hyperlink, sparkline, and native-image readers receive deep copies, avoiding repeated XML parsing without sharing mutable reader state. The established 128 KiB-per-payload, 4 MiB-total, 16 KiB-per-tree, 2,048-elements-per-tree, and 8,192-total-elements boundaries remain in force. On the controlled 512-sheet workbook, public 0.182 had a 1.517561-second median profile time; 0.183 emitted byte-identical JSON in 1.274422 seconds, about 16.02% less elapsed time. The 1,305-test suite, hosted CI and Action contract, public asset hashes, and fresh wheel/sdist installs were verified. The 0.183 validation record has the exact boundary and artifact evidence.

The 0.184 release adds a deliberately narrow review path for mistakes that precede version control: formulafence lint WORKBOOK. It flags an interruption only when two matching immediate formula peers and a third contiguous peer establish the same relative-copy pattern. A blank or stored error is high-severity FF082; a manual value is medium; a text marker is low; and a formula outlier is medium FF083. The lint does not calculate a workbook, retains coordinates rather than formula text, skips short or array-formula patterns, and fails closed when array metadata is incomplete. On a 48,037-formula public workbook in ExceLint test data, its 50 candidates were 44 low text markers and six medium manual values, so the high gate stayed quiet. The 1,322-test suite, fresh wheel/sdist installs, and public artifact hashes were verified. The 0.184 validation record has the detailed evidence.

The 0.185 release adds FF084, a separate static prompt for a pure local SUM, AVERAGE, MIN, MAX, or COUNT range that stops before two or more contiguous literal numeric cells on the same row or column. It is medium severity, retains only the function and range coordinates, and never calculates or prints the stored formula. The rule deliberately ignores named, table, external, 3-D, computed, multi-range, one-cell-gap, nonnumeric-gap, tokenizer-failure, and array-territory cases; its gap inspection is bounded to 128 cells by default. All ten public ExceLint workbooks, a finance ledger, and a compatibility file remained free of FF084 findings. The 48,037-formula public-debt workbook retained its 50 prior copied-pattern findings and zero aggregate omissions; five post-snapshot lint runs had a 0.473-second median. The 1,331-test suite, hosted CI, fresh wheel/sdist installs, and public artifact hashes were verified. The 0.185 validation record has the exact scope and evidence.

The 0.186 release adds FF085, a conservative warning for an ordinary formula cell that has an explicit direct locked=false assignment while its worksheet is actively protected. It is medium severity and retains only the cell location and direct-cell protection scope; it never prints or evaluates the formula. FormulaFence intentionally does not guess at row, column, default-style, or allowed-edit-range protection precedence, because those broader cases need a complete effective-style model. The candidate was quiet across 419 safely readable generated fixtures and 12 independent public workbooks; the latter had no actively protected worksheet. The 1,336-test suite, hosted CI, fresh wheel/sdist installs, and public artifact hashes were verified. The 0.186 validation record has the exact scope and evidence.

The 0.187 release adds FF086, a narrow calculation-freshness warning for a workbook that has at least one formula and explicitly stores calcMode=manual plus calcCompleted=false. It is medium severity and retains only those two calculation-status facts; it never evaluates a formula or prints a formula or cached value. Manual mode alone, automatic calculation, completed or omitted completion metadata, and formula-free workbooks stay quiet, so this is a review prompt rather than a claim that a saved result is incorrect. The candidate was quiet across 419 safely readable generated fixtures and 12 independent public workbooks containing 50,367 formulas. The 1,341-test suite, hosted CI, fresh wheel/sdist installs, and public artifact hashes were verified. The 0.187 validation record has the exact scope and evidence.

The 0.188 release adds FF087, a high-severity direct circular-reference prompt for an ordinary formula whose resolved scalar static dependency returns to the same cell while calculation iteration is disabled. It retains only the cell location, disabled-iteration fact, and direct-static scope; it never evaluates a formula or prints formula text or cached values. Enabled iteration, indirect cycles, static ranges, dynamic references, spill references, explicit intersection, and array territory stay quiet, so FormulaFence does not pretend to solve a general calculation graph. The candidate was quiet across 438 generated fixtures and 12 independent public workbooks containing 50,367 formulas. The 1,346-test suite, hosted CI, fresh wheel/sdist installs, and public artifact hashes were verified. The 0.188 validation record has the exact boundary and evidence.

The 0.189 release adds FF088, a critical prompt for a stored formula whose tokenized syntax contains an actual #REF! error operand. It retains only the cell location; FormulaFence never evaluates the formula or prints its text or a cached value. The rule is exact rather than substring-based: text literals, INDIRECT text arguments, valid quoted worksheet names, and tokenization failures stay quiet. An IFERROR wrapper does not erase the stored broken operand, so it remains reviewable. The candidate found one FF088 finding across 438 generated fixtures and six across 12 independent public workbooks containing 50,362 stored formula cells. The 1,353-test suite, hosted CI, fresh wheel/sdist installs, and public artifact hashes were verified. See the 0.189 validation record.

The 0.190 release adds FF089, a high-severity prompt for a formula whose well-formed saved result records a broken-reference error. This observes the last saved display state; it never evaluates the formula or claims that a fresh calculation would still fail. The boundary is deliberately narrow: only an exact saved #REF! result qualifies, while generic cached errors, missing or malformed cache records, and locations already covered by critical FF088 stay quiet. Evidence retains only the formula location, never its text or cached value. Across 12 independent public workbooks with 50,362 stored formula cells, the candidate found 54 saved #REF! results and emitted 50 FF089 findings after de-duplication. The 1,357-test suite, hosted CI, fresh wheel/sdist installs, and public artifact hashes were verified. See the 0.190 validation record.

The 0.191 release adds FF090, a high-severity prompt for an ordinary formula that belongs to a proven multi-cell static circular-reference component while calculation iteration is disabled. It traverses FormulaFence’s existing resolved scalar dependency graph without evaluating formulas or expanding ranges. The boundary keeps ordinary review low-noise: direct self-references remain under FF087, and iteration-enabled, dynamic-reference, 3-D, spill, explicit-intersection, array, and tokenizer-failure territory stays quiet. Findings retain only each location, scope, and component size—never formula text, peer edges, or cached results. Across 11 independent public workbooks with 50,357 formula cells, FF090 emitted zero findings. The 1,365-test suite, hosted CI, fresh wheel/sdist installs, and public artifact hashes were verified. See the 0.191 validation record.

The 0.192 release adds FF091, a medium-severity prompt when a workbook stores recognized Excel error-checking suppressions that can hide review prompts. It uses FormulaFence’s hardened raw-OOXML inventory but retains only aggregate warning categories, suppression-rule counts, and target-range counts—never individual ranges, worksheet names, formula text, values, or cached results. The rule does not decide whether Excel would show a prompt or whether a suppression was justified. Across 12 independent public workbooks with 50,367 formula cells, the full lint found zero recognized ignored-error rules and zero FF091 findings. The 1,369-test suite, hosted CI, fresh wheel/sdist installs, and public artifact hashes were verified. See the 0.192 validation record.

The 0.193 release adds FF092, a medium-severity prompt for an isolated interior exception in an Excel Table calculated column. It fingerprints the stored scalar master formula at the first data row and requires immediate eligible formula peers above and below the exception to match it, without evaluating any formula. That admits both A1-relative and structured-reference masters while keeping Table edges, arrays, explicit #REF! formulas, uninspectable formulas, and broader exception runs outside the boundary. A finding retains only the cell location, exception kind, and peer count—never the Table identity, master formula, or exception formula. Across 15 independent public workbooks with 51,094 formula cells, 34 Tables, and 10 eligible stored masters, the candidate emitted zero FF092 findings. The 1,383-test suite, hosted CI, fresh wheel/sdist installs, public artifact hashes, and a clean public-download install were verified. See the 0.193 validation record.

The 0.194 release adds FF093, a high-severity prompt for a native SUMIFS or COUNTIFS call whose direct static range arguments have different dimensions. Excel requires those corresponding ranges to share a shape; FormulaFence checks only calls with valid arity and individually bounded internal A1 cell/range or whole-column arguments, never evaluates a formula, and leaves names, Tables, external or 3-D references, full rows, unions, computed/dynamic/spill/implicit forms, malformed calls, explicit #REF! operands, and array territory outside the boundary. Evidence retains only the formula-cell location and aggregate call/range-mismatch counts. Across 454 successfully loaded independent public workbooks with 60,823 formula cells, the full lint emitted zero FF093 findings. The 1,391-test suite, hosted CI, and fresh wheel/sdist installs were verified. See the 0.194 validation record.

The 0.195 release extends FF093 to direct static AVERAGEIFS, MAXIFS, and MINIFS calls alongside SUMIFS and COUNTIFS. It recognizes only Excel native spellings and the exact standardized _xlfn.MAXIFS and _xlfn.MINIFS serializations, while arbitrary namespaces remain outside the boundary. Valid arity and individually direct bounded internal A1 or whole-column ranges are still required; FormulaFence compares dimensions without evaluation and emits only aggregate evidence. A replay of 469 public workbooks again produced zero FF093 findings, and public Highline College course workbooks exercised a native AVERAGEIFS plus serialized MAXIFS and MINIFS without a false positive. The 1,391-test suite, hosted CI, fresh wheel/sdist installs, public artifact hashes, and clean public-download installs were verified. See the 0.195 validation record.

The 0.196 release adds FF094, a high-severity prompt for an unqualified native SUMPRODUCT call whose direct static range arguments have different dimensions. Excel documents the resulting #VALUE! boundary, so FormulaFence accepts only calls with at least two comma-separated, individually direct bounded internal A1 or whole-column arguments, optionally after @. It leaves computed arrays, names, Tables, external or 3-D references, full rows, unions, dynamic/spill/implicit forms, malformed calls, explicit #REF! operands, array territory, and arbitrary namespaces outside the rule. The evidence has only aggregate call and mismatched-array counts. The 1,400-test suite, hosted CI, fresh wheel/sdist installs, public artifact hashes, and clean public-download installs were verified. A 469-workbook replay emitted zero FF094 findings; three public teaching workbooks exercised eight real SUMPRODUCT calls without a false positive. See the 0.196 validation record.

The 0.197 release adds FF095, a high-severity prompt for a native MMULT call whose two direct static arrays have incompatible inner dimensions. Excel documents the matching column/row requirement and its #VALUE! boundary. FormulaFence accepts exactly two direct bounded internal A1 or whole-column arguments, optionally after @, and compares only the left array's column count with the right array's row count. It does not calculate a formula or inspect values; names, Tables, external or 3-D references, full rows, unions, computed/dynamic/spill/implicit forms, malformed calls, explicit #REF! operands, array territory, and arbitrary namespaces stay outside the rule. Evidence retains only aggregate call and incompatible-matrix-pair counts. The 1,408-test suite, hosted CI, fresh wheel/sdist installs, and a public-download install were verified. A 469-workbook replay found one valid native MMULT call and zero FF095 findings. See the 0.197 validation record.

The 0.198 release adds FF096, a high-severity prompt for native VLOOKUP or HLOOKUP calls whose literal return index falls outside a direct static table range. Excel documents VLOOKUP's column-index #REF! boundary, and its HLOOKUP reference gives the equivalent row-index rule. FormulaFence accepts only three- or four-argument calls with a direct bounded internal A1 or whole-column table plus a direct positive integer index, optionally after @. It does not calculate a formula or inspect lookup/table values; names, Tables, external or 3-D references, full rows, unions, computed/dynamic/spill/implicit forms, nonliteral or nonpositive indices, malformed calls, explicit #REF! operands, array territory, and arbitrary namespaces remain outside the rule. Evidence retains only aggregate call and out-of-range-literal-index counts. The 1,416-test suite, hosted CI, fresh wheel/sdist installs, and a public-download install were verified. A 469-workbook replay saw 3,244 native legacy lookup calls, including 3,153 direct-table/literal-index calls all within range, and zero FF096 findings. See the 0.198 validation record.

The 0.199 release adds FF097, a high-severity prompt for a native CHOOSE call whose bare decimal index is zero or exceeds its supplied value-argument count. Excel documents the resulting #VALUE! boundary. FormulaFence accepts only an unqualified native call with one through 254 nonempty value arguments, optionally after @; it does not calculate a formula or inspect selected values. Computed, signed, decimal, array, dynamic, malformed, explicit #REF!, array-territory, and arbitrary-namespace cases remain outside the rule. Evidence retains only aggregate call and out-of-range-literal-index counts. The 1,424-test suite, hosted CI, and fresh public wheel/sdist downloads were verified. A 469-workbook full-lint replay loaded 454 workbooks, inspected 60,823 formula cells, saw 30 native CHOOSE calls with computed indexes, and emitted zero FF097 findings. See the 0.199 validation record.

The 0.200 release adds FF098, a high-severity prompt for a native RANDBETWEEN call whose direct literal bottom is above its top. Microsoft’s RANDBETWEEN reference defines bottom as the smallest returned integer and top as the largest, so FormulaFence reports only the statically provable impossible interval. It accepts exactly two direct decimal integer literals, optionally after @ and with one unary sign; it never calculates a random value or exposes either literal. Decimal/scientific, computed, reference, array, malformed, explicit #REF!, and namespaced forms remain outside the rule. The 1,432-test suite, hosted CI, and fresh public wheel/sdist downloads were verified. A 469-workbook full-lint replay loaded 454 workbooks, inspected 60,823 formula cells, saw 14 native RANDBETWEEN calls with correctly ordered direct integer bounds, and emitted zero FF098 findings. See the 0.200 validation record.

The 0.201 release adds FF099, a high-severity prompt for a native SUBTOTAL whose direct literal function number is outside Excel’s documented 111 and 101111 code families. Microsoft’s SUBTOTAL reference defines that argument and permits one required reference plus up to 254 optional references. FormulaFence accepts only an unqualified native call, optionally after @, with a bare nonnegative decimal code and one through 254 nonempty reference arguments; it does not calculate a subtotal or expose a function code, formula, or reference. Computed, signed, decimal, array, malformed, explicit #REF!, and namespaced forms remain outside the rule. The 1,440-test suite, hosted CI, and fresh public wheel/sdist downloads were verified. A 469-workbook full-lint replay loaded 454 workbooks, inspected 60,823 formula cells, saw 27 native SUBTOTAL calls with direct valid function numbers (three in the standard family and 24 in the hidden-row-ignoring family), and emitted zero FF099 findings. See the 0.201 validation record.

The 0.207 release adds FF105, a high-severity prompt for an infix division whose immediate divisor is a direct signed decimal integer zero. Microsoft’s #DIV/0! guidance documents the direct-zero boundary. FormulaFence does not calculate either operand; parenthesized, powered, percent, computed, reference, decimal/scientific, repeated-sign, malformed, explicit #REF!, and array forms remain quiet. An aggregate raw survey covered 14,649 infix division operators across 123,136 reconstructed public formulas; SpreadsheetBench had zero candidates, while the compatibility corpus had 11 statically visible candidates that are reported as syntax rather than assumed user defects. The 1,488-test suite, hosted CI, fresh wheel/sdist installs, and public-download wheel verification passed. See the 0.207 validation record.

The 0.208 release adds FF106, a high-severity prompt when a well-formed formula cache records an exact saved #DIV/0! result. It is evidence of the workbook's last saved display state, not a formula evaluation or a claim that the error would still occur after recalculation. FormulaFence keeps the boundary private: JSON, Markdown, and SARIF retain only a cell location and saved_formula_result scope, never a formula or cached value; other saved errors and malformed caches stay quiet, and an already-proven direct-literal-zero FF105 location is reported once. Microsoft's #DIV/0! guidance motivates the error boundary. A full safe-lint replay over verified SpreadsheetBench loaded 796 workbooks, inspected 68,497 formula cells, emitted 42 FF106 findings and zero FF105 findings, with zero lint rejections. The 1,491-test suite, hosted CI, fresh wheel/sdist installs, and public-download wheel verification passed. See the 0.208 validation record.

The 0.209 release adds FF107, a high-severity prompt when a well-formed formula cache records an exact saved #NUM! result. It is evidence of the workbook's last saved display state, not a formula evaluation, diagnosis of a numeric cause, or a claim that the error would still occur after recalculation. FormulaFence keeps the boundary private: JSON, Markdown, and SARIF retain only a cell location and saved_formula_result scope, never a formula or cached value; other saved errors and malformed caches stay quiet, and locations already proved by direct FF098 or FF103 checks are reported only by those more specific rules. Microsoft's #NUM! guidance motivates the error boundary. A full safe-lint replay over verified SpreadsheetBench loaded 796 workbooks, inspected 68,497 formula cells, emitted 137 FF107 findings and zero FF098/FF103 findings, with zero lint rejections. The 1,494-test suite, hosted CI, fresh wheel/sdist installs, and public-download wheel verification passed. See the 0.209 validation record.

The 0.210 release adds FF108, a high-severity prompt when a well-formed formula cache records an exact saved #NAME? result. It is evidence of the workbook's last saved display state, not a formula evaluation, diagnosis of a name/function/add-in cause, or claim that the error would still occur after recalculation. FormulaFence keeps the boundary private: JSON, Markdown, and SARIF retain only a cell location and saved_formula_result scope, never a formula or cached value; other saved errors and malformed caches stay quiet. Microsoft's #NAME? guidance motivates the error boundary. Candidate-only production replays safely loaded and linted all 19 full-corpus artifacts with saved name errors, emitting 33 FF108 findings with zero rejections; an independent compatibility replay covered five artifacts and 12 findings. The 1,497-test suite, hosted CI, fresh wheel/sdist installs, and public-download wheel verification passed. See the 0.210 validation record.

The 0.211 release adds FF109, a high-severity prompt when a well-formed formula cache records an exact saved #VALUE! result. It is evidence of the workbook's last saved display state, not a formula evaluation, diagnosis of a value-error cause, or claim that the error would still occur after recalculation. FormulaFence keeps the boundary private: JSON, Markdown, and SARIF retain only a cell location and saved_formula_result scope, never a formula or cached value; other saved errors and malformed caches stay quiet, and a location already proved by direct-static FF093 conditional-aggregate range-shape analysis is reported only by that more-specific finding. Candidate-only production replay safely loaded and linted all 71 full-corpus artifacts with saved value errors, classified 20,003 candidates, emitted 20,000 FF109 findings and three FF093 findings, with zero rejections. The 1,500-test suite, hosted CI, fresh wheel/sdist installs, and public-download wheel verification passed. See the 0.211 validation record.

FormulaFence 0.212.0: direct SUM overlap evidence

FormulaFence 0.212 adds FF110, a high-severity prompt for a native SUM
call whose direct static argument ranges overlap. It proves only that at least
one stored cell is included more than once; it does not read values, calculate
the formula, or decide whether the inclusion was intended. Microsoft documents
SUM's range and argument semantics
.

The rule accepts two through 255 bounded internal A1 cell/range arguments,
optionally after @, and resolves every referenced worksheet before comparing
same-sheet rectangles. Names, Tables, external/3-D, whole-column/full-row,
union, computed, dynamic, spill, argument-level implicit-intersection,
malformed, explicit #REF!, array, and unresolved-sheet forms remain quiet.
JSON, Markdown, and SARIF retain only an affected location plus aggregate
qualifying-call and overlapping-pair counts.

An exact public 5,464-artifact production replay loaded 5,446 workbooks,
selected 226 qualifying calls across 42 candidates, linted all 42, and emitted
zero FF110 findings under that deliberately strict boundary. The 1,509-test
suite, hosted CI, fresh wheel/sdist installs, and public-download wheel
verification passed. See the 0.212 validation record.

FormulaFence 0.213.0: direct AGGREGATE literal-argument evidence

FormulaFence 0.213 adds FF111, a high-severity prompt for a native AGGREGATE call with a direct literal function number outside 1-19, an option outside 0-7, or a function 14-19 that omits its required second reference. It recognizes Excel’s exact stored _xlfn.AGGREGATE form as well as the ordinary spelling, but it does not evaluate a formula or inspect referenced values. Computed, reference, decimal/scientific, array, malformed, explicit #REF!, array-territory, and arbitrary namespace forms remain quiet. JSON, Markdown, and SARIF retain only an affected location and aggregate error-class counts. An exact public replay streamed 5,464 workbook artifacts and 782,140 formula cells, selected 111 workbooks containing 6,549 qualifying calls, loaded and linted every candidate, and emitted zero FF111 findings under that deliberately strict boundary. The 1,518-test suite, fresh wheel/sdist checks, and a public-download wheel verification passed. See the 0.213 validation record.

FormulaFence 0.214.0: native MOD literal-zero-divisor evidence

FormulaFence 0.214 adds FF112, a high-severity prompt for a native MOD call with exactly two nonempty arguments when its direct signed integer divisor is zero. It recognizes optional @ but does not calculate the number argument or read referenced values. Computed, reference, decimal/scientific, array, malformed, explicit #REF!, array-territory, and arbitrary namespace forms remain quiet; a same-location FF112 supersedes a saved-result FF106 record. JSON, Markdown, and SARIF retain only an affected location and an aggregate zero-divisor-call count. A public survey streamed 5,464 workbook artifacts and 7,865 bounded worksheet XML parts, inspected 709,797 ordinary stored formula cells, saw 721 valid native calls including 640 direct integer divisors, and found zero direct literal zero divisors. A two-stage production replay loaded and linted all 36 candidate workbooks with zero rejections and zero FF112 findings. The 1,526-test suite, hosted CI, and fresh wheel/sdist checks passed. See the 0.214 validation record.

FormulaFence 0.215.0: native date-function literal-code evidence

FormulaFence 0.215 adds FF113, a high-severity prompt for native YEARFRAC, WEEKDAY, or WEEKNUM calls with an unsupported direct signed-integer code in their explicitly supplied code slot. The rule follows Microsoft's documented YEARFRAC, WEEKDAY, and WEEKNUM domains, without evaluating dates, formulas, or referenced values. Computed, reference, decimal/scientific, array, malformed, explicit #REF!, array-territory, and arbitrary namespace forms remain quiet; a same-location FF113 supersedes the generic saved #NUM! result. JSON, Markdown, and SARIF keep only a location plus aggregate error-class counts.

An aggregate-only SpreadsheetBench v0.1 survey expanded 1,200,528 ordinary public formula instances and saw 372 direct signed-integer date-function code slots, all valid. A separate production replay streamed 709,797 stored formula cells from 5,464 public workbook artifacts, selected 27 candidate workbooks containing 99 direct integer code slots, and loaded and linted every candidate with zero rejections and zero FF113 findings. The release source tree passed 1,534 tests, and fresh wheel and source-distribution installs passed. See the 0.215 validation record.

The current release is FormulaFence 0.215.0 on GitHub. The canonical version of this post lives at sybilgambleyyu.github.io/posts/formulafence.html.

Top comments (0)