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:

  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.
  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. 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.