ADR 0020 - Identifier arity is a hard boundary
- Status: Accepted
- Date: 2026-07-07
- Tracking: internal pre-Codeberg tracker (2029 / PR 2035; builds on ADR 0005 and the FlexIDs lineage 881, 884, 1327)
Context
The FlexIDs lineage made identifier value shapes liberal: multiple input forms for a number list are normalized to []string. For example, numbers accepts 441, "441", [441], ["441"], and "441, 442" interchangeably. ADR 0005 codified the complementary rule for names:
parameter names are strict (gh/tea/API-anchored), with only a bounded
did-you-mean. The identifier arity sweep extended shape liberality across operation arity in one
direction: every single-target action accepts a single-element numbers=[N]
as equivalent to number=N (resolveOneNumber), because that input’s
meaning is unambiguous.
That sweep leaves one deliberate asymmetry, which this ADR records so it is
never “fixed” as a bug: the singular number parameter does not
comma-split. number="1,2" is one (malformed) identifier that fails loudly
downstream — it is never reinterpreted as two.
Decision
Arity is declared by the parameter type — FlexID (singular) or FlexIDs
(plural) — and is a hard boundary. Shape coercion is liberal within a
declared arity and never across it:
- A singular parameter never coerces to a batch.
FlexIDaccepts a string or a number; it never splits commas and never accepts an array.number="1,2"stays one invalid identifier and fails loudly. - A plural parameter naming one thing is valid everywhere. A
single-element
numbers=[N]is accepted by every action that takesnumber=N, batch or single-target. - A plural with multiple elements on a single-target operation is a
teaching error, never a partial application.
resolveOneNumberrejects multi-element input naming both accepted forms; there is no first-element-wins.
Rationale
The direction of coercion is what matters. Widening the shapes a plural accepts (scalar → one-element list, comma-string → list) is safe: intent is unambiguous and the operation’s cardinality is unchanged. Widening a singular’s arity is not safe in either direction:
- Up-coercion (splitting
number="1,2"into a batch) can silently turn one intended mutation into N — a close, delete, or merge fanning out beyond what the caller reviewed. The liberal-shapes principle is “coerce when meaning is clear”; a plural value in a singular slot is precisely where meaning is NOT clear (typo’d paste? literal identifier? intended batch?). - Down-coercion (taking the first element of a multi-element
numberson a single-target action) silently drops work the caller asked for.
Failing loudly with an error that names both accepted forms costs one round-trip in the rare ambiguous case; either silent coercion costs a wrong mutation in the same case.
Consequences
FlexIDnever gains comma-splitting or array acceptance; its doc comment references this ADR. A future “agents keep passing comma-strings innumber” paper cut resolves by improving the error/description (ADR 0005’s proactive lever), not by splitting.- New identifier parameters choose
FlexIDvsFlexIDsby the operation’s true cardinality, and route through the shared helpers:collectNumbersfor batch actions,resolveOneNumberfor single-target ones — never a bespoke merge. - Batch promotion (an action’s cardinality changing from one to many, e.g. promoting a close action from single to batch) is an explicit per-action semantic decision — switching helper and spec — never an emergent property of input coercion.
Canonical source: docs/adr/0020-identifier-arity-boundary.md in the madtea repo.