Skip to content

CONTRACT §4: add a scalar-typing clause (numbers not strings, one type per logical field, numeric sibling for human-formatted values) #27

Description

@DTTerastar

Problem

CONTRACT.md §4 pins down which codecs exist and the stdout/stderr/exit-code split, and §2 pins down timestamp formatting. Nothing pins down scalar typing. That gap has now produced the same bug in two repos independently:

  • liftoff-export-cli #33bodyweight was emitted as a quoted string ("175") by workouts list/show but as a JSON number by workouts stats. One logical field, two types depending on which subcommand you asked. Fixed in liftoff v1.3.0.
  • liftoff-export-cli #36sessionDuration was available only as the human phrase "01 hours 06 minutes 01 seconds", sitting next to genuinely numeric caloriesBurned and prCount. No arithmetic without parsing prose. Fixed in v1.3.0 by adding sessionDurationSeconds alongside.
  • withings-export-clidate is RFC3339-with-offset in measurements (cmd/measurements.go:64), a bare YYYY-MM-DD string in activity (cmd/activity.go:16), and epoch-seconds integers in sleep/workouts/intraday. jq '.[].date' returns three different shapes across five subcommands. Partly covered by CONTRACT §4: add a scalar-typing clause (numbers not strings, one type per logical field, numeric sibling for human-formatted values) #27 and docs(contract): flip §4 withings --format cells to machine #20 over there.
  • withings-export-clisleep --format csv emits total_sleep_min/light_min/deep_min/rem_min in minutes, while --format json passes the upstream blob through with the same durations nested and in seconds.

Each was fixed or filed as a local wart. They're one unwritten rule being violated four times.

Proposal

Add a §4 sub-clause covering three things:

  1. 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.
  2. 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, including null rather than ""/0 when absent, so a missing measurement can't be mistaken for a real one.
  3. A human-formatted quantity carries a machine-readable sibling. Keep sessionDuration for display, but ship sessionDurationSeconds next to it. Additive, so nothing breaks for existing consumers.

Then back it with a compat/ assertion the way compat/dates and compat/formats already back §4's stdout-purity rule — that's what made the liftoff #31 violation machine-attestable rather than a style opinion.

Note on enforcement reach

Worth flagging while we're here: the Status table marks "Date flags — surface" as machine for all three CLIs, but only crono-export-cli actually imports compat/dates in its own compat_contract_test.go. liftoff and withings import only compat/formats. The partial-adoption disclaimer covers this honestly, but the practical effect is that two of three CLIs don't run the stdout-purity assertion in their own CI. A new scalar-typing bundle would inherit the same reach problem unless the imports get widened.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions