OXYGENHUB
◆ job-waits oxygenhub / docs / reports.md
8.2 KiB · Markdown · last changed Reports: keep a job's report file by file, index its declaration, metrics and files 25 days ago renderedsource blame history raw
50d93fa4 Andy McCall ↶ 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 }
260c33b4 Andy McCall ↶ 35
36
[context]                      # free-form, shown with the report
37
target_fps    = 60
38
budget_cycles = 133333
50d93fa4 Andy McCall ↶ 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.
260c33b4 Andy McCall ↶ 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.
50d93fa4 Andy McCall ↶ 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
a36bab6b Andy McCall ↶ 79
The bundle travels file by file: each to
80
`POST /api/v1/flow/jobs/{job}/reports/{name}/files` with its path in
81
`X-Flow-Report-Path`, answered with its blob; then the manifest,
82
`{"version":1,"files":[{"path","blob","size"}]}`, as the job's artifact
83
under the report's name, which the receipt names. The hub refuses a
84
manifest naming a file it does not hold, reads `report.toml` and indexes
85
the report; a `report.toml` it cannot read is kept as a custom report whose
86
summary says why, so the job's log and the tab show what to fix. Paths are
87
`/`-separated names of letters, digits, `.`, `_`, `-` and `+`; a report
88
holds at most 2000 files and 512 MiB.
89
50d93fa4 Andy McCall ↶ 90
**From anywhere else:**
91
`oh report upload frame-timing ./report --bookmark develop` attaches it to
92
the bookmark's current state, or `--change ID` to a change's. The API is
93
`POST /repos/{o}/{n}/reports/{name}` with the bundle as a tar.
94
95
## 3. How the hub keeps it
96
97
Three layers.
98
99
1. **The bundle, per job, content-addressed.** Each file is a blob in the
100
   repository's own store, chunked and deduplicated as artifacts are, and
101
   the bundle is a small **tree**: a manifest of path → blob. A file that
102
   did not change since the last run (the page, its scripts, a dataset that
103
   did not move) is stored once however many runs name it; one that
104
   changed a little costs only its changed chunks. Keeping every run's
105
   report is cheap.
106
2. **An index in the database:**
107
   - `reports`: one row per report per job or upload: repository, name,
108
     kind, title, summary, run and job (or uploader), bookmark, the
109
     measured state and heads, time, and the tree's blob.
110
   - `report_metrics`: one row per metric per report: key, value, unit,
111
     better, thresholds.
a36bab6b Andy McCall ↶ 112
   - `report_files`: one row per file per report: path, blob, size; what
113
     garbage collection keeps and the frame serves.
50d93fa4 Andy McCall ↶ 114
115
   History, trends, the scrubber and checks read only this. Coverage's
116
   and Tests' existing tables become their kinds' details, keyed by
117
   report.
118
3. **Retention, by policy:**
119
   - metrics: forever (they are a few bytes a run);
120
   - bundles: every run on the default bookmark; the last 50 on any other
121
     bookmark; a proposal's until it lands or closes plus 30 days; a tagged
122
     release's forever. Set per repository in Settings › Reports.
123
124
   An expired bundle's row keeps its metrics and loses its tree; the blobs
125
   go at the next garbage collection.
126
127
**Garbage collection** keeps every tree and file blob a kept report names,
128
as it keeps artifacts (oxygenhub#126): the database is the only thing that
129
names them.
130
131
## 4. What readers see
132
133
- **A Reports tab** (in place of Coverage and Tests), shown once a
134
  repository has a report: each report's latest on the chosen bookmark,
135
  with its kind, summary, metrics and their change against the run
136
  before. Coverage and Tests are entries in it; their old addresses
137
  redirect.
138
- **A report's page:**
139
  - **built-in kinds** show their own views (the coverage tree and file
140
    view, the test cases and their history), as today;
141
  - **performance and custom** show `index.html` in a **sandboxed frame
142
    served from a separate origin**, with no network access and no access
143
    to the hub's cookies or session, so a page may run any scripts and
144
    charts it likes and still cannot act as the viewer;
145
  - **metrics** as a strip of numbers with sparklines across runs.
146
- **The timeline:** along the top, one stop per run on the bookmark,
147
  oldest to newest, each labelled with the change it measured.
148
  - **Scrub** to any run, or step with the arrow keys.
149
  - **Play** steps through the runs at a chosen speed, so a chart can be
150
    watched changing.
151
  - **Compare** two runs side by side, with every metric's delta; on a
152
    proposal the default is its base against its head.
153
- **A frame interface** for pages that animate rather than reload: inside
154
  the frame, `report.run()` gives the run on screen, and
155
  `report.history("dataset")` that dataset across the runs, so a page can
156
  redraw as the timeline moves or chart its own trends.
157
- **Downloads:** the whole bundle as a zip, and each dataset alone.
158
159
## 5. Checks
160
161
A metric with `fail_above` or `fail_below` sets a check named
162
`report/NAME/metric` on the change, failing when crossed, which a proposal
163
may require (Settings › Protection). Coverage's existing check becomes
164
this.
165
166
## 6. The API and `oh`
167
168
- `GET /repos/{o}/{n}/reports` (`?bookmark=`): the latest of each.
169
- `GET /repos/{o}/{n}/reports/{name}` (`?bookmark=&limit=&before=`): its
170
  history, metrics included.
171
- `GET /repos/{o}/{n}/reports/{name}/{id}`: one report, and
172
  `…/{id}/files/{path}` its files.
173
- `POST /repos/{o}/{n}/reports/{name}` (repo:write): an upload.
174
- `oh report list | view NAME | upload NAME DIR`.
175
176
## 7. For a project adopting it
177
178
1. Make the test or benchmark job write `out/report/` with a `report.toml`
179
   and, for a page, `index.html` with its libraries bundled (no CDN: the
180
   frame has no network) and its data in `data/`.
181
2. Declare `report "NAME" { path "/src/out/report" when "always" }` in the
182
   job's output.
183
3. Push. The report appears on the Reports tab after the run.
184
185
## 8. Order of work
186
187
1. Storage and index: the `report` output kind in OxygenFlow, the runner's
188
   bundle upload, the hub's tree, tables and gc roots; coverage and junit
189
   read through it.
190
2. The Reports tab and kinds; the sandboxed frame; metrics and checks.
191
3. The timeline: scrub, play, compare, and the frame interface.
192
4. `oh report` and the API; this document's §7 as the guide.