From 50138b6116aee5c13af1eae2fa83ad9551eebd38 Mon Sep 17 00:00:00 2001 From: DTTerastar Date: Sun, 2 Aug 2026 00:33:48 -0400 Subject: [PATCH] =?UTF-8?q?docs(contract):=20=C2=A74=20=E2=80=94=20add=20a?= =?UTF-8?q?=20JSON=20scalar-typing=20clause?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_012MvqxTC64Z9EEDewCUbNNo --- CONTRACT.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CONTRACT.md b/CONTRACT.md index 7202a63..9fed9b8 100644 --- a/CONTRACT.md +++ b/CONTRACT.md @@ -23,6 +23,7 @@ Each cell shows whether the section is implemented and how it is attested for th | `--format markdown` (default) | human | human | **machine** | | `--format json` | human | human | **machine** | | `--format csv` | — | — | **machine** | +| JSON scalar typing (§4) | — | human | — | | `auth status` subcommand | human | human | human | | Headless auth precedence (env over token file) | n/a | human | human | | `prime` subcommand | human | human | human | @@ -87,6 +88,16 @@ Output rules: - **Empty result**: success with empty output (`[]` for JSON, no rows for markdown/CSV), exit code 0. Empty is not an error. - **Exit code**: 0 success, non-zero only for auth or network failure. +### Scalar typing + +§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. + +- **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). +- **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. +- **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. + +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. + ## 5. Auth Auth flows differ legitimately across upstreams (env-var basic auth, OAuth2, interactive credential prompts), but two surface elements are required of every CLI: