docs: split the command table into an index and a reference - #375
docs: split the command table into an index and a reference#375eschizoid wants to merge 4 commits into
Conversation
The Commands table was holding two jobs at once. Someone scanning "what can this do" needs one line per command; someone debugging "why didn't my session group" needs the anchor-matching rules. One cell was 1110 characters and ended by pointing at `Metrics.anchor_filter` for the exact rules it had just spent a paragraph restating. A table is a lookup structure. When the answer needs a paragraph, the reader is not looking anything up. README keeps the index: 31 rows, one line each, worst cell now 75 characters and mean 56 (was 1110 and 296). Columns are aligned per group, computed on CHARACTER width — macOS awk counts length() in bytes, so an em dash silently shifts a row by two. docs/commands.md takes the detail, every description preserved verbatim, grouped exactly as the four sub-tables were. Coverage: the table now names all 38 commands in commands.json. Only one was genuinely absent before (`config unset`); the apparent 23-of-38 gap was an artifact of counting rows against names, since rows combine commands (`config set` / `config get`, `zones` (alias `pz`)). Every heading in the reference is written `stride <cmd>` rather than `<cmd>`, which is both how you would type it and what makes it visible to tools/command-claims.sh — its extractor matches a literal `stride ` prefix. Without that prefix the file parses clean while naming anything at all: verified by planting `stride frobnicate`, which the gate ignored with bare headings and rejects with prefixed ones, naming file, line and command. It also rejects a bad SUBcommand (`stride week bogus`), so the compound headings carry the prefix on both halves. BUILD rc=0, SUITE rc=0 (1111 == 1111), four gates rc=0, mutation-proved against main's gate version (#373 changed that script).
There was a problem hiding this comment.
🟡 Changes recommended
The new docs/commands.md has a misplaced/duplicate “Coaching log” section header, and the README index row for config omits the documented utc_offset_minutes fallback.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR restructures command documentation by turning the README “Commands” table into a concise index and moving full per-command semantics into a dedicated reference doc, improving scanability while keeping detailed behavior documented.
Changes:
- Replaces the README command table’s long descriptions with short, one-line index entries and adds a pointer to the full reference.
- Adds
docs/commands.mdas a per-command reference document containing the detailed descriptions previously embedded in the README.
File summaries
| File | Description |
|---|---|
| README.md | Converts the Commands section into a short index and links to the full command reference. |
| docs/commands.md | Introduces a detailed, per-command reference document (new file). |
Review details
Suppressed comments (1)
docs/commands.md:147
- After moving the “Coaching log” header above the coaching commands, the trailing “## Coaching log / The adaptation loop.” block at the end becomes duplicate and should be removed to avoid an empty section footer.
## Coaching log
The adaptation loop.
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| | --------------------------------------------- | --------------------------------------------------------------- | | ||
| | `init` | create `~/.stride` and migrate the SQLite db — idempotent | | ||
| | `auth` | one-time Strava OAuth; stores tokens and client creds in the db | | ||
| | `config set <key> <val>` / `config get <key>` | your numbers: HR zone bounds, timezone | |
|
|
||
| _What have I done, ever and this year?_ Career and year-to-date totals per sport: sessions, hours, distance. | ||
|
|
||
| ### `stride week add <date> <type> <detail> <rationale> [target]` |
There was a problem hiding this comment.
🟡 Changes recommended
The new reference doc has structural inconsistencies (misplaced/dangling “Coaching log” heading) and omits at least one callable command form (stride config), which undermines the stated goal of a complete per-command reference.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
docs/commands.md:122
docs/commands.mdplaces the "Coaching log" heading at the very end of the file, after all the coaching commands, so the coaching section currently has no heading where it starts and ends with a dangling header.
### `stride week add <date> <type> <detail> <rationale> [target]`
docs/commands.md:148
- The file ends with a second "## Coaching log" header (and "The adaptation loop.") after the last command description, which reads like leftover text and makes the section structure confusing.
## Coaching log
The adaptation loop.
README.md:256
progress's synopsis currently shows[asc\|desc]in inline code, which renders with a backslash in the README table. Use a pipe-free spelling so the rendered synopsis stays clean.
| `progress [date] [asc\|desc]` | trend on a repeated workout, sport-aware lens |
- Files reviewed: 2/2 changed files
- Comments generated: 5
- Review effort level: Lite
| | `init` | create `~/.stride` and migrate the SQLite db — idempotent | | ||
| | `auth` | one-time Strava OAuth; stores tokens and client creds in the db | | ||
| | `config set <key> <val>` / `config get <key>` | your numbers: HR zone bounds, timezone | | ||
| | `config unset <key>` | remove a key outright | |
| | `import <zip\|dir>` | load a Strava account export — no API creds needed | | ||
| | `analyze` | compute training metrics (TSS, zones, CTL/ATL/TSB) | | ||
| | `rate <activity_id\|latest> <1-10>` | record perceived effort for a session | |
| | `top <metric> [n] [sport]` | best sessions by a metric — hr, tss, power, distance | | ||
| | `load [days]` | fitness/fatigue/form series (default 90) | | ||
| | `zones` (alias `pz`) | power-zone watt ranges from your derived FTP | | ||
| | `compare [week\|month]` | this period vs the one before it | |
| @@ -0,0 +1,148 @@ | |||
| # Command reference | |||
|
|
|||
| One section per command, with the behaviour you need to use it correctly. | |||
| (#276); an empty value is refused for every key class. | ||
|
|
||
| ## Data (daily) |
Two agents reviewed the split — one checking every claim against the running binary, one attacking my "the gate guards this file" claim. Both found real problems, including a regression I introduced. Wrong in the one-liners I wrote fresh: - `plan` said "14d outlook". The window is the LAST 14 days: Plan.roc computes `anchor - 13` and the payload field is `recent_activities_14d`. The text I was moving verbatim had it right; my summary inverted it. - `top` listed four of seven metrics, skipping over `intensity` to reach `distance`, so it read as a complete set rather than a truncation. `intensity`, `time` and `output` became undiscoverable from the index. - `pace-curve` dropped "name a sport". Without one, ReportHealth short-circuits to an empty fit, so the default invocation returns a refusal the index did not warn about. - `skip` omitted `|none`, which is the only way to release an existing activity link. - `relabel` said "planned session", inviting exactly the reading the command exists to fix — it works on any status, done ones included. - `complete` said "bare = rest day"; bare completion is ACCEPTED only for a rest session and otherwise raises `activity_required`. Structural: - `## Coaching log` was an empty heading at the end of the file. Every coaching command sat under "Reading your training", so `relabel` was filed as a reading command and the section the README pointed at had nothing in it. The file is rebuilt with the README's grouping. - bare `config` was in neither the index nor the reference, though `--help` lists it and schema-check depends on it. - `units` was missing from the settable keys, contradicting the quickstart 200 lines above in the same README. - the index promised `--all` and the rest-day rule; the reference now documents both. A claim that was simply false: - "`stride --help` is the authoritative list ... generated from the parser, so it cannot drift." The HUMAN help is a hand-maintained string literal in app.roc, and it had already drifted: it omitted `config unset`, a command the parser accepts and the JSON help lists — so my README documented a command its own stated oracle did not mention. The claim is now scoped to the machine form, which IS generated, and the literal has the missing line. Carried over and rotten on arrival: `season` claimed "currently 2021 to today", a property of one database that changes on any older import. Also: the three alias claims are now written `(alias `stride pz`)` rather than `(alias `pz`)`, which puts them under the gate — proven by breaking one and watching command-claims name it. BUILD rc=0, SUITE rc=0 (1111 == 1111), four gates rc=0.
There was a problem hiding this comment.
🟡 Changes recommended
A few newly added documentation claims/signatures are objectively inaccurate or inconsistent with the code (notably “generated from the parser” phrasing and the skip signature), and should be corrected before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
docs/commands.md:61
- There’s a duplicated “Each answers a different question.” line: it appears once before the section header and again immediately after it. Dropping the pre-header copy keeps the structure clean and avoids accidental drift if one copy is edited later.
This issue also appears on line 158 of the same file.
docs/commands.md:160
- The header for
skipdoesn’t mention the supportednonesentinel, but the CLI accepts[activity_id|none](and the section text below relies on it). Updating the signature here avoids readers thinkingnoneis an undocumented extra.
### `stride skip <id> <reason> [activity_id]`
Marks a planned session skipped, with the reason — optionally linking the activity done instead (rendered `→ id` in `week`). Adherence history stays honest either way; a bare re-skip keeps an existing link; pass a new id to change it or `none` to release it. A done session refuses skip — re-complete to fix a mis-link.
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Lite
| Full detail for every command — grouping rules, matching semantics, output | ||
| shapes — lives in **[docs/commands.md](docs/commands.md)**. | ||
| `STRIDE_FORMAT=json stride --help` is generated from the parser and is the | ||
| authoritative list; this table is the index. |
| `STRIDE_FORMAT=json stride --help` is generated from the parser, so its list of | ||
| commands and arguments cannot drift. This file explains what they *mean*. |
| @@ -78,6 +78,7 @@ help_text = | |||
| \\ auth authorize with Strava (one-time paste flow; stores creds) | |||
| \\ config list the config that is set (secrets redacted) | |||
| \\ config get|set <key> [value] read or write one key (hr zone bounds, timezone) | |||
There was a problem hiding this comment.
🟡 Changes recommended
Several new documentation statements are inaccurate or duplicated (notably the “generated from the parser” claim and repeated sentences), and should be corrected before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
README.md:219
- The README says
STRIDE_FORMAT=json stride --helpis “generated from the parser”, but the JSON help payload is emitted fromCommand.specs(andCommand.specsis explicitly documented as hand-written + cross-checked, not generated). This wording overstates the guarantee and is inaccurate (see src/app.roc:205-207 and src/Command.roc:288-289).
Full detail for every command — grouping rules, matching semantics, output
shapes — lives in **[docs/commands.md](docs/commands.md)**.
`STRIDE_FORMAT=json stride --help` is generated from the parser and is the
authoritative list; this table is the index.
docs/commands.md:7
- This claim is not accurate:
STRIDE_FORMAT=json stride --helpis not “generated from the parser”. The JSON help payload is emitted fromCommand.specs, andCommand.specsis documented as hand-written + cross-checked rather than generated (src/app.roc:205-207, src/Command.roc:288-289). Consider rewording to match what is actually enforced.
`STRIDE_FORMAT=json stride --help` is generated from the parser, so its list of
commands and arguments cannot drift. This file explains what they *mean*.
docs/commands.md:176
- The document ends with a duplicate “The adaptation loop.” footer even though the Coaching log section already introduces itself with the same sentence. Dropping this trailing line avoids accidental copy/paste leftovers.
The adaptation loop.
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
| \\ config get|set <key> [value] read or write one key (hr zone bounds, timezone) | ||
| \\ config unset <key> remove a key outright |
There was a problem hiding this comment.
🟢 Approval recommended
The changes are documentation-focused and the only issues found are minor duplicated lines in the new reference doc with straightforward fixes.
Review details
Suppressed comments (1)
docs/commands.md:61
- The line "Each answers a different question." is duplicated immediately before and after the "Reading your training" section header; keeping only one avoids accidental drift and reads cleaner.
Each answers a different question.
## Reading your training
Each answers a different question.
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
|
|
||
| The adaptation loop. | ||
|
|
There was a problem hiding this comment.
🟡 Changes recommended
There are a few concrete documentation/UX inconsistencies (duplicate sentences, a mismatched skip signature, and an inaccurate config get|set help line) that should be corrected before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
src/app.roc:82
- The help text shows
config get|set <key> [value], but the parser requires a value forconfig set(andconfig getdoes not accept one). This can mislead users and contradicts the usage strings produced elsewhere.
\\ config list the config that is set (secrets redacted)
\\ config get|set <key> [value] read or write one key (hr zone bounds, timezone)
\\ config unset <key> remove a key outright
\\ — FTP is derived, never set
docs/commands.md:61
- The sentence “Each answers a different question.” appears twice in a row here (once before and once after the section header), which reads like an accidental duplication.
Each answers a different question.
## Reading your training
Each answers a different question.
docs/commands.md:175
- The file ends with a second standalone “The adaptation loop.” sentence (already stated at the start of the Coaching log section), which looks like an accidental duplicate footer.
The adaptation loop.
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟢 Approval recommended
The changes are documentation-focused and the only findings are minor redundancy nits in the new reference file.
Review details
Suppressed comments (2)
docs/commands.md:61
- This sentence is duplicated immediately before and after the section header, which reads like an accidental copy/paste and adds noise to the reference page. Consider keeping it only once under the "Reading your training" heading.
Each answers a different question.
## Reading your training
Each answers a different question.
docs/commands.md:175
- This closing sentence repeats the section intro at line 140 ("The adaptation loop.") and doesn’t add new information at the end of the document. Removing it will keep the reference tighter.
The adaptation loop.
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Lite
Copilot's Suppressed section caught both. Rebuilding docs/commands.md to fix the grouping split it into per-section files by `###` heading and skipped `##` headings — but the one-line intro under each `##` is not a heading, so it was captured into the trailing section of the PREVIOUS group, then emitted again when I re-added the group intros. Result: "Each answers a different question." immediately before and after its own header, and "The adaptation loop." at the end of the file as well as under its heading. Both stray copies removed; the ones under their headers stay. The adjacent-duplicate check I added earlier today could not see these — the copies are separated by a blank line and an `##` header, so they are not adjacent. Replaced with a whole-file repeated-line check, which finds them and finds nothing else here. Run across src/ too: one hit, a power/pace twin pair in Metrics that belongs to the P5 branch. BUILD rc=0, SUITE rc=0 (1111 == 1111), four gates rc=0.
There was a problem hiding this comment.
🔵 Needs a closer look
A few newly added/edited documentation/help lines overstate guarantees or have minor command-form mismatches that should be corrected for accuracy and consistency.
Review details
Suppressed comments (6)
Previously missed (1) — in code that hasn't changed since the last review.
README.md:251
- The README notes that
STRIDE_FORMAT=json stride --helpis the authoritative list and earlier text explains the command-claims gate only validates backtickedstride <cmd>forms. Writing the alias aspzhere leaves it unverified by that gate; usingstride pzkeeps the alias claim checkable.
This issue also appears on line 254 of the same file.
src/app.roc:82
- The help text implies
config set's value is optional ([value]), but the parser requires a value forconfig setand supportsconfig unsetas a separate form (seeCommand.parse/ usage string). This can mislead users and tools that rely on the human help output.
\\ config list the config that is set (secrets redacted)
\\ config get|set <key> [value] read or write one key (hr zone bounds, timezone)
\\ config unset <key> remove a key outright
\\ — FTP is derived, never set
README.md:219
- This claims the JSON help is “generated from the parser”, but the implementation emits
commands.jsonfromCommand.specs(an explicit command table). Suggest rephrasing to avoid implying it is mechanically derived fromCommand.parse/ impossible to drift.
`STRIDE_FORMAT=json stride --help` is generated from the parser and is the
authoritative list; this table is the index.
README.md:255
- Same as the
pzalias above: writing these aliases without thestrideprefix leaves them outside the command-claims check for backtickedstride <cmd>forms.
| `power-curve [days] [sport]` (alias `pc`) | power-duration curve + Critical Power |
| `pace-curve [days] [sport]` (alias `cs`) | speed-duration curve + Critical Speed; name a sport |
docs/commands.md:156
- The heading omits the documented/implemented
noneliteral for releasing an existing activity link;Command.parse/usage uses[activity_id|none]and the paragraph below already mentionsnone. The command form in the heading should match.
### `stride skip <id> <reason> [activity_id]`
docs/commands.md:7
- This claims
STRIDE_FORMAT=json stride --helpis “generated from the parser” and “cannot drift”, but the binary builds the help payload fromCommand.specs(a maintained command table). Rephrase to avoid overstating the guarantee.
`STRIDE_FORMAT=json stride --help` is generated from the parser, so its list of
commands and arguments cannot drift. This file explains what they *mean*.
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Lite
All six were real; none appeared as a review thread. - `config get|set <key> [value]` in help_text implied the value is optional. `Command.parse` requires it — `[_, "config", "set", key, val]` is a five-element match. Split into `config get <key>` and `config set <key> <value>`, which also makes room to name `units`. - "generated from the parser, so it cannot drift" was imprecise in a way that matters. The JSON help is emitted from `Command.specs`, a table distinct from `Command.parse`; what stops them drifting is an e2e check pinning the two verb lists at 33, not the fact of generation. Both README and docs now say what actually holds. - the `skip` heading in the reference still omitted `|none`. I fixed the README row in the previous round and missed its twin — the same one-site-of-two miss the fleet-merge notes list as a recurring class. - the three alias claims in the README table were written `(alias `pc`)`, outside the gate's `stride `-prefixed shape. Flagged twice, and the reviewer was right that my "readability over coverage" trade was the wrong call: prefixing costs seven characters per row and buys a real check. Proven by breaking one — `stride pcx` now fails by file, line and name. The table is regenerated from its source TSV rather than patched, because three cells growing by seven characters silently broke the column alignment when I edited in place. Each group measures one width again. BUILD rc=0, SUITE rc=0 (1111 == 1111), four gates rc=0.
Closes part of #365 (P6).
The Commands table was doing two jobs at once. Someone scanning "what can this thing do" needs one line per command; someone debugging "why didn't my session group with the others" needs the anchor-matching rules. Both were living in the same cell — the worst at 1110 characters, which ended by pointing at
Metrics.anchor_filterfor the exact rules it had just spent a paragraph restating.A table is a lookup structure. When the answer needs a paragraph, the reader is not looking anything up any more.
What changed
README.mdkeeps the index — 32 rows, one line each, worst cell 75 characters against 1110 before.docs/commands.mdtakes the detail, grouped as the README groups it.What review caught
Two agents reviewed this: one checking every claim against the running binary, one attacking the guard claim below. Both found real defects, which are fixed in
15aec1b. The ones worth naming:plansaid "14d outlook". The window is the last 14 days —Plan.roccomputesanchor - 13, the payload field isrecent_activities_14d. The text I was moving verbatim had it right and my one-line summary inverted it.toplisted four of seven metrics, skipping overintensityto reachdistance— so it read as the complete set rather than a truncation, and three metrics became undiscoverable from the index.## Coaching logwas an empty heading at the end of the file. Every coaching command sat under "Reading your training", sorelabelwas filed as a reading command and the section the README pointed at contained nothing.stride --helpis the authoritative list ... generated from the parser, so it cannot drift" was false. The human help is a hand-maintained string literal inapp.roc, and it had already drifted: it omittedconfig unset, which the parser accepts and the JSON help lists. So the README documented a command its own stated oracle did not mention. The claim is now scoped to the machine form, which genuinely is generated, and the literal has the missing line.The guard claim, corrected
I originally wrote that this file is "guarded by construction" because every command is written
stride <cmd>. That is not defensible as written. An adversarial pass found the honest scope:Every command name written as a backticked
`stride <cmd>`is resolved against the binary's own table, so the 31 headings cannot name a command or subcommand that does not exist. Within that shape the gate is strong — headings, prose, tables, links, emphasis, both fence dialects, any indentation, and every slot of a compound heading.Outside it, the gate does not check: arguments (
stride ttewith the required<watts>omitted passes), any third token, any flag, bracketed argument syntax at all, or a capitalised name —stride Frobnicatepasses silently, with no output and no counter movement. Its corpus floors are also global rather than per-file, sodocs/commands.mdcan lose every one of its references, or be deleted outright, and the gate stays green.So this file's coverage is a convention the gate rewards, not an invariant it enforces. Two narrower fixes did land: the alias claims are now written
(aliasstride pz)so they are checked, and breaking one is confirmed to fail the gate by name.Worth a follow-up issue, not this PR: the unparsed-line counter can be satisfied by an unrelated wording change elsewhere in the same commit, which lets an unbackticked bogus command through — and
help_texthas no gate asserting it mentions everyCommand.specsname, which is what letconfig unsetgo missing in the first place.Verification
BUILD rc=0,SUITE rc=0(1111 == 1111), four gatesrc=0. Coverage verified both directions: all 38 commands incommands.jsonappear in the index, and nothing named is invented.One note for whoever edits the table next: column widths are computed on character width, not byte width. macOS
awkcountslength()in bytes, so an em dash silently shifts a row by two — the same trapcommand-claims.shdocuments in its own header.