You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 #33 — bodyweight 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 #36 — sessionDuration 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-cli — sleep --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:
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.
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.
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.
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:bodyweightwas emitted as a quoted string ("175") byworkouts list/showbut as a JSON number byworkouts stats. One logical field, two types depending on which subcommand you asked. Fixed in liftoff v1.3.0.sessionDurationwas available only as the human phrase"01 hours 06 minutes 01 seconds", sitting next to genuinely numericcaloriesBurnedandprCount. No arithmetic without parsing prose. Fixed in v1.3.0 by addingsessionDurationSecondsalongside.dateis RFC3339-with-offset inmeasurements(cmd/measurements.go:64), a bareYYYY-MM-DDstring inactivity(cmd/activity.go:16), and epoch-seconds integers insleep/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.sleep --format csvemitstotal_sleep_min/light_min/deep_min/rem_minin minutes, while--format jsonpasses 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:
bodyweightis a number in one place it is a number everywhere, includingnullrather than""/0when absent, so a missing measurement can't be mistaken for a real one.sessionDurationfor display, but shipsessionDurationSecondsnext to it. Additive, so nothing breaks for existing consumers.Then back it with a
compat/assertion the waycompat/datesandcompat/formatsalready 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/datesin its owncompat_contract_test.go. liftoff and withings import onlycompat/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.