Skip to content

docs: split the command table into an index and a reference - #375

Open
eschizoid wants to merge 4 commits into
mainfrom
docs/365-p6-command-reference
Open

docs: split the command table into an index and a reference#375
eschizoid wants to merge 4 commits into
mainfrom
docs/365-p6-command-reference

Conversation

@eschizoid

@eschizoid eschizoid commented Sep 5, 2026

Copy link
Copy Markdown
Owner

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_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 any more.

What changed

README.md keeps the index — 32 rows, one line each, worst cell 75 characters against 1110 before. docs/commands.md takes 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:

  • plan said "14d outlook". The window is the last 14 days — Plan.roc computes anchor - 13, the payload field is recent_activities_14d. The text I was moving verbatim had it right and my one-line summary inverted it.
  • top listed four of seven metrics, skipping over intensity to reach distance — so it read as the complete set rather than a truncation, and three metrics became undiscoverable from the index.
  • ## 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 contained nothing.
  • The claim "stride --help is the authoritative list ... generated from the parser, so it cannot drift" was false. The human help is a hand-maintained string literal in app.roc, and it had already drifted: it omitted config 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 tte with the required <watts> omitted passes), any third token, any flag, bracketed argument syntax at all, or a capitalised name — stride Frobnicate passes silently, with no output and no counter movement. Its corpus floors are also global rather than per-file, so docs/commands.md can 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 (alias stride 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_text has no gate asserting it mentions every Command.specs name, which is what let config unset go missing in the first place.

Verification

BUILD rc=0, SUITE rc=0 (1111 == 1111), four gates rc=0. Coverage verified both directions: all 38 commands in commands.json appear 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 awk counts length() in bytes, so an em dash silently shifts a row by two — the same trap command-claims.sh documents in its own header.

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).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.md as 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.

Comment thread README.md Outdated
| --------------------------------------------- | --------------------------------------------------------------- |
| `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 |
Comment thread docs/commands.md

_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]`

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.md places 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

Comment thread README.md
Comment on lines +224 to +227
| `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 |
Comment thread README.md
Comment on lines +234 to +236
| `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 |
Comment thread README.md Outdated
| `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 |
Comment thread docs/commands.md
@@ -0,0 +1,148 @@
# Command reference

One section per command, with the behaviour you need to use it correctly.
Comment thread docs/commands.md
Comment on lines +29 to +31
(#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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 skip doesn’t mention the supported none sentinel, but the CLI accepts [activity_id|none] (and the section text below relies on it). Updating the signature here avoids readers thinking none is 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

Comment thread README.md Outdated
Comment on lines +216 to +219
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.
Comment thread docs/commands.md Outdated
Comment on lines +6 to +7
`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*.
Comment thread src/app.roc Outdated
@@ -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)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 --help is “generated from the parser”, but the JSON help payload is emitted from Command.specs (and Command.specs is 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 --help is not “generated from the parser”. The JSON help payload is emitted from Command.specs, and Command.specs is 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

Comment thread docs/commands.md Outdated
Comment thread src/app.roc Outdated
Comment on lines +80 to +81
\\ config get|set <key> [value] read or write one key (hr zone bounds, timezone)
\\ config unset <key> remove a key outright

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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

Comment thread docs/commands.md Outdated
Comment on lines +174 to +176

The adaptation loop.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 for config set (and config get does 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

Comment thread docs/commands.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 --help is the authoritative list and earlier text explains the command-claims gate only validates backticked stride <cmd> forms. Writing the alias as pz here leaves it unverified by that gate; using stride pz keeps 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 for config set and supports config unset as a separate form (see Command.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.json from Command.specs (an explicit command table). Suggest rephrasing to avoid implying it is mechanically derived from Command.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 pz alias above: writing these aliases without the stride prefix leaves them outside the command-claims check for backticked stride <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 none literal for releasing an existing activity link; Command.parse/usage uses [activity_id|none] and the paragraph below already mentions none. The command form in the heading should match.
### `stride skip <id> <reason> [activity_id]`

docs/commands.md:7

  • This claims STRIDE_FORMAT=json stride --help is “generated from the parser” and “cannot drift”, but the binary builds the help payload from Command.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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants