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
@repeatexpansion? what if the target cell is inside an expanded block?). - Resolve conflicts when multiple
@updatedirectives 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.mdCategory C gains an item linking to this ADR.- Listed in
INFORMATIONAL_ADRSofsrc/__tests__/spec-coverage.test.tsso 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)