An 'api' Field takes no gesture
Context
Field.editable: 'api' says one thing: the app writes this value, and the grid does not. But
canWrite (src/view/capability.ts:183) refuses only 'never' before it asks the consumer's rule
and the variant's rule (capability.ts:184-187). Either rule can answer WRITABLE. So
capabilities: { edit: true }, or a variant's edit: true, reopens an 'api' cell for the grid
editor, a bar move and a resize. The library's own last word, libraryWriteRule
(src/data/write-rule.ts:166-173), runs last and would refuse the same write — the consumer rule
just never reaches it.
A rolling-up cell has the same shape of gap. libraryWriteRule refuses a derived start/end on a
parent, but capabilities.edit: true opens the resize handle first. The drag paints, and the commit
throws DerivedFieldNotWritableError.
ADR 0015 left this derived case out of scope on purpose: its own capabilities.edit ruling "touches
only the editable-lock ('never') case", and names the derived trade-off as a different question for
#470 to answer. This record closes it, alongside
'api', because both are the same question — does the data layer's refusal outrank the consumer's
rule — and #470 already answered the part that could have made a rolling-up parent's cell writable at
all: it deleted writeToChildren from the Field surface, so no Field can turn a derived cell into one
a consumer's write splits onto the children. What survives #470 is the summary-bar move, and it never
asked the parent's own canWrite — entriesMovedBy moves the dated descendants below a rolling-up
parent directly (ADR 0013), gated by ownsField's structural answer, not by the parent cell's own
verdict. So this decision closes the resize handle and the parent's own cell, and the summary-bar move
keeps moving the descendants exactly as before.
Decision
The data layer's answer comes first. The consumer's rule and the variant's rule only narrow an
'anywhere' cell. They never widen 'api' or 'never'. A per-entry lock rule
(ctx.edits.setLockRule, ADR 0015) still may reopen a cell — every view sees its answer, so it is
not a narrowing exception, it is the effective editable itself.
canWrite reads five questions, in this order:
- Is there a stored home for this value?
- What is the effective
editable? A per-entry lock rule answers first;Field.editableanswers when the rule is silent. - Does the library refuse this cell? A derived cell refuses. Any effective
editableother than'anywhere'refuses. - What does the consumer's own rule say?
- What does the variant's own rule say?
Step 3 answers before step 4 and step 5 now. Today it answers last, so a 'never' refusal short-
circuits at step 2 and never reaches the derived question, while an 'api' cell and a derived cell
both fall through to the consumer and the variant.
A gesture is any interaction write: a grid cell, a drag, a resize, a keyboard nudge, or Delete on a
bar. Every one of them asks canWrite through the one resolver src/view/capability.ts owns, and no
gesture has a write path around it, so this decision closes the gap at every gesture, not only the
grid cell.
'never': no write, from any door. 'api': entries.update() only, never a gesture. 'anywhere':
code and gestures both.
Consequences
edit: truekeeps one meaning. InWriteRule,edit: truestill beats a variant'sedit: false— the consumer wins over the variant (capability.test.ts:344-350). Its own docs already say "no narrowing from this Gantt", never "turn editing on". Nothing about that meaning changes here.- An
EditExtendercascade still writes an'api'Field, including one a drag starts. A cascade is plugin code, so ADR 0015's cascade ruling already counts it as code, not a gesture. This ADR does not touch that door. - A refusal names its reason.
canWritenow returnslibraryWriteRule's verdict for an'api'or a'never'cell, not a bare unexplained refusal. For an'api'cell with no consumer rule, the verdict does not change — a derived parent cell already said'derived-value'. One cell's answer does change: a'never'rolling-up cell on a parent now carries the'derived-value'reason, where it used to refuse with no reason at all. No shipped behavior depended on the silent refusal. - Delete on a bar passes over an
'api'date the same way a drag does. It clearsstart/endthrough the samecanWritegate a move or a resize uses, so the fix reaches it without a separate rule. - A derived cell refuses before the consumer or the variant gets a say, the same as an
'api'or a'never'cell.edit: truepaints no resize handle on a rolling-up parent, and the commit that used to throwDerivedFieldNotWritableErrorno longer has a handle to reach it from.
Out of scope
parentIdships'anywhere'(#425). A vertical drag writes it through thereordercapability. The grid cell stays dead because the inline editor offers no editor for anentryIdField, not because ofeditable. Root cause:0abead8cset'api'only to keep a text editor off aparentIdcolumn; this ADR then made'api'mean "no gesture", which locked drag re-parenting as a side effect.entries.update()'s own thresholds.data/write-rule.tsdoes not change; only the orderview/capability.tsreads it in changes.- Delete on a row, undo/redo, and column gestures.
Field.editablenever governed them, and still does not.