Skip to content

Commit cce757b

Browse files
DTTerastarclaude
andauthored
docs(contract): §4 — add a JSON scalar-typing clause (#29)
Closes #27. §4 pinned down which codecs exist and the stdout/stderr split, and §2 pins timestamp formatting, but nothing pinned the JSON type of an individual field. That gap produced the same class of bug four times across two repos (liftoff #33 quoted-string bodyweight, liftoff #36 duration-only-as-prose, withings #27/#20/#42 key-and-type drift). One unwritten rule violated repeatedly. Adds a Scalar typing subsection under §4 with three requirements — numbers are JSON numbers not strings; a logical field has one type (and null, not ""/0, when absent) across every subcommand; a human-formatted quantity ships a canonical-unit sibling — plus the timestamp carve-out (§2 already fixes their shape). Status table gains a JSON-scalar-typing row: liftoff is human-attested (fixed its known violations in v1.3.0), withings and crono stay at — (withings has open violations #27/#20/#42; crono is unaudited). Not machine-attested yet: a compat/scalars bundle is the follow-up, and it inherits the enforcement-reach gap #27 flags — liftoff and withings import only compat/formats, so a new bundle needs their imports widened to actually run in their CI. Tracked separately rather than bundled into this text change. Claude-Session: https://claude.ai/code/session_012MvqxTC64Z9EEDewCUbNNo Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fec40e8 commit cce757b

1 file changed

Lines changed: 11 additions & 0 deletions

File tree

CONTRACT.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Each cell shows whether the section is implemented and how it is attested for th
2323
| `--format markdown` (default) | human | human | **machine** |
2424
| `--format json` | human | human | **machine** |
2525
| `--format csv` ||| **machine** |
26+
| JSON scalar typing (§4) || human ||
2627
| `auth status` subcommand | human | human | human |
2728
| Headless auth precedence (env over token file) | n/a | human | human |
2829
| `prime` subcommand | human | human | human |
@@ -87,6 +88,16 @@ Output rules:
8788
- **Empty result**: success with empty output (`[]` for JSON, no rows for markdown/CSV), exit code 0. Empty is not an error.
8889
- **Exit code**: 0 success, non-zero only for auth or network failure.
8990

91+
### Scalar typing
92+
93+
§4 above pins down *which* codecs exist and the stdout/stderr split; §2 pins down timestamp formatting. This clause pins down the JSON type of an individual field, so a `jq` expression or an LLM agent can rely on the shape across every subcommand of every CLI. It governs the `json` codec; markdown and CSV are rendered text and carry no type.
94+
95+
- **Numeric quantities are JSON numbers, not quoted strings** — even when the upstream API sends them as strings. Decoding MAY keep the upstream shape; output MUST NOT. `"weight": 175` (`"weight": "175"` is wrong).
96+
- **A logical field has one JSON type across every subcommand that emits it.** If `bodyweight` is a number in one place it is a number everywhere. When the value is absent it is `null` — never `""`, never `0`, so a missing measurement can't be read as a real one.
97+
- **A human-formatted quantity carries a machine-readable sibling.** A field whose value is a display phrase (`"01 hours 06 minutes"`) MUST ship a sibling in a canonical unit next to it (`sessionDurationSeconds`). The unit belongs in the key name. This is additive — keep the display field.
98+
99+
Timestamps are the one deliberate exception: §2 fixes their formatting (RFC3339 with offset), so a `date` field MAY be a formatted string. Even then it is the *same* representation across every subcommand of a given CLI — one logical field, one shape.
100+
90101
## 5. Auth
91102

92103
Auth flows differ legitimately across upstreams (env-var basic auth, OAuth2, interactive credential prompts), but two surface elements are required of every CLI:

0 commit comments

Comments
 (0)