ADR 0020 — Identifier arity is a hard boundary: a singular parameter never becomes a batch
- Status: Accepted
- Date: 2026-07-07
- Tracking: legacy tracker 2029 (PR legacy tracker 2035). Builds on ADR 0005 and the FlexIDs lineage (legacy tracker 881, legacy tracker 884, legacy tracker 1327).
Context
The FlexIDs lineage made identifier value shapes liberal: numbers
accepts 441, "441", [441], ["441"], and "441, 442", all normalized
to []string. ADR 0005 codified the complementary rule for names:
parameter names are strict (gh/tea/API-anchored), with only a bounded
did-you-mean. legacy tracker 2029 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 (legacy tracker 2029). - 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. legacy tracker 2030) 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.