Skip to main content

ADR 0042 - Rejected: runtime cell mutation (jx:updateCell style)

  • Status: rejected
  • Date: 2026-05-18
  • Spec target: XTL 0.1 (rejection; no spec change)
  • Affects: ADR-0034 (Corollary 3 application)

Context

JXLS exposes jx:updateCell — a directive that modifies the value of an existing cell during rendering, distinct from substitution via {{ ... }}. The directive targets a cell coordinate, supplies a value (often computed from a JEXL expression), and optionally supplies a predicate gating when the update fires. The same shape appears in several JXLS forks under different names (jx:setCell, jx:writeCell).

The natural XTL spelling would be something like:

{{ @update D5 = SUM([Amount]) WHEN [Status] = "FINAL" }}

or a function-form variant:

{{ @update D5 [Total] }}

This ADR records the decision not to ship that feature in XTL 0.1, 0.x, or on the roadmap to 1.0. The rejection is deliberate, not an oversight.

Per ADR-0034 Corollary 3, this is the next feature-level rejection ADR following ADR-0037 (dynamic image insertion).

Considered Options

A. Ship @update (or equivalent) matching JXLS's jx:updateCell. A new directive that, during render, overwrites the value at an explicit coordinate.

B. Reject — keep templates declarative. Adopted below. Author-side substitution ({{ ... }} written in the cell that should change) covers the use cases without introducing runtime mutation semantics.

C. Defer (no decision yet). Leaves the question open and recurring.

Decision

Adopt B. Runtime cell mutation is out of scope for XTL 0.1 and not on the roadmap to 1.0.

Why rejected

Five distinct objections, each sufficient on its own:

1. Conflicts with "template is the handover artifact"

README § "Why xl3 exists" pins the thesis: the template is the handover artifact — it can be reviewed, versioned, archived, and passed to the next operator without asking them to read the automation code. A directive that modifies cells the author wrote as static (no {{ }} markers) makes the template ambiguous in exactly the way the thesis forbids: the reader cannot tell, from the template alone, what the output will contain without simulating execution.

Substitution via {{ ... }} is the inverse: every cell that might change in the output carries a visible marker. A user opening the template in Excel sees, by inspection, which cells are dynamic and which are static. Runtime mutation breaks that invariant.

2. Substitution already covers the use case

Every use case jx:updateCell solves — conditional cell values, computed totals, status-dependent labels — is already expressible by writing the expression in the cell that should hold the value:

  • "If status is FINAL, show the total; else blank" → {{ IF([Status] = "FINAL", SUM([Amount]), "") }} in the target cell.
  • "Stamp a different label depending on a flag" → {{ IF([Flag], "Approved", "Pending") }} in the cell.
  • "Compute a value from multiple columns" → the formula goes in the cell.

The author makes the intent explicit by where they write it. @update D5 = … from a different cell hides intent: the reader of cell D5 sees a static "0" and has no signal that some other cell will overwrite it.

3. Cross-impl complexity is high

A faithful @update would need to:

  • Parse target-cell coordinates (absolute, relative, or named).
  • Evaluate predicate expressions in the same evaluation model as the rest of XTL.
  • Order updates against the rest of substitution (does the update fire before or after @repeat expansion? what if the target cell is inside an expanded block?).
  • Resolve conflicts when multiple @update directives target the same cell.

Each port (xl3-py, future Rust/Go) would replicate all four — significantly more state than substitution, which evaluates each cell independently. ADR-0034 Corollary 1 asks for cheap absorption; this is the opposite.

4. Encourages templates to depend on evaluation order

Runtime mutation tends to chain: "after cell A updates, cell B reads A's new value, then cell C reads B's new value." Once that chain forms, evaluation order is observable — the template's output depends on the order in which the engine visits cells. Substitution avoids this entirely: each {{ ... }} is a pure function of source data, and order is internal to the engine.

ADR-0016 (ordering and stability) explicitly limits the surfaces on which order is observable — output filename ordering, row ordering — precisely so authors don't write templates that silently break when the engine optimizes traversal. Runtime mutation would re-introduce the exact class of bug ADR-0016 is designed to prevent.

5. No demand signal

Korean operations templates (거래명세서, 정산서, 발주서, 인보이스 — the originating audience for XTL) have not surfaced a single case where substitution is insufficient and runtime mutation is the natural answer. Conditional cell content is handled by IF() in the cell; computed totals by SUM() in the cell; status labels by string-formula substitution. Adding @update would be a speculative feature — paying ~500 lines per port for a use case nobody has reported.

Future production users might surface such a case. If they do, this ADR can be reopened per GOVERNANCE.md § "Disagreements" — but the bar is concrete evidence, not analogy with JXLS.

What is not rejected

  • Substitution via {{ [Col] }} writing the column's value into the cell. Supported since 0.1.
  • Conditional cell content via {{ IF(condition, then, else) }}. Supported since 0.1.
  • Computed totals via {{ SUM([Amount]) }} and the rest of the function table. Supported since 0.1 per ADR-0024.
  • Multi-output via file-per-group / sheet-per-group (@file / @sheet). Supported since 0.1 per ADR-0012.
  • Future reconsideration if production users report runtime cell mutation as a real blocker, with a specific use case that substitution provably cannot express. A reopening would need to address all five objections above.

Consequences

  • Future proposers of runtime-mutation features can cite this ADR to know the bar (overcome all five objections) before drafting.
  • No new directive, no new function, no new evaluation phase in 0.1.
  • No conformance fixture is added — rejected ADRs do not need fixtures; the rejection IS the contract.
  • docs/internal/jxls-absorption-plan.md Category C gains an item linking to this ADR.
  • Listed in INFORMATIONAL_ADRS of src/__tests__/spec-coverage.test.ts so the ADR↔fixture coverage check passes.

References

  • ADR-0034 — Relationship to prior-art template engines (Corollary 3: some prior-art choices are explicitly out of scope)
  • ADR-0037 — Rejected: dynamic image insertion (precedent rejection following the same structure)
  • ADR-0016 — Ordering and stability (the surfaces this ADR protects from order-dependence)
  • ADR-0024 — Function arity (where IF, SUM, etc. live; the substitute-side answer to mutation's use cases)
  • README § "Why xl3 exists" (the thesis the rejection protects)
  • GOVERNANCE.md § "Disagreements" (revisability)