Skip to content
Merged

V0.5 #51

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
16ac250
Add infrastructure for plugin rewrite
konnerhorton Mar 29, 2026
8df528a
`ruff format`
konnerhorton Mar 29, 2026
568ca89
Covert builtins to new plugin requirements
konnerhorton Mar 29, 2026
e45dc4c
Unify cli after plugin change
konnerhorton Mar 29, 2026
6026dae
Remove stats and history plugins
konnerhorton Mar 29, 2026
f37e370
Add movement name in single line session as session name
konnerhorton Mar 29, 2026
9b0d8d6
Move `example.ox` file and update tests/docs
konnerhorton Apr 3, 2026
8150c52
add future.md to docs
konnerhorton Apr 3, 2026
d786689
added srpe plugin
konnerhorton Apr 8, 2026
1b4ba76
update CLAUDE.md and docs
konnerhorton Apr 8, 2026
72863b2
Fix `BW+` in examples
konnerhorton Apr 8, 2026
c7fe11b
Update plugin loading.
konnerhorton Apr 11, 2026
cb18987
Add list of user defined plugins on file load
konnerhorton Apr 11, 2026
fc8856c
bump version
konnerhorton Apr 19, 2026
7948695
Revert "bump version"
konnerhorton Apr 19, 2026
4771caa
Reapply "bump version"
konnerhorton Apr 19, 2026
547bb58
bump version
konnerhorton Apr 19, 2026
c9e8989
add tests for `wendler531` plugin
konnerhorton Apr 19, 2026
2d17436
update cli docs for plugins
konnerhorton Apr 19, 2026
c1f4018
update `parse.py` to remoce flag todo
konnerhorton Apr 19, 2026
9b5db6e
Add generalized plotting tools
konnerhorton Apr 20, 2026
0c7f62e
Migrate `plot.py` utility use to other plugins
konnerhorton Apr 20, 2026
244a61c
Removed unused code
konnerhorton Apr 20, 2026
2b721fc
Support implied units in progressive weights (#45)
konnerhorton Apr 22, 2026
148fedb
Update tests
konnerhorton Apr 22, 2026
953c09f
Added tests for `lsp.py`
konnerhorton Apr 22, 2026
ae15230
Add functionality to excercise blocks
konnerhorton Apr 22, 2026
83fec52
Update example files
konnerhorton Apr 22, 2026
52e78d7
Remove orphaned `_index.md`
konnerhorton Apr 23, 2026
e771b10
Update textmate with `@movement` instead of `@exercise`
konnerhorton Apr 23, 2026
0151ad9
Update `CLAUDE.md` and `SPEC.md` to reflect exercise -> movement
konnerhorton Apr 23, 2026
7d8c059
Change from exercise definitions to movement definitions
konnerhorton Apr 23, 2026
5eca7cc
Update session title in wendler plugin
konnerhorton Apr 23, 2026
9449553
Update docs for recent changes
konnerhorton Apr 23, 2026
c0419cd
Remove `run` command, it was redundant
konnerhorton Apr 23, 2026
62d3a59
Update tests to match wendler plugin change
konnerhorton Apr 23, 2026
4e17978
Add `CHANGELOG.md` for version releases
konnerhorton Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ build/
.venv/
venv/
.env
.uv-cache/

# Testing
.pytest_cache/
Expand Down
75 changes: 75 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# v0.5.0

First release after v0.2.0. This is a large jump — the reports system has been replaced by a proper plugin architecture, parsing has grown in several directions, and the CLI, LSP, and docs have all been reworked. The notes below group changes by theme rather than by commit.

## Breaking changes

- **`@exercise` blocks are now `@movement` blocks.** The block type, the parsed dataclass (`MovementDefinition`), and the tree-sitter grammar all use the new name. Update any `.ox` logs that use the old directive.
- **The `run` REPL command has been removed.** Use `plugins` to list available plugins, and invoke a plugin directly by name (e.g. `volume -m squat`). Avoid naming a custom plugin the same as a built-in command (`query`, `tables`, `reload`, `lint`, `plugins`, `help`, `exit`, `quit`) — built-ins win the name lookup.
- **Reports have been replaced by plugins.** The previous `report` / `generate` CLI commands are gone. The `stats` and `history` plugins were removed; other analyses moved to the new plugin system.
- **Unit normalization.** `lbs` has been unified to `lb`. The grammar now accepts any pint-compatible mass unit (`g`, `oz`, `stone`, `grain`, `kg`, `lb`, …).
- **Time tokens are ISO 8601.** Durations are written `PT30M`, `PT1H30M15S`, etc.

## New plugins

Four built-in plugins ship with this release:

- **`e1rm`** — Estimated 1RM progression from sets tagged with `^rm`, with Brzycki and Epley formulas and table/plot output.
- **`weighin`** — Body-weight tracking with rolling average, trend, multi-scale breakdown, and plot.
- **`srpe`** — Session RPE training load, AU totals per time bin, and ACWR / monotony / strain output modes.
- **`wendler531`** — Generates a 4-week Wendler 5/3/1 cycle as planned sessions (`!` flag), with optional `^rm` tagging and configurable start date and unit.

The existing `volume` plugin remains.

## Plugin system

- Plugins are first-class. A plugin exports `register()` and its functions receive `PluginContext(db, log)`, returning `TableResult`, `TextResult`, or `PlotResult`.
- Load user plugins from your log with `@plugin "path/to/plugin.py"`. Paths resolve relative to the `.ox` file.
- User plugins loaded via `@plugin` override built-ins with the same name.
- On startup, the CLI prints the list of user plugins that were loaded.
- The REPL lists plugins via `plugins` and invokes them by name.

## Parser and language features

- **Movement definitions** — `@movement name … @end` blocks with `equipment`, `tags`, `note`, and `url` fields. Parsed into `MovementDefinition` and exposed on `TrainingLog.movement_definitions`.
- **Notes are first-class objects** — both single-line `note "…"` entries and in-session `note:` lines flow through `Note` / session notes and into the database.
- **Stored queries** — `2025-01-10 query "name" "SELECT …"` lines are parsed and surfaced via the `query` command by name.
- **Weigh-ins** — `date W weight [time] [scale]` lines parse into `WeighIn` dataclasses and populate a `weigh_ins` table.
- **Implied units in progressive weights** — `160/185/210lb` now parses correctly; each segment inherits the nearest following unit.
- **`BW` inside progressions** — `BW/24kg/32kg` and similar forms work without lint errors.
- **Parse diagnostics / linter** — parse errors are collected on load and surfaced via the `lint` command and through the LSP.
- **SQLite `REGEXP`** — available in `query` expressions.
- **Short flags** on plugin parameters (e.g. `-m`, `-b`).

## CLI

- `plugins` — new command to list available plugins.
- Plugins are invoked directly by name. Running a plugin with no args prints its usage.
- `reload` — re-parse the current log from disk without leaving the REPL. Reprints parse diagnostics and re-announces loaded user plugins.
- `tables -h` — show column details alongside the table/view list.
- `query name` — recall a stored query by name, or run inline SQL with `query SELECT …`.
- `--version` reads from `pyproject.toml` so there is a single source of truth.

## Plots

All built-in plots now route through a small `plot` facade over `plotext`, giving consistent axes, markers, and legends across `e1rm`, `weighin`, and `srpe`. The earlier hand-rolled ASCII plots are gone.

## Editor support

- **VSCode** — syntax highlighting updated to cover `@movement`, `@session`, `@template`, `@plugin`, `@include`, `note` entries, `query` entries, and the `equipment`/`tags`/`note`/`url` fields inside movement definitions. Comment folding is fixed.
- **LSP** — movement-name autocomplete is populated from `@movement` blocks in the parsed log; diagnostics surface parse errors and invalid `@include` paths; comment folding is supported.

## Documentation

- `docs/getting-started.md` covers movement definitions, session-level notes, stored queries, and `@plugin` loading.
- `docs/plugins.md` documents every built-in plugin, loading rules, the plugin API, and reserved names.
- `docs/api-reference.md` now lists every dataclass (including `MovementDefinition`), the full `TrainingLog` surface, and the plugin result types.
- `docs/cli-reference.md` reflects the current command set.
- `docs/editor-support.md` clarifies that VSCode is the only shipped extension; Neovim and Helix users can wire up the grammar and `ox-lsp` directly.

## Fixes

- `e1rm` only considers completed sessions.
- Progressive weights with `BW` no longer trigger lint errors.
- Weekly time bins default to Sunday dates.
- Single-line sessions now use the movement name as the session name.
64 changes: 42 additions & 22 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,47 +20,52 @@ uv run ruff format src/ tests/ # format

```
src/ox/
parse.py - Tree-sitter node → data structures (the core parser)
data.py - Dataclasses: TrainingSet, Movement, TrainingSession, TrainingLog, Note, WeighIn, StoredQuery, Diagnostic
db.py - In-memory SQLite layer: create_db(log) → Connection
reports.py - Reports: volume, matrix; get_all_reports() merges builtin + plugin reports
plugins.py - Plugin discovery and registry (report + generator types)
units.py - Pint unit registry (shared instance)
cli.py - Click CLI with interactive REPL (stats, history, report, generate, query, tables, lint, reload)
lsp.py - LSP server: diagnostics, movement completion, comment folding
lint.py - Parse error collection for CLI lint command and LSP
parse.py - Tree-sitter node → data structures (the core parser)
data.py - Dataclasses: TrainingSet, Movement, TrainingSession, TrainingLog, Note, WeighIn, StoredQuery, Diagnostic
db.py - In-memory SQLite layer: create_db(log) → Connection
plugins.py - Plugin discovery, registry, PluginContext, result types (TableResult, TextResult, PlotResult)
sql_utils.py - SQL helper utilities for plugins (parse_plugin_args, plugin_usage, _weight_sql_expr, _time_bin_expr)
units.py - Pint unit registry (shared instance)
cli.py - Click CLI with interactive REPL (run, query, tables, lint, reload)
lsp.py - LSP server: diagnostics, movement completion, comment folding
lint.py - Parse error collection for CLI lint command and LSP
builtins/
e1rm.py - Estimated 1RM report (Brzycki/Epley)
weighin.py - Weigh-in stats/plot report (rolling average, trend, multi-scale)
wendler531.py - Wendler 5/3/1 cycle generator
volume.py - Volume over time plugin
e1rm.py - Estimated 1RM plugin (Brzycki/Epley)
weighin.py - Weigh-in stats/plot plugin (rolling average, trend, multi-scale)
srpe.py - Session RPE training load plugin (ACWR, monotony, strain)
wendler531.py - Wendler 5/3/1 cycle generator plugin
tests/
conftest.py - Shared fixtures (simple_log_*, weight_edge_cases, log_with_query_*, log_with_weigh_ins_*, weigh_in_multi_scale_*, simple_db, example_db)
test_parse.py - Weight/rep parsing
test_data.py - Data structures
test_db.py - SQLite schema, loading, views, queries
test_reports.py - Reports, arg parsing, registry
test_reports.py - SQL utils, volume plugin, arg parsing, plugin registry
test_plugins.py - Plugin registration, loading, builtins
test_integration.py - End-to-end parsing
test_weighin.py - Weigh-in report (rolling avg, trend, table/plot/stats)
test_weighin.py - Weigh-in plugin (rolling avg, trend, table/plot/stats)
test_notes.py - Note parsing, session notes, DB population
test_srpe.py - sRPE plugin (training load, ACWR, monotony, strain)
test_lint.py - Diagnostic collection
tree-sitter-ox/
grammar.js - Tree-sitter grammar definition for .ox format
editors/
vscode/ - VSCode extension for .ox syntax highlighting
examples/
plugins/ - Example plugin scripts (wendler531.py)
plugins/ - Example plugin scripts (wendler531.py)
plugin_template.py - Template for writing user plugins
docs/ - MkDocs documentation source
example/
example.ox - Reference training log with all supported formats
examples/
example.ox - Reference training log with all supported formats
advanced.ox - sRPE tracking example with 8 weeks of training data
```

## .ox File Format

```
# Comments start with #

# Single-line entry: date flag exercise: weight reps "note"
# Single-line entry: date flag movement: weight reps "note"
2025-01-10 * pullups: BW 5x10

# Session block
Expand All @@ -82,9 +87,24 @@ kb-oh-press: 24kg 5/5/5
# Include another file
@include "other.ox"

# Movement definition
@movement squat
equipment: barbell
tags: squat, lower
note: back squat
@end

# Template block
@template "my-template"
movement: details
@end

# Load a plugin
@plugin "my_plugin.py"

# Flags: * = completed, ! = planned, W = weigh-in
# Weight units: kg, lb, g, oz, stone, grain, and more (any pint-compatible mass unit)
# Weight formats: 24kg, BW, 24kg+32kg (combined), 24kg/32kg/48kg (progressive)
# Weight formats: 24kg, BW, 24kg+32kg (combined), 24kg/32kg/48kg (progressive), 160/185/210lb (implied unit)
# Rep formats: 5x5 (sets x reps), 5/5/5 (per-set reps)
# Duration: ISO 8601 (PT30M, PT1H30M15S)
# Distance: numeric + unit (m, km, ft, mi, etc.)
Expand All @@ -95,9 +115,9 @@ kb-oh-press: 24kg 5/5/5
- Python 3.12, dependencies managed with uv
- Frozen dataclasses with `slots=True` for data structures
- `pint.Quantity` for all weight values (never raw numbers)
- Exercise names are hyphenated lowercase (e.g. `kb-oh-press`, `bench-press`)
- Movement names are hyphenated lowercase (e.g. `kb-oh-press`, `bench-press`)
- `to_ox()` methods serialize back to .ox format (round-trip support)
- Tree-sitter nodes are processed in `parse.py`; data structures live in `data.py` — keep this separation
- All analysis features are plugins (builtins or user-defined). Plugins receive `PluginContext(db, log)` and return `TableResult`, `TextResult`, or `PlotResult`
- CLI commands: `plugins` to list available plugins, `query` for raw SQL. Plugins are invoked by name directly (e.g. `volume -m squat`)

## Known Issues
- Progressive weights for the same movement require explicit units, this is a known bug and applicable tests are skipped.
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Ox

Plain text training log format and toolchain. Write workouts in `.ox` files, parse into structured data, analyze progress over time.
Plain text training log format and toolchain. Record training in `.ox` files, parse into structured data, analyze progress over time.

Inspired by [Beancount](https://github.com/beancount/beancount) (plain text accounting, but for training). Named after [Milo of Croton](https://en.wikipedia.org/wiki/Milo_of_Croton).

Expand Down Expand Up @@ -35,7 +35,7 @@ Full docs at [konnerhorton.github.io/ox](https://konnerhorton.github.io/ox):
- [CLI Reference](https://konnerhorton.github.io/ox/cli-reference/) — commands and usage
- [Reports & Plugins](https://konnerhorton.github.io/ox/plugins/) — built-in reports, plugin system
- [API Reference](https://konnerhorton.github.io/ox/api-reference/) — Python library
- [Editor Support](https://konnerhorton.github.io/ox/editor-support/) — VSCode, Neovim, Helix
- [Editor Support](https://konnerhorton.github.io/ox/editor-support/) — VSCode extension, LSP, tree-sitter grammar

## Syntax Overview

Expand All @@ -48,7 +48,7 @@ Full docs at [konnerhorton.github.io/ox](https://konnerhorton.github.io/ox):
2025-01-15 * Lower Body
squat: 135lb 5x5
deadlift: 185lb 3x5
note: easy day
note: "easy day"
@end

# Weigh-in
Expand All @@ -59,19 +59,29 @@ note: easy day

# Include another file
@include "other.ox"

# Movement definition
@movement squat
equipment: barbell
tags: squat, lower
note: back squat
@end

# Load a plugin
@plugin "plugins/my_plugin.py"
```

**Flags:** `*` completed, `!` planned, `W` weigh-in

**Weights:** `24kg`, `135lb`, `BW`, `24kg+32kg` (combined), `24kg/32kg/48kg` (progressive)
**Weights:** `24kg`, `135lb`, `BW`, `24kg+32kg` (combined), `24/32/48kg` (progressive, with implied units)

**Reps:** `5x5` (sets x reps), `5/3/1` (per-set)

**Duration:** ISO 8601 (`PT30M`, `PT1H30M15S`)

**Distance:** `5km`, `3mi`, `400m`

**Exercise names:** no spaces, hyphenated lowercase (`kb-oh-press`, `bb-back-squat`)
**Movement names:** no spaces (`kb-oh-press`, `bb-back-squat`)

## Installation

Expand Down
11 changes: 5 additions & 6 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ Developers and power users comfortable with text files and CLIs.
- **Python parser** (`src/ox/parse.py`) — tree-sitter nodes → dataclasses
- **Data model** (`src/ox/data.py`) — `TrainingSet`, `Movement`, `TrainingSession`, `TrainingLog`, `Note`, `WeighIn`, `StoredQuery`, `Diagnostic`
- **SQLite query layer** (`src/ox/db.py`) — in-memory DB with `sessions`, `movements`, `sets`, `notes`, `session_notes`, `weigh_ins`, `queries` tables and `training` view
- **Plugin system** (`src/ox/plugins.py`) — discovery from `~/.ox/plugins/`, entry points, and builtins; report and generator types
- **Plugin system** (`src/ox/plugins.py`) — built-in plugins plus user plugins loaded via `@plugin` directives in `.ox` files
- **Built-in reports** (`src/ox/reports.py`) — `volume` (volume over time) and `matrix` (session count per movement)
- **Built-in plugins** — `e1rm` (estimated 1RM via Brzycki/Epley), `weighin` (weight tracking with stats/plot/rolling average), `wendler531` (5/3/1 cycle generator)
- **CLI** (`src/ox/cli.py`) — interactive REPL with `stats`, `history`, `report`, `generate`, `query`, `tables`, `lint`, `reload` commands and tab completion
- **CLI** (`src/ox/cli.py`) — interactive REPL with `report`, `generate`, `query`, `tables`, `lint`, `reload` commands and tab completion
- **LSP** (`src/ox/lsp.py`) — diagnostics (syntax errors + include validation), movement name completion, comment folding ranges
- **Weigh-in tracking** — full pipeline: parse → `WeighIn` dataclass → DB → builtin report with table/plot/stats output
- **Notes** — standalone and session-level notes, parse → `Note` dataclass → DB, `to_ox()` round-trip
Expand All @@ -45,22 +45,21 @@ Developers and power users comfortable with text files and CLIs.
### What's incomplete

- Planned sessions (`!` flag) — parsed but ignored in analysis
- Exercise definitions (`@exercise` blocks) — parsed but not used in analysis
- Template blocks (`@template`) — grammar exists, no processing
- Progressive implied weights (e.g. `160/185/210lbs`) — known parsing bug
- CLI exercise autocompletion (tab-complete exercise names, not just commands)
- CLI movement autocompletion (tab-complete movement names, not just commands)

## Direction

### Richer analysis

- Cycle tracking — micro/meso/macro periodization
- Exercise definitions feeding into analysis (e.g. grouping by movement pattern)
- Movement definitions feeding into analysis (e.g. grouping by movement tag)
- `pint.Quantity` for time/distance — enables derived units like pace and speed

### Better editor experience

- LSP hover info (exercise definitions, recent history for a movement)
- LSP hover info (movement definitions, recent history for a movement)
- LSP completions for session templates
- Snippets for common entry patterns

Expand Down
47 changes: 47 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ All are frozen dataclasses with `slots=True`.
|---|---|
| `sessions` | `tuple[TrainingSession, ...]` |
| `notes` | `tuple[Note, ...]` |
| `weigh_ins` | `tuple[WeighIn, ...]` |
| `queries` | `tuple[StoredQuery, ...]` |
| `movement_definitions` | `tuple[MovementDefinition, ...]` |
| `diagnostics` | `tuple[Diagnostic, ...]` |

**Properties:** `completed_sessions`, `planned_sessions`
Expand Down Expand Up @@ -65,6 +68,18 @@ All are frozen dataclasses with `slots=True`.

**Properties:** `volume` (`reps × weight`, or `None` for BW)

### MovementDefinition

| Attribute | Type |
|---|---|
| `name` | `str` |
| `equipment` | `str \| None` |
| `tags` | `tuple[str, ...]` |
| `note` | `str \| None` |
| `url` | `str \| None` |

Parsed from `@movement` blocks. Used by the LSP for name completion; queryable directly off the log.

### WeighIn

| Attribute | Type |
Expand Down Expand Up @@ -144,3 +159,35 @@ session.to_ox() # serialize session to .ox format
movement.to_ox() # serialize movement
note.to_ox() # serialize note
```

## Plugin API

Plugins receive a `PluginContext` and return one of three result types. All are frozen dataclasses in `ox.plugins`.

### PluginContext

| Attribute | Type |
|---|---|
| `db` | `sqlite3.Connection` |
| `log` | `TrainingLog` |

### TableResult

| Attribute | Type |
|---|---|
| `columns` | `list[str]` |
| `rows` | `list[tuple]` |

### TextResult

| Attribute | Type |
|---|---|
| `text` | `str` |

### PlotResult

| Attribute | Type |
|---|---|
| `lines` | `list[str]` |

See [Plugins](plugins.md) for a walkthrough of writing a plugin and registering it via `register()`.
Loading
Loading