# ADR 0257: CLI dotted values follow their declared schema

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0257-cli-dotted-values-follow-their-declared-schema.md

---

# ADR 0257 — CLI dotted values follow their declared schema

**Invariants:** I1, I2, I3, I8, I14.

## Context

A dotted CLI argument arrives as text. The old fallback guessed from spelling when the receiving
schema was a free `record<string, unknown>`: `5` became a number and `true` became a boolean, while
`007`, `+007` and `1e3` lost or changed lexical meaning. The same syntax under a declared object has
enough schema information to coerce safely, so treating both cases alike either guesses without a
contract or makes typed leaves inconvenient.

This behavior belongs to the stable `stitchkit/cli` entrypoint. Correcting it changes values seen by
handlers whose free records depended on the guess, so the change needs one named migration rather
than an undocumented parser heuristic.

## Decision

1. A dotted leaf under a declared number or boolean schema is coerced and then validated by that
   leaf. String leaves remain lexical. Literal sets, unions, wrappers and arrays use the same
   schema-derived rule rather than the option name or spelling alone.
2. A dotted leaf under `record<string, unknown>` preserves the exact argv text. Unknown means there
   is no contract authorizing conversion.
3. A whole-object JSON value retains JSON's explicit primitive types. A caller that needs numbers or
   booleans in a free record passes the record as JSON, or declares a structured object schema for a
   dotted interface.
4. A wholly unknown root admitted by `allowUnknown` keeps its existing best-effort coercion. That
   passthrough has no declared record boundary and is outside this correction; changing it would
   widen the migration without improving schema fidelity.
5. Invalid values for typed dotted leaves fail before the handler. Tests cover lexical lookalikes,
   valid typed leaves and invalid number/boolean controls through the real CLI argument builder.

## Consequences

- `--data.count=5` under a free record now yields `{ count: "5" }`; use
  `--data='{"count":5}'` for a numeric value.
- A declared `z.object({ count: z.number() })` continues to receive the number `5` from the dotted
  form.
- The release is a pre-1.0 minor with an explicit breaking section and migration.
