OXYGENHUB
# 260c33b4b6c4 oxygenhub / docs / reports.md
7.4 KiB · Markdown · last changed docs/reports.md: a report's free-form [context] (a budget, a frame rate) 25 days ago renderedsource blame history raw
1
# Reports
2
3
*Design, 2026-10-11. Supersedes the separate Coverage and Tests tabs
4
(spec §13.6, §13.7), which become two kinds of report.*
5
6
A project produces more than pass and fail: coverage, test results,
7
frame timings, sizes, benchmark curves. Today the hub reads two of them,
8
LCOV coverage and JUnit tests, each with a tab of its own. This design
9
makes reports one thing: a project declares what a report is, the hub
10
keeps every run's copy, and readers move through its history.
11
12
## 1. What a report is
13
14
A **report** is a directory a Flow job writes, or anyone uploads:
15
16
```
17
report/
18
  report.toml        the declaration (required)
19
  index.html         a page to show (custom and performance kinds)
20
  data/*.csv|json    datasets the page draws from
21
  assets/…           images, scripts, styles: everything the page needs
22
```
23
24
`report.toml` says what it is:
25
26
```toml
27
kind    = "performance"        # coverage | tests | performance | custom
28
title   = "Frame timing"
29
summary = "Median frame 16.4 ms, p99 18.1 ms over 12 scenes"
30
31
[metrics]                      # the numbers the hub tracks across runs
32
median_frame_ms = { value = 16.4, unit = "ms", better = "lower" }
33
p99_frame_ms    = { value = 18.1, unit = "ms", better = "lower", fail_above = 20 }
34
scenes          = { value = 12 }
35
36
[context]                      # free-form, shown with the report
37
target_fps    = 60
38
budget_cycles = 133333
39
```
40
41
- **Built-in kinds** name their data file and need no page:
42
  `kind = "coverage"` with `file = "lcov.info"` (LCOV or Cobertura), and
43
  `kind = "tests"` with `file = "junit.xml"`. The hub reads them as it
44
  does today, and works out their metrics itself (line coverage; passed,
45
  failed, skipped).
46
- **`performance`** is a page with metrics, as above.
47
- **`custom`** is a page, metrics optional.
48
- **`[context]`** is free-form: what a reader needs to read the numbers,
49
  shown with the report and kept per run, such as a performance budget
50
  (`target_fps = 30`, `budget_cycles = 268800`). The hub assumes no frame
51
  rate or budget; each report's metrics and thresholds carry their own.
52
- **A metric** is a number with an optional `unit`, `better` (`higher` or
53
  `lower`, for colouring changes) and `fail_above` / `fail_below`, which
54
  set a check on the change (§5).
55
56
**Implicit declarations:** a job artifact named `coverage` or `junit`, the
57
names used today, is read as a report of that kind with no `report.toml`,
58
so every existing pipeline keeps working unchanged.
59
60
## 2. How a report arrives
61
62
**From Flow**, a job declares it in its outputs:
63
64
```kdl
65
job "perf" {
66
    exec "sh" "-eu" ".flow/jobs/perf.sh"
67
    output {
68
        report "frame-timing" {
69
            path "/src/out/report"
70
            when "always"
71
        }
72
    }
73
}
74
```
75
76
The runner uploads the directory as one bundle. `when "always"` keeps a
77
failing job's report, as for artifacts.
78
79
**From anywhere else:**
80
`oh report upload frame-timing ./report --bookmark develop` attaches it to
81
the bookmark's current state, or `--change ID` to a change's. The API is
82
`POST /repos/{o}/{n}/reports/{name}` with the bundle as a tar.
83
84
## 3. How the hub keeps it
85
86
Three layers.
87
88
1. **The bundle, per job, content-addressed.** Each file is a blob in the
89
   repository's own store, chunked and deduplicated as artifacts are, and
90
   the bundle is a small **tree**: a manifest of path → blob. A file that
91
   did not change since the last run (the page, its scripts, a dataset that
92
   did not move) is stored once however many runs name it; one that
93
   changed a little costs only its changed chunks. Keeping every run's
94
   report is cheap.
