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:

  1. A singular parameter never coerces to a batch. FlexID accepts a string or a number; it never splits commas and never accepts an array. number="1,2" stays one invalid identifier and fails loudly.
  2. A plural parameter naming one thing is valid everywhere. A single-element numbers=[N] is accepted by every action that takes number=N, batch or single-target (legacy tracker 2029).
  3. A plural with multiple elements on a single-target operation is a teaching error, never a partial application. resolveOneNumber rejects 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 numbers on 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

  • FlexID never gains comma-splitting or array acceptance; its doc comment references this ADR. A future “agents keep passing comma-strings in number” paper cut resolves by improving the error/description (ADR 0005’s proactive lever), not by splitting.
  • New identifier parameters choose FlexID vs FlexIDs by the operation’s true cardinality, and route through the shared helpers: collectNumbers for batch actions, resolveOneNumber for 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.