Skip to content

Commit ddefb0f

Browse files
docs: add Cronometer protocol spec for clean-room replace of gocronometer (QUA-37)
Phase 1 of QUA-12 plan v4. Captures the public-surface contract the clean-room MIT client must reimplement (constructor, session, five export operations, record-type fields, datetime semantics) sourced exclusively from this repo's own MIT code plus the names of gocronometer's exported identifiers we already call. Marks the wire-level details (URLs, cookies, CSV headers, error shapes) as TBD-by-HAR for Phase 3. Authored without consulting gocronometer source. Refs: QUA-37, QUA-12 Co-Authored-By: Paperclip <[email protected]>
1 parent af97fdf commit ddefb0f

1 file changed

Lines changed: 273 additions & 0 deletions

File tree

docs/cronometer-protocol.md

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
# Cronometer Protocol Spec — for clean-room client (Phase 1)
2+
3+
Status: **draft, Phase 1 of [QUA-12](https://github.com/quantcli/common) plan v4**
4+
5+
This document specifies the surface that an MIT-licensed internal Cronometer
6+
client must reimplement so that `crono-export-cli` can drop its dependency
7+
on the GPL-2.0 `github.com/jrmycanady/gocronometer` package.
8+
9+
## Clean-room ground rules
10+
11+
The implementer **must not consult `gocronometer`'s source code** while
12+
working from this spec. The only permitted inputs to the reimplementation
13+
are:
14+
15+
1. This spec document.
16+
2. `gocronometer`'s **public API surface** — exported names and Go type
17+
signatures, as already named and called from `crono-export-cli`'s own
18+
MIT-licensed code (`internal/cronoclient/client.go`, `cmd/format.go`).
19+
3. Cronometer's **observable HTTP behaviour** — what the Cronometer web
20+
app sends and receives in a browser session against
21+
`https://cronometer.com`, captured as HAR / proxy logs against a real
22+
test account.
23+
24+
`gocronometer`'s source files, internal helpers, comments, request
25+
construction, and parsing logic are **out of scope** and must not be
26+
read or copied. If a question can only be answered by reading
27+
`gocronometer` source, capture the answer empirically from a HAR
28+
instead.
29+
30+
This document was authored without consulting `gocronometer` source.
31+
32+
## Where the spec comes from
33+
34+
Everything in the "Public surface we consume" section below is derived
35+
from `crono-export-cli`'s own code — that is, the call sites and field
36+
references already present in our MIT-licensed source. Nothing in this
37+
document was copied from `gocronometer`.
38+
39+
Wire-level details (URL paths, request bodies, exact response CSV
40+
headers, cookie names) are marked **TBD** wherever they cannot be
41+
established from public sources, and must be filled in empirically
42+
during Phase 3 by HAR capture against a real Cronometer account.
43+
44+
## Public surface we consume
45+
46+
`crono-export-cli` currently uses the following exported names from
47+
`github.com/jrmycanady/gocronometer`. The clean-room replacement must
48+
provide equivalents (under different package and type names — see
49+
"Naming" below) sufficient to delete every reference to the upstream
50+
package from our codebase.
51+
52+
### Constructor and session
53+
54+
| Upstream call | Used in | Purpose |
55+
| -------------------------------------- | ------------------------------------------ | ---------------------------------------- |
56+
| `gocronometer.NewClient(httpClient)` | `internal/cronoclient/client.go` (Login) | Construct a client. We pass `nil` today. |
57+
| `(*Client).Login(ctx, user, pass)` | `internal/cronoclient/client.go` (Login) | Username/password session login. |
58+
| `(*Client).Logout(ctx)` | `internal/cronoclient/client.go` (Logout) | Tear down the session, best-effort. |
59+
60+
Auth model the replacement must support:
61+
62+
- Credentials are read from environment variables `CRONOMETER_USERNAME`
63+
and `CRONOMETER_PASSWORD`. The CLI logs in fresh on every invocation
64+
and does not cache tokens.
65+
- There is no SSO / API-token / OAuth pathway exposed to individual
66+
Cronometer users today (per the project README, last verified
67+
2026-05). Password POST is the only available auth mode.
68+
- Session lifetime, cookie names, anti-forgery tokens, redirect chain:
69+
**TBD via HAR**.
70+
71+
### Export operations
72+
73+
| Upstream call | Returns | Used by |
74+
| ------------------------------------------------------------------- | ---------------------------------------- | -------------------------------- |
75+
| `ExportServingsParsedWithLocation(ctx, start, end, loc)` | `gocronometer.ServingRecords` | `cronoclient.Client.Servings` |
76+
| `ExportExercisesParsedWithLocation(ctx, start, end, loc)` | `gocronometer.ExerciseRecords` | `cronoclient.Client.Exercises` |
77+
| `ExportBiometricRecordsParsedWithLocation(ctx, start, end, loc)` | `gocronometer.BiometricRecords` | `cronoclient.Client.Biometrics` |
78+
| `ExportDailyNutrition(ctx, start, end)` | raw CSV `string` | `cronoclient.Client.Nutrition` |
79+
| `ExportNotes(ctx, start, end)` | raw CSV `string` | `cronoclient.Client.Notes` |
80+
81+
Parameter conventions (the only ones we depend on):
82+
83+
- `ctx context.Context` — cancellation / timeout. Replacement must
84+
honour `ctx.Done()` on the underlying HTTP request.
85+
- `start, end time.Time` — inclusive `[start, end]` window. Only the
86+
calendar date (`YYYY-MM-DD`) of each endpoint is significant on the
87+
wire. Time of day and zone on these values do **not** round-trip.
88+
Date strings on the wire use the user's local calendar.
89+
- `loc *time.Location` — the parsed `RecordedTime` on rows must be
90+
produced in this zone. We always pass `time.Local`.
91+
- CSV-only methods (`ExportDailyNutrition`, `ExportNotes`) do not
92+
accept a `*time.Location`. The CSV `Date` / `Day` columns are
93+
rendered verbatim as Cronometer emits them.
94+
95+
### Record types we read
96+
97+
These are the only fields `crono-export-cli` reads off
98+
`gocronometer`'s typed record types (proof: `cmd/format.go` and
99+
`cmd/auth.go` are the only consumers). The replacement must expose
100+
identically-named fields with these Go types on its own record types
101+
so the `cmd/format.go` reflection-driven renderer continues to work
102+
unchanged (or so that a thin compatibility shim does).
103+
104+
`ServingRecord` (one row per food logged, full nutrient breakdown):
105+
106+
| Field | Go type | Notes |
107+
| ----------------- | ----------- | ------------------------------------------------------ |
108+
| `RecordedTime` | `time.Time` | Parsed in caller-supplied `*time.Location`. |
109+
| `Group` | `string` | Meal group / category bucket (e.g., "Breakfast"). |
110+
| `FoodName` | `string` | Free-form. |
111+
| `QuantityValue` | `float64` | Serving size numeric. |
112+
| `QuantityUnits` | `string` | Serving size unit ("g", "ml", "cup", …). |
113+
| `Category` | `string` | (Currently skipped from markdown rendering.) |
114+
| many `float64` | `float64` | Nutrient columns; field names follow `<Name><Unit>`. |
115+
116+
Nutrient field naming convention (used by `cmd/format.go`'s
117+
`strippedSuffix`): the upstream type's Go field names embed the unit
118+
as a suffix. The renderer recognises these suffixes (longest-first):
119+
120+
| Field suffix | Display unit |
121+
| ------------ | ------------ |
122+
| `Kcal` | `kcal` |
123+
| `Ug` | `µg` |
124+
| `Mg` | `mg` |
125+
| `UI` | `IU` |
126+
| `G` | `g` |
127+
128+
So `EnergyKcal` → "Energy: X kcal", `ProteinG` → "Protein: X g",
129+
`B12Ug` → "B12: X µg", `VitaminAUI` → "Vitamin A: X IU". The
130+
replacement record type's nutrient fields must follow the same
131+
suffix convention so the existing renderer keeps working without
132+
changes. The exact list of nutrient columns must be captured from
133+
the live CSV header during Phase 3 (Cronometer adds new nutrient
134+
columns periodically).
135+
136+
`BiometricRecord` (weight, body fat, blood pressure, custom metrics):
137+
138+
| Field | Go type | Notes |
139+
| -------------- | ----------- | ------------------------------------------------ |
140+
| `RecordedTime` | `time.Time` | Parsed in caller-supplied `*time.Location`. |
141+
| `Metric` | `string` | Metric name (e.g., "Weight"). |
142+
| `Amount` | `float64` | Numeric value. |
143+
| `Unit` | `string` | Unit string emitted by Cronometer (e.g., "lbs"). |
144+
145+
`ExerciseRecord` (logged exercises):
146+
147+
| Field | Go type | Notes |
148+
| ---------------- | ----------- | ---------------------------------------------- |
149+
| `RecordedTime` | `time.Time` | Parsed in caller-supplied `*time.Location`. |
150+
| `Exercise` | `string` | Exercise name. |
151+
| `Minutes` | `float64` | Duration. Zero when unset by the user. |
152+
| `CaloriesBurned` | `float64` | Reported energy burn. Zero when unset. |
153+
| `Group` | `string` | Optional tag/category. |
154+
155+
Aliases used in callers:
156+
157+
- `gocronometer.ServingRecords` is the slice form
158+
`[]gocronometer.ServingRecord`. Same for `BiometricRecords` and
159+
`ExerciseRecords`.
160+
161+
### CSV-only endpoints we use raw
162+
163+
For `Nutrition` and `Notes`, our existing `internal/cronoclient/client.go`
164+
treats the upstream return as an opaque CSV string and parses it with
165+
`encoding/csv` into `[]map[string]string`. We do not use any upstream
166+
typed parser for these. The replacement must either:
167+
168+
- return CSV verbatim from those endpoints (same `string` shape), or
169+
- decode to `[]map[string]string` directly with the same column
170+
semantics described below.
171+
172+
Notes column tolerance: `cmd/format.go`'s `renderNotes` picks the
173+
date column from `Day` or `Date` (in that order) and the note column
174+
from `Note`, `Notes`, or `Comment`, with a fallback to dumping all
175+
non-empty fields. The replacement must preserve at least one of each
176+
group's column names so this fallback is rarely triggered.
177+
178+
Known nutrition gotcha: per the prime text, the `Day` field on
179+
`ServingRecord` is always null and the renderer uses `RecordedTime`
180+
instead; the replacement must preserve that behaviour (i.e., do not
181+
silently start populating `Day` with a stale value, since downstream
182+
LLM agents have been instructed it is null).
183+
184+
## Datetime semantics
185+
186+
- All `RecordedTime` values are parsed using the caller-supplied
187+
`*time.Location` (we always pass `time.Local`). They must reflect
188+
the calendar moment the Cronometer UI shows for that row, not UTC.
189+
- The `--since` / `--until` flags parse to local-midnight `time.Time`
190+
values (see `internal/cronoclient/daterange.go`); only the calendar
191+
date (`YYYY-MM-DD`) of each endpoint of the inclusive window matters
192+
on the wire.
193+
- CSV `Date` / `Day` columns are passed through as Cronometer emits
194+
them — no re-parsing in our client today.
195+
196+
These semantics are codified in [`quantcli/common`'s `CONTRACT.md`
197+
§3 Date flags](https://github.com/quantcli/common/blob/main/CONTRACT.md#3-date-flags)
198+
and must continue to hold post-rewrite.
199+
200+
## Wire layer — TBD during Phase 3
201+
202+
The items below cannot be specified without hitting the real
203+
`cronometer.com` endpoints. They will be captured empirically in
204+
Phase 3 against a real Cronometer test account (board-provided HAR
205+
or DT-supplied credentials). They are listed here so the Phase 3
206+
implementer has a checklist; nothing here may be answered by
207+
reading `gocronometer` source.
208+
209+
| Concern | Resolution route |
210+
| ------------------------------------------------------ | -------------------------------------- |
211+
| Login URL, method, request body shape | HAR: cronometer.com login form |
212+
| Anti-forgery / CSRF token bootstrap | HAR: first page load |
213+
| Session cookie name(s) and `Set-Cookie` flags | HAR: post-login response |
214+
| Logout URL, request shape, expected response | HAR: logout click |
215+
| Servings export URL + query params + date format | HAR: "Export" servings flow |
216+
| Exercises export URL + params | HAR: "Export" exercises flow |
217+
| Biometrics export URL + params | HAR: "Export" biometrics flow |
218+
| Daily-nutrition export URL + params | HAR: "Export" daily-nutrition flow |
219+
| Notes export URL + params | HAR: "Export" notes flow |
220+
| Response content types (CSV vs JSON; charset) | HAR: response headers |
221+
| Exact CSV header column names per endpoint | HAR: response bodies |
222+
| Time-of-day format inside `RecordedTime` parsing | HAR: response body shape |
223+
| Pagination / cursor behaviour, if any | HAR: long-window export |
224+
| Rate-limit and retry-after semantics | HAR: forced rapid replays |
225+
| Error response shapes (auth fail, range too long, …) | HAR: deliberate-failure runs |
226+
227+
The Phase 3 implementer must record these HAR samples under
228+
`internal/cronoclient/testdata/fixtures/` (golden files) so the
229+
clean-room client's tests run hermetically with no live network.
230+
231+
## Naming for the replacement
232+
233+
The replacement package must not be named `gocronometer` and must
234+
not re-export gocronometer-style identifiers verbatim. Suggested
235+
shape (final naming decided in Phase 3 PR review):
236+
237+
- Package path: `github.com/quantcli/crono-export-cli/internal/cronoapi`
238+
(or a similar name under `internal/`). Keep the existing
239+
`internal/cronoclient` wrapper layer intact so call sites in
240+
`cmd/*.go` don't churn.
241+
- Client type: `cronoapi.Client` (or similar) with `NewClient`,
242+
`Login`, `Logout`.
243+
- Record types: `cronoapi.ServingRecord`, `cronoapi.BiometricRecord`,
244+
`cronoapi.ExerciseRecord`, with collection aliases as needed.
245+
- Field names on record types: **identical** to those listed in
246+
"Record types we read" above, so `cmd/format.go`'s reflection
247+
walker keeps working without modification.
248+
249+
## Testing strategy
250+
251+
- All client-package tests must run against **recorded fixtures**
252+
(HAR or hand-written CSV golden files) under
253+
`internal/cronoclient/testdata/` and friends.
254+
- No CI job may hit `cronometer.com`. Live calls happen only on
255+
developer workstations, only when capturing fresh fixtures.
256+
- The existing `compat_contract_test.go` against
257+
`github.com/quantcli/common/compat` continues to be the
258+
cross-CLI compliance gate. The replacement client must not
259+
break its expectations (date-flag parsing, format flag,
260+
prime subcommand, env-var auth, exit codes).
261+
262+
## Out of scope for this spec
263+
264+
- Anything beyond the five export operations actually used today
265+
(servings, exercises, biometrics, daily nutrition, notes).
266+
- Cronometer endpoints that are not exposed through `gocronometer`'s
267+
public surface (we don't depend on them, so they are not in our
268+
surface).
269+
- Cronometer Pro / Gold / team-account features.
270+
- The Cronometer ToS §10(b) AI/automation question — that is a
271+
board-accepted record-only risk per
272+
[QUA-12 plan v4](https://github.com/quantcli/common) and is
273+
resolved at the policy layer, not at the protocol layer.

0 commit comments

Comments
 (0)