95
2. **An index in the database:**
96
   - `reports`: one row per report per job or upload: repository, name,
97
     kind, title, summary, run and job (or uploader), bookmark, the
98
     measured state and heads, time, and the tree's blob.
99
   - `report_metrics`: one row per metric per report: key, value, unit,
100
     better, thresholds.
101
102
   History, trends, the scrubber and checks read only this. Coverage's
103
   and Tests' existing tables become their kinds' details, keyed by
104
   report.
105
3. **Retention, by policy:**
106
   - metrics: forever (they are a few bytes a run);
107
   - bundles: every run on the default bookmark; the last 50 on any other
108
     bookmark; a proposal's until it lands or closes plus 30 days; a tagged
109
     release's forever. Set per repository in Settings › Reports.
110
111
   An expired bundle's row keeps its metrics and loses its tree; the blobs
112
   go at the next garbage collection.
113
114
**Garbage collection** keeps every tree and file blob a kept report names,
115
as it keeps artifacts (oxygenhub#126): the database is the only thing that
116
names them.
117
118
## 4. What readers see
119
120
- **A Reports tab** (in place of Coverage and Tests), shown once a
121
  repository has a report: each report's latest on the chosen bookmark,
122
  with its kind, summary, metrics and their change against the run
123
  before. Coverage and Tests are entries in it; their old addresses
124
  redirect.
125
- **A report's page:**
126
  - **built-in kinds** show their own views (the coverage tree and file
127
    view, the test cases and their history), as today;
128
  - **performance and custom** show `index.html` in a **sandboxed frame
129
    served from a separate origin**, with no network access and no access
130
    to the hub's cookies or session, so a page may run any scripts and
131
    charts it likes and still cannot act as the viewer;
132
  - **metrics** as a strip of numbers with sparklines across runs.
133
- **The timeline:** along the top, one stop per run on the bookmark,
134
  oldest to newest, each labelled with the change it measured.
135
  - **Scrub** to any run, or step with the arrow keys.
136
  - **Play** steps through the runs at a chosen speed, so a chart can be
137
    watched changing.
138
  - **Compare** two runs side by side, with every metric's delta; on a
139
    proposal the default is its base against its head.
140
- **A frame interface** for pages that animate rather than reload: inside
141
  the frame, `report.run()` gives the run on screen, and
142
  `report.history("dataset")` that dataset across the runs, so a page can
143
  redraw as the timeline moves or chart its own trends.
144
- **Downloads:** the whole bundle as a zip, and each dataset alone.
145
146
## 5. Checks
147
148
A metric with `fail_above` or `fail_below` sets a check named
149
`report/NAME/metric` on the change, failing when crossed, which a proposal
150
may require (Settings › Protection). Coverage's existing check becomes
151
this.
152
153
## 6. The API and `oh`
154
155
- `GET /repos/{o}/{n}/reports` (`?bookmark=`): the latest of each.
156
- `GET /repos/{o}/{n}/reports/{name}` (`?bookmark=&limit=&before=`): its
157
  history, metrics included.
158
- `GET /repos/{o}/{n}/reports/{name}/{id}`: one report, and
159
  `…/{id}/files/{path}` its files.
160
- `POST /repos/{o}/{n}/reports/{name}` (repo:write): an upload.
161
- `oh report list | view NAME | upload NAME DIR`.
162
163
## 7. For a project adopting it
164
165
1. Make the test or benchmark job write `out/report/` with a `report.toml`
166
   and, for a page, `index.html` with its libraries bundled (no CDN: the
167
   frame has no network) and its data in `data/`.
168
2. Declare `report "NAME" { path "/src/out/report" when "always" }` in the
169
   job's output.
170
3. Push. The report appears on the Reports tab after the run.
171
172
## 8. Order of work
173
174
1. Storage and index: the `report` output kind in OxygenFlow, the runner's
175
   bundle upload, the hub's tree, tables and gc roots; coverage and junit
176
   read through it.
177
2. The Reports tab and kinds; the sandboxed frame; metrics and checks.
178
3. The timeline: scrub, play, compare, and the frame interface.
179
4. `oh report` and the API; this document's §7 as the guide.