tick-bench-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CITATION.cff +21 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/docs/agent-adapters.md +31 -0
- package/docs/banner.svg +59 -0
- package/docs/connect-your-table.md +104 -0
- package/docs/metrics.md +28 -0
- package/docs/report-screenshot.png +0 -0
- package/docs/task-authoring.md +28 -0
- package/examples/my-table-adapter.mjs +57 -0
- package/examples/walkthrough/README.md +114 -0
- package/examples/walkthrough/adapter.mjs +35 -0
- package/examples/walkthrough/my-table.mjs +49 -0
- package/examples/walkthrough/sample-results/report-stream-grid.html +23 -0
- package/examples/walkthrough/sample-results/report-stream-grid.json +74 -0
- package/package.json +61 -0
- package/schemas/task.schema.json +24 -0
- package/schemas/trace.schema.json +28 -0
- package/src/cli.mjs +129 -0
- package/src/index.d.ts +77 -0
- package/src/index.mjs +7 -0
- package/src/oracles/a11y.mjs +31 -0
- package/src/oracles/runtime.mjs +93 -0
- package/src/oracles/security.mjs +24 -0
- package/src/report/aggregate.mjs +17 -0
- package/src/report/html.mjs +56 -0
- package/src/report/verdict.mjs +16 -0
- package/src/trace/generate.mjs +29 -0
- package/tasks/stream-grid/oracle.config.json +14 -0
- package/tasks/stream-grid/reference/grid.mjs +29 -0
- package/tasks/stream-grid/spec.md +9 -0
- package/tasks/stream-grid/submissions/claude-fable-5/PROVENANCE.md +10 -0
- package/tasks/stream-grid/submissions/claude-fable-5/grid.mjs +72 -0
- package/tasks/stream-grid/submissions/naive-baseline/PROVENANCE.md +6 -0
- package/tasks/stream-grid/submissions/naive-baseline/grid.mjs +32 -0
package/CITATION.cff
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use TickBench in your research, please cite it as below."
|
|
3
|
+
title: "TickBench: A Benchmark Harness for Performance- and Correctness-Critical Financial Dashboard Engineering"
|
|
4
|
+
type: software
|
|
5
|
+
authors:
|
|
6
|
+
- family-names: Fedytskyi
|
|
7
|
+
given-names: Roman
|
|
8
|
+
email: fedytskyi@gmail.com
|
|
9
|
+
version: 0.1.0
|
|
10
|
+
date-released: 2026-07-08
|
|
11
|
+
license: MIT
|
|
12
|
+
repository-code: "https://github.com/RomanFedytskyi/TickBench"
|
|
13
|
+
keywords:
|
|
14
|
+
- benchmark
|
|
15
|
+
- fintech
|
|
16
|
+
- coding agents
|
|
17
|
+
- real-time dashboards
|
|
18
|
+
- display staleness
|
|
19
|
+
- web performance
|
|
20
|
+
- accessibility
|
|
21
|
+
- security
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roman Fedytskyi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# TickBench
|
|
2
|
+
|
|
3
|
+
[](https://github.com/RomanFedytskyi/TickBench/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/tick-bench-cli)
|
|
5
|
+
[](https://www.npmjs.com/package/tick-bench-cli)
|
|
6
|
+
[](https://bundlephobia.com/package/tick-bench-cli)
|
|
7
|
+
[](https://www.npmjs.com/package/tick-bench-cli)
|
|
8
|
+
[](https://www.npmjs.com/package/tick-bench-cli)
|
|
9
|
+
[](#benchmark-your-own-component)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](docs/task-authoring.md)
|
|
12
|
+
[](https://doi.org/10.5281/zenodo.21272054)
|
|
13
|
+
[](https://github.com/RomanFedytskyi/TickBench)
|
|
14
|
+
|
|
15
|
+
> **Never ship a grid that paints a stale price.**
|
|
16
|
+
|
|
17
|
+
TickBench replays **seeded, replayable market-data tick streams** against your data-grid or
|
|
18
|
+
dashboard component in headless Chromium and tells you what your users would actually see:
|
|
19
|
+
**display staleness**, update latency percentiles, dropped frames, lost updates, broken money
|
|
20
|
+
formatting, live XSS sinks, and WCAG violations — one command, one JSON report, one
|
|
21
|
+
pass/fail verdict.
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<img src="docs/banner.svg" alt="TickBench: benchmark your live data grid under real market load — display staleness, update latency, frame stability, money formatting, XSS, WCAG." width="100%" />
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
## What is this?
|
|
28
|
+
|
|
29
|
+
**TickBench is like Lighthouse, but for live data tables.** Lighthouse loads your page and
|
|
30
|
+
grades it. TickBench goes further for one specific, high-stakes kind of UI — tables that
|
|
31
|
+
display fast-changing numbers (prices, positions, risk, metrics) — and grades what a user
|
|
32
|
+
would actually see while thousands of updates per second are flowing in.
|
|
33
|
+
|
|
34
|
+
Your unit tests check logic. Your visual tests check pixels at rest. **Nothing you run today
|
|
35
|
+
checks whether the number on screen right now is the number that arrived 400 ms ago** — or
|
|
36
|
+
whether your table silently drops updates, stutters, mis-formats money, executes a malicious
|
|
37
|
+
string, or loses its accessible structure under load. TickBench checks exactly that, in a
|
|
38
|
+
real (headless) Chrome, and gives you a PASS/FAIL verdict per implementation.
|
|
39
|
+
|
|
40
|
+
**Use it when you:**
|
|
41
|
+
|
|
42
|
+
- build any dashboard with live numbers — trading, crypto, risk, fleet, analytics, admin;
|
|
43
|
+
- are choosing a table/grid library and don't want to trust vendor marketing benchmarks;
|
|
44
|
+
- let AI coding tools write frontend code and need an objective gate before merging it;
|
|
45
|
+
- want a CI check proving a refactor didn't make the table slower or wrong.
|
|
46
|
+
|
|
47
|
+
## How it works (60 seconds)
|
|
48
|
+
|
|
49
|
+
**Step 1 — you write one small file** (an adapter). Important: your app never imports
|
|
50
|
+
TickBench, and `applyTick` is not imported from anywhere — **you define it, and TickBench
|
|
51
|
+
calls it**. TickBench imports *your* file, calls your `createGrid` once to mount the table,
|
|
52
|
+
then calls your `applyTick` ~17,000 times with simulated market updates while filming the
|
|
53
|
+
result frame by frame.
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
// my-adapter.mjs — the whole contract
|
|
57
|
+
export function createGrid(container, symbols, cols) {
|
|
58
|
+
// mount YOUR table into `container` (any framework; relative imports work)
|
|
59
|
+
// each rendered cell needs data-sym / data-col attributes so TickBench can watch it
|
|
60
|
+
return {
|
|
61
|
+
applyTick(tick) {
|
|
62
|
+
// TickBench calls this with { sym, col, v } for every update.
|
|
63
|
+
// Route it into your table the same way your app would (setState, setCell, ...).
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Step 2 — run it** from your project folder:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx tick-bench-cli bench --impl ./my-adapter.mjs --name my-table
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Step 3 — read the verdict.** Three outputs every run: the console table, a browser-ready
|
|
76
|
+
**HTML report** (`./tickbench-results/report-stream-grid.html`), and JSON for CI:
|
|
77
|
+
|
|
78
|
+
<p align="center">
|
|
79
|
+
<img src="docs/report-screenshot.png" alt="TickBench HTML report: PASS cards with per-gate checklists for reference and simple-table, plus the full metric comparison table." width="90%" />
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
Complete runnable example with a real component file: [`examples/walkthrough/`](examples/walkthrough/).
|
|
83
|
+
Framework recipes (React, AG Grid): [`docs/connect-your-table.md`](docs/connect-your-table.md).
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
## How do you test a real-time data grid?
|
|
87
|
+
|
|
88
|
+
If you searched for *how to test a real-time data grid*, *measure display staleness*, *React
|
|
89
|
+
table streaming updates benchmark*, *virtualized grid dropped updates*, *validate AI-generated
|
|
90
|
+
frontend code*, or *my dashboard shows stale prices under load* — this is the tool for that.
|
|
91
|
+
|
|
92
|
+
## Why this exists
|
|
93
|
+
|
|
94
|
+
Every trading, risk, lending, and analytics UI is a grid fed by a stream. Under burst load,
|
|
95
|
+
implementations fail in ways no existing tool measures:
|
|
96
|
+
|
|
97
|
+
- the DOM shows a **price that is no longer true** (staleness),
|
|
98
|
+
- the **latest** update is silently never painted (lost update),
|
|
99
|
+
- the main thread stalls for hundreds of ms (frame stability),
|
|
100
|
+
- `1234567.5` cents becomes `12345.68` instead of `12,345.68` (money formatting),
|
|
101
|
+
- an interpolated symbol name becomes an **XSS payload**,
|
|
102
|
+
- the grid loses its accessible structure (WCAG).
|
|
103
|
+
|
|
104
|
+
Grid vendors publish marketing benchmarks; js-framework-benchmark tests static rows. TickBench
|
|
105
|
+
tests **your implementation, under your workload, against executable acceptance criteria** —
|
|
106
|
+
including code written by AI coding agents before you let it near production.
|
|
107
|
+
|
|
108
|
+
## Quick start (this repository)
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
git clone https://github.com/RomanFedytskyi/TickBench.git && cd TickBench
|
|
112
|
+
npm ci
|
|
113
|
+
npx playwright-core install chromium-headless-shell # one-time browser download
|
|
114
|
+
|
|
115
|
+
npm test # unit tests
|
|
116
|
+
npm run validate # oracle validation: reference passes, mutants are caught
|
|
117
|
+
npm run bench # score all bundled submissions for tasks/stream-grid
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
reference claude-fable-5 naive-baseline
|
|
122
|
+
catch-up latency p50 (ms) 24.3 24.1 8.1
|
|
123
|
+
catch-up latency p95 (ms) 31.8 32.1 37.3
|
|
124
|
+
stale cell-frames (%) 17.41 17.44 0
|
|
125
|
+
long frames >33ms (%) 0 0.2 2.49
|
|
126
|
+
worst frame (ms) 16.8 49.9 516.7
|
|
127
|
+
final format errors 0 0 157
|
|
128
|
+
XSS triggered (dynamic) false false true
|
|
129
|
+
security findings (high) 0 0 1
|
|
130
|
+
axe violations 0 0 1
|
|
131
|
+
REGULATED-GRADE PASS true true false
|
|
132
|
+
```
|
|
133
|
+
Three submissions ship with the repo: a human **reference**, a genuine single-shot
|
|
134
|
+
**AI-generated submission** (`claude-fable-5`, provenance documented, unedited model output),
|
|
135
|
+
and a hand-written **naive-baseline** used to validate the oracles. Note the trade-off the
|
|
136
|
+
numbers expose: the unbatched baseline paints *faster at the median* but stalls for half a
|
|
137
|
+
second at the worst — while rAF-batched implementations trade a single frame of staleness
|
|
138
|
+
for stability. That frontier is invisible until you measure it.
|
|
139
|
+
|
|
140
|
+
## What gets measured
|
|
141
|
+
|
|
142
|
+
Formal definitions live in [`docs/metrics.md`](docs/metrics.md):
|
|
143
|
+
|
|
144
|
+
| Dimension | Oracle | Metrics |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| Update correctness | runtime, frame-sampled | display staleness, catch-up latency p50/p95/p99, lost updates |
|
|
147
|
+
| Rendering performance | runtime | long-frame rate, worst frame |
|
|
148
|
+
| Money display | final-state sweep | value errors, locale/format errors |
|
|
149
|
+
| Security | static sink scan + dynamic canary | injection findings, XSS execution |
|
|
150
|
+
| Accessibility | axe-core | WCAG 2.1 A/AA violations |
|
|
151
|
+
|
|
152
|
+
Thresholds are part of each task's `oracle.config.json` — acceptance criteria are versioned
|
|
153
|
+
data, not harness code. Every gate is validated: the shipped `naive-baseline` submission is a
|
|
154
|
+
deliberately broken implementation that `tickbench validate` proves each oracle catches.
|
|
155
|
+
|
|
156
|
+
## Real workloads
|
|
157
|
+
|
|
158
|
+
Synthetic traces are seeded and deterministic (`tickbench gen --seed …`). You can also record
|
|
159
|
+
real feeds (e.g., public exchange WebSocket streams, or your own production feed) into the
|
|
160
|
+
same schema — set `"synthetic": false` and ship a provenance file. See
|
|
161
|
+
[`docs/task-authoring.md`](docs/task-authoring.md).
|
|
162
|
+
|
|
163
|
+
## CLI
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
tick-bench-cli bench --impl ./my-adapter.mjs [--name LABEL] benchmark YOUR component (any directory)
|
|
167
|
+
tick-bench-cli bench [--task DIR] [--submission NAME] benchmark a task's reference + submissions
|
|
168
|
+
tick-bench-cli gen [--seed N] [--duration MS] [--symbols N] [--out FILE]
|
|
169
|
+
tick-bench-cli validate [--task DIR]
|
|
170
|
+
tick-bench-cli new-submission <name> [--task DIR] scaffold a submission for model output
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Common options: `--trace FILE` (custom workload), `--out DIR` (default `./tickbench-results`),
|
|
174
|
+
`--no-reference` (skip the built-in comparison baseline).
|
|
175
|
+
|
|
176
|
+
## Contributing
|
|
177
|
+
|
|
178
|
+
New tasks, new metrics, and submissions of real-world grid implementations are all welcome —
|
|
179
|
+
see [CONTRIBUTING.md](CONTRIBUTING.md). Metrics need a definition **and** a validation mutant.
|
|
180
|
+
|
|
181
|
+
## Citation
|
|
182
|
+
|
|
183
|
+
If TickBench is useful in your work, cite it via [`CITATION.cff`](CITATION.cff).
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
Code: MIT © 2026 Roman Fedytskyi. Bundled and recorded trace data (`traces/`): CC BY 4.0.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Evaluating Coding Agents with TickBench
|
|
2
|
+
|
|
3
|
+
TickBench evaluates *artifacts* (submissions), so any agent can be tested by writing its
|
|
4
|
+
output into `tasks/<task>/submissions/<agent-id>/grid.mjs`:
|
|
5
|
+
|
|
6
|
+
1. Give the agent `tasks/<task>/spec.md` (and nothing else) as the task prompt.
|
|
7
|
+
2. Save each sample as `submissions/<agent-id>-s<k>/grid.mjs` for pass@k evaluation.
|
|
8
|
+
3. Record model, version, temperature, prompt, and date in `PROVENANCE.md`.
|
|
9
|
+
4. Run `tickbench bench --task tasks/<task>` — every submission is scored per dimension and
|
|
10
|
+
against the regulated-grade gate.
|
|
11
|
+
|
|
12
|
+
Planned: an adapter CLI that drives agent APIs
|
|
13
|
+
directly, multi-turn repair loops with oracle feedback, and regulated-grade-pass@k reporting.
|
|
14
|
+
|
|
15
|
+
## Seeding submissions across models
|
|
16
|
+
|
|
17
|
+
For a model comparison, repeat per model:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx tick-bench-cli new-submission claude-sonnet-5 # scaffolds grid.mjs + PROVENANCE.md
|
|
21
|
+
# paste tasks/stream-grid/spec.md into the model (claude.ai, API, IDE agent...)
|
|
22
|
+
# paste its UNEDITED output into submissions/claude-sonnet-5/grid.mjs
|
|
23
|
+
# fill PROVENANCE.md (model, date, mode, prompt, "post-editing: none")
|
|
24
|
+
npx tick-bench-cli bench --task tasks/stream-grid # scores all submissions side by side
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Provenance rules that keep the comparison honest: output is pasted unedited; the prompt is
|
|
28
|
+
exactly the task spec (plus any system prompt, recorded verbatim); one directory per model
|
|
29
|
+
and per sample (`claude-sonnet-5-s2` for the second sample when computing pass@k); if the
|
|
30
|
+
generating model may have seen similar public code in training, note it — the shipped
|
|
31
|
+
`claude-fable-5` submission carries the same caveat.
|
package/docs/banner.svg
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<svg width="1200" height="300" viewBox="0 0 1200 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="TickBench: benchmark your live data grid under real market load">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#0b1220"/>
|
|
5
|
+
<stop offset="0.55" stop-color="#0e1a2b"/>
|
|
6
|
+
<stop offset="1" stop-color="#0a2230"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
|
|
9
|
+
<stop offset="0" stop-color="#22d3a5"/>
|
|
10
|
+
<stop offset="1" stop-color="#38bdf8"/>
|
|
11
|
+
</linearGradient>
|
|
12
|
+
</defs>
|
|
13
|
+
<rect width="1200" height="300" fill="url(#bg)"/>
|
|
14
|
+
<g stroke="#1e3a52" stroke-width="1" opacity="0.35">
|
|
15
|
+
<path d="M0 60H1200M0 120H1200M0 180H1200M0 240H1200"/>
|
|
16
|
+
<path d="M200 0V300M400 0V300M600 0V300M800 0V300M1000 0V300"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g font-family="ui-sans-serif,-apple-system,'Segoe UI',Helvetica,Arial,sans-serif">
|
|
19
|
+
<text x="64" y="112" font-size="64" font-weight="800" fill="#f1f5f9">Tick<tspan fill="url(#accent)">Bench</tspan></text>
|
|
20
|
+
<text x="66" y="150" font-size="21" fill="#94a3b8">Benchmark your live data grid under real market load.</text>
|
|
21
|
+
<text x="66" y="178" font-size="16" fill="#64748b">Display staleness · update latency · frame stability · money formatting · XSS · WCAG</text>
|
|
22
|
+
<g font-size="13" font-weight="600">
|
|
23
|
+
<rect x="66" y="206" rx="12" width="112" height="26" fill="#0f2e26" stroke="#22d3a5" stroke-width="1"/>
|
|
24
|
+
<text x="122" y="223" fill="#22d3a5" text-anchor="middle">correctness</text>
|
|
25
|
+
<rect x="188" y="206" rx="12" width="118" height="26" fill="#0d2836" stroke="#38bdf8" stroke-width="1"/>
|
|
26
|
+
<text x="247" y="223" fill="#38bdf8" text-anchor="middle">performance</text>
|
|
27
|
+
<rect x="316" y="206" rx="12" width="96" height="26" fill="#2b1a30" stroke="#e879f9" stroke-width="1"/>
|
|
28
|
+
<text x="364" y="223" fill="#e879f9" text-anchor="middle">security</text>
|
|
29
|
+
<rect x="422" y="206" rx="12" width="80" height="26" fill="#2e2410" stroke="#fbbf24" stroke-width="1"/>
|
|
30
|
+
<text x="462" y="223" fill="#fbbf24" text-anchor="middle">a11y</text>
|
|
31
|
+
</g>
|
|
32
|
+
</g>
|
|
33
|
+
<!-- mock live-quotes card -->
|
|
34
|
+
<g>
|
|
35
|
+
<rect x="672" y="22" width="464" height="200" rx="10" fill="#0d1626" stroke="#24405c" stroke-width="1.5"/>
|
|
36
|
+
<text x="694" y="52" font-family="ui-sans-serif,'Segoe UI',sans-serif" font-size="14" font-weight="700" fill="#cbd5e1">LIVE QUOTES</text>
|
|
37
|
+
<circle cx="1106" cy="47" r="5" fill="#22d3a5"/>
|
|
38
|
+
<g font-family="ui-monospace,SFMono-Regular,Menlo,Consolas,monospace" font-size="14">
|
|
39
|
+
<g fill="#64748b">
|
|
40
|
+
<text x="694" y="82">SYM</text><text x="812" y="82">BID</text><text x="924" y="82">ASK</text><text x="1036" y="82">LAST</text>
|
|
41
|
+
</g>
|
|
42
|
+
<g fill="#e2e8f0">
|
|
43
|
+
<text x="694" y="112">AAPL</text><text x="812" y="112" fill="#22d3a5">212,341.10</text><text x="924" y="112">212,341.35</text><text x="1036" y="112" fill="#22d3a5">▲ 341.20</text>
|
|
44
|
+
<text x="694" y="142">MSFT</text><text x="812" y="142" fill="#f87171">418,220.55</text><text x="924" y="142">418,220.90</text><text x="1036" y="142" fill="#f87171">▼ 220.75</text>
|
|
45
|
+
<text x="694" y="172">NVDA</text><text x="812" y="172">131,904.00</text><text x="924" y="172" fill="#22d3a5">131,904.40</text><text x="1036" y="172">904.15</text>
|
|
46
|
+
</g>
|
|
47
|
+
<rect x="804" y="126" width="104" height="24" rx="4" fill="none" stroke="#fbbf24" stroke-width="1.5" stroke-dasharray="4 3"/>
|
|
48
|
+
<text x="694" y="206" font-size="12.5" fill="#fbbf24">⚠ stale 412 ms → flagged by runtime oracle</text>
|
|
49
|
+
</g>
|
|
50
|
+
</g>
|
|
51
|
+
<!-- frame-time sparkline, fully below the card -->
|
|
52
|
+
<g transform="translate(672,282)">
|
|
53
|
+
<text x="0" y="-40" font-family="ui-monospace,monospace" font-size="11" fill="#64748b">frame time</text>
|
|
54
|
+
<polyline points="0,-12 32,-13 64,-11 96,-13 128,-12 160,-13 192,-30 208,-6 240,-12 272,-13 304,-11 336,-12 368,-13 400,-11 432,-12 460,-13" fill="none" stroke="#38bdf8" stroke-width="2" stroke-linejoin="round"/>
|
|
55
|
+
<circle cx="192" cy="-30" r="3.5" fill="#f87171"/>
|
|
56
|
+
<text x="204" y="-32" font-family="ui-monospace,monospace" font-size="11" fill="#f87171">long frame</text>
|
|
57
|
+
</g>
|
|
58
|
+
<rect x="0" y="296" width="1200" height="4" fill="url(#accent)"/>
|
|
59
|
+
</svg>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Connect your own table
|
|
2
|
+
|
|
3
|
+
TickBench doesn't need your app. It benchmarks a **component in isolation**: you write one
|
|
4
|
+
small adapter file that mounts your table and routes updates into it, then point the CLI at
|
|
5
|
+
that file. Three steps, ~5 minutes.
|
|
6
|
+
|
|
7
|
+
## 1. Write an adapter
|
|
8
|
+
|
|
9
|
+
The whole contract:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
export function createGrid(container, symbols, cols) {
|
|
13
|
+
// 1. mount your table into `container`, one row per symbol, one column per col
|
|
14
|
+
// 2. every rendered cell must carry data-sym / data-col attributes
|
|
15
|
+
return {
|
|
16
|
+
applyTick(tick) {
|
|
17
|
+
// 3. route { sym, col, v } into your table's update path
|
|
18
|
+
// price columns arrive as integer cents; format as en-US, 2 decimals
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Relative imports work — your adapter can `import { MyTable } from './my-table.mjs'` like
|
|
25
|
+
normal code. See `examples/walkthrough/` for a complete runnable example with a real
|
|
26
|
+
component file, or copy `examples/my-table-adapter.mjs` as a single-file starting point — it is a complete working adapter
|
|
27
|
+
with the two "REPLACE" sections marked.
|
|
28
|
+
|
|
29
|
+
**Vanilla / any DOM component:** call your component's mount and update methods directly.
|
|
30
|
+
|
|
31
|
+
**React:** the harness runs your file in a bare browser page (no bundler), so import React
|
|
32
|
+
from a CDN with full URLs:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import React from 'https://esm.sh/react@18';
|
|
36
|
+
import { createRoot } from 'https://esm.sh/react-dom@18/client';
|
|
37
|
+
|
|
38
|
+
export function createGrid(container, symbols, cols) {
|
|
39
|
+
const root = createRoot(container);
|
|
40
|
+
let setTicks; // captured from the component
|
|
41
|
+
function App() {
|
|
42
|
+
const [ticks, s] = React.useState({});
|
|
43
|
+
setTicks = s;
|
|
44
|
+
return React.createElement(MyPriceTable, { ticks, symbols, cols }); // your component
|
|
45
|
+
}
|
|
46
|
+
root.render(React.createElement(App));
|
|
47
|
+
return { applyTick(t) { setTicks(prev => ({ ...prev, [t.sym + '|' + t.col]: t })); } };
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Make sure `MyPriceTable` renders `data-sym` / `data-col` on its cells (add them via your
|
|
52
|
+
cell renderer if they aren't there yet).
|
|
53
|
+
|
|
54
|
+
**AG Grid:**
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import { createGrid as agCreateGrid } from 'https://esm.sh/ag-grid-community@31';
|
|
58
|
+
|
|
59
|
+
export function createGrid(container, symbols, cols) {
|
|
60
|
+
const api = agCreateGrid(container, {
|
|
61
|
+
rowData: symbols.map(sym => ({ sym })),
|
|
62
|
+
getRowId: p => p.data.sym,
|
|
63
|
+
columnDefs: [{ field: 'sym' }, ...cols.map(c => ({
|
|
64
|
+
field: c,
|
|
65
|
+
cellRenderer: p => { const el = document.createElement('span');
|
|
66
|
+
el.dataset.sym = p.data.sym; el.dataset.col = c; el.textContent = p.value ?? ''; return el; }
|
|
67
|
+
}))],
|
|
68
|
+
domLayout: 'autoHeight'
|
|
69
|
+
});
|
|
70
|
+
const fmt = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
71
|
+
return { applyTick(t) {
|
|
72
|
+
api.applyTransactionAsync({ update: [{ sym: t.sym,
|
|
73
|
+
[t.col]: t.col === 'qty' ? new Intl.NumberFormat('en-US').format(t.v) : fmt.format(t.v / 100) }] });
|
|
74
|
+
} };
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
(CDN imports need network access when the benchmark runs; everything else is offline.)
|
|
79
|
+
|
|
80
|
+
## 2. Run
|
|
81
|
+
|
|
82
|
+
From your project directory:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx tick-bench-cli bench --impl ./my-table-adapter.mjs --name my-table
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
TickBench uses its built-in task and workload (or pass `--trace` / `--task` for your own),
|
|
89
|
+
runs your adapter and the built-in human reference side by side in headless Chromium, and
|
|
90
|
+
scores both.
|
|
91
|
+
|
|
92
|
+
## 3. Read the results
|
|
93
|
+
|
|
94
|
+
Three places, printed at the end of every run:
|
|
95
|
+
|
|
96
|
+
1. **Console** — the metric comparison table, immediately.
|
|
97
|
+
2. **`./tickbench-results/report-stream-grid.html`** — open in a browser: pass/fail cards
|
|
98
|
+
per submission with per-gate breakdown, plus the full metric table.
|
|
99
|
+
3. **`./tickbench-results/report-stream-grid.json`** — raw numbers for CI gates, charts,
|
|
100
|
+
or tracking over time.
|
|
101
|
+
|
|
102
|
+
A submission earns **PASS** only if it clears every gate: correct final values and
|
|
103
|
+
formatting, staleness within budget, stable frames, no XSS (static + dynamic), and no WCAG
|
|
104
|
+
violations. Thresholds live in the task's `oracle.config.json`.
|
package/docs/metrics.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# TickBench Metric Definitions
|
|
2
|
+
|
|
3
|
+
All metrics are computed by the runtime oracle from frame-sampled observations of rendered
|
|
4
|
+
cells against an **independent expected model** derived from the task specification (never
|
|
5
|
+
from any submission).
|
|
6
|
+
|
|
7
|
+
Let tick *k* for cell *c* arrive at time `a(c,k)` (when `applyTick` is dispatched), carrying
|
|
8
|
+
expected display value `E(c,k)`. Let `D(c,t)` be the numeric value parsed from the rendered
|
|
9
|
+
cell at frame time *t*.
|
|
10
|
+
|
|
11
|
+
| Metric | Definition |
|
|
12
|
+
|---|---|
|
|
13
|
+
| **Catch-up latency** | For each version *k* first observed rendered at frame time *t*: `t − a(c,k)`. Reported as p50/p95/p99 across all (c,k). Sampling is frame-quantized: values are upper bounds accurate to one frame. |
|
|
14
|
+
| **Stale cell-frame rate** | Fraction of (cell × frame) observations where `D(c,t) ≠ E(c, latest)` (tolerance 0.005 dollars). Measures how often users see a non-current value. |
|
|
15
|
+
| **Lost update (final)** | After quiescence, cells whose rendered numeric value differs from the final expected value — the latest version was silently never displayed. |
|
|
16
|
+
| **Format error (final)** | Rendered numeric value is correct but the string violates the task's locale formatting spec (e.g., missing thousands separators, wrong decimals). Monetary display correctness. |
|
|
17
|
+
| **Long-frame rate** | Fraction of animation-frame deltas > 33.4 ms (two missed vsync at 60 Hz). Worst frame reported separately. |
|
|
18
|
+
| **Dynamic XSS** | Trace symbol names include an injection canary; the oracle checks whether it executed (`window.__xss`). Complements static sink scanning. |
|
|
19
|
+
| **Regulated-grade pass** | Conjunction of per-dimension gates with thresholds from the task's `oracle.config.json`. |
|
|
20
|
+
|
|
21
|
+
Design notes:
|
|
22
|
+
- **Value vs. format separation** keeps staleness (recency) orthogonal to monetary formatting
|
|
23
|
+
correctness: a value can be current but mis-formatted, or correctly formatted but stale.
|
|
24
|
+
- **Coalescing is legitimate**: skipping intermediate versions is not penalized; failing to
|
|
25
|
+
ever display the *latest* version is.
|
|
26
|
+
- **Batching trade-off is visible, not punished**: rAF-batched implementations show ~1 frame
|
|
27
|
+
of added staleness but better frame stability; thresholds are set so both correct designs
|
|
28
|
+
and correct trade-offs pass while unstable or incorrect ones fail.
|
|
Binary file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Authoring a TickBench Task
|
|
2
|
+
|
|
3
|
+
A task directory contains:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
tasks/<name>/
|
|
7
|
+
spec.md natural-language spec = what an agent (or human) receives
|
|
8
|
+
oracle.config.json trace + regulated-grade thresholds (tickbench-task/1 schema)
|
|
9
|
+
reference/grid.mjs human reference implementation (must pass regulated-grade)
|
|
10
|
+
submissions/<id>/grid.mjs submissions under evaluation (agent output or mutants)
|
|
11
|
+
submissions/<id>/PROVENANCE.md model, version, prompt, date — or mutant rationale
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Contract: `createGrid(container, symbols, cols) -> { applyTick(tick) }`; rendered cells must
|
|
15
|
+
carry `data-sym` and `data-col` attributes (the observable surface the oracle samples).
|
|
16
|
+
|
|
17
|
+
Rules:
|
|
18
|
+
1. The reference must achieve `regulated_grade_pass` (`tickbench validate`).
|
|
19
|
+
2. Every oracle you rely on needs at least one **validation mutant** listed in
|
|
20
|
+
`validationMutants` that the oracle demonstrably fails.
|
|
21
|
+
3. Traces: synthetic traces are generated (`tickbench gen`, seeded). Real-feed traces
|
|
22
|
+
(recorded WebSocket market data, e.g. public crypto feeds) must set `"synthetic": false`
|
|
23
|
+
and ship a `*.provenance.md` (source, window, sample size) next to the trace file.
|
|
24
|
+
4. Do not leak the oracle into the spec verbatim; specs state requirements, oracles verify them.
|
|
25
|
+
|
|
26
|
+
## Trace data license
|
|
27
|
+
|
|
28
|
+
Trace files under `traces/` (synthetic or recorded) are licensed **CC BY 4.0**; the harness code is MIT. Recorded traces must be anonymized: no session tokens, no PII, provenance limited to source tool, window, and sample size.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// EXAMPLE ADAPTER — copy this file into your project and edit the marked lines.
|
|
2
|
+
//
|
|
3
|
+
// TickBench doesn't care what framework your table uses. It only needs this contract:
|
|
4
|
+
// createGrid(container, symbols, cols) -> { applyTick(tick) }
|
|
5
|
+
// and rendered cells carrying data-sym / data-col attributes so the oracle can read them.
|
|
6
|
+
//
|
|
7
|
+
// Replace the internals below with calls into YOUR component:
|
|
8
|
+
// - mount: render your table into `container` (ReactDOM.createRoot, new AgGrid, etc.)
|
|
9
|
+
// - update: route `applyTick` into your component's data path (setState, applyTransaction…)
|
|
10
|
+
//
|
|
11
|
+
// Framework imports must be full URLs (the harness loads this file as a module in a bare
|
|
12
|
+
// browser page — no bundler), e.g.: import { createRoot } from 'https://esm.sh/react-dom@18/client'
|
|
13
|
+
|
|
14
|
+
export function createGrid(container, symbols, cols) {
|
|
15
|
+
// --- mount: REPLACE with your component's mount code -------------------
|
|
16
|
+
const priceFmt = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
17
|
+
const qtyFmt = new Intl.NumberFormat('en-US');
|
|
18
|
+
const table = document.createElement('table');
|
|
19
|
+
table.setAttribute('aria-label', 'My table under test');
|
|
20
|
+
const caption = document.createElement('caption');
|
|
21
|
+
caption.textContent = 'My table under test';
|
|
22
|
+
table.appendChild(caption);
|
|
23
|
+
const cells = new Map();
|
|
24
|
+
const thead = document.createElement('thead'); const hr = document.createElement('tr');
|
|
25
|
+
const h0 = document.createElement('th'); h0.scope = 'col'; h0.textContent = 'Symbol'; hr.appendChild(h0);
|
|
26
|
+
for (const c of cols) { const th = document.createElement('th'); th.scope = 'col'; th.textContent = c; hr.appendChild(th); }
|
|
27
|
+
thead.appendChild(hr); table.appendChild(thead);
|
|
28
|
+
const tbody = document.createElement('tbody');
|
|
29
|
+
for (const s of symbols) {
|
|
30
|
+
const tr = document.createElement('tr');
|
|
31
|
+
const th = document.createElement('th'); th.scope = 'row'; th.textContent = s; tr.appendChild(th);
|
|
32
|
+
for (const c of cols) {
|
|
33
|
+
const td = document.createElement('td');
|
|
34
|
+
td.dataset.sym = s; td.dataset.col = c; // <-- REQUIRED: observable surface
|
|
35
|
+
tr.appendChild(td); cells.set(s + '|' + c, td);
|
|
36
|
+
}
|
|
37
|
+
tbody.appendChild(tr);
|
|
38
|
+
}
|
|
39
|
+
table.appendChild(tbody); container.appendChild(table);
|
|
40
|
+
|
|
41
|
+
// --- update: REPLACE with your component's update path ------------------
|
|
42
|
+
const pending = new Map(); let scheduled = false;
|
|
43
|
+
const flush = () => {
|
|
44
|
+
scheduled = false;
|
|
45
|
+
for (const [k, t] of pending) {
|
|
46
|
+
const td = cells.get(k);
|
|
47
|
+
if (td) td.textContent = t.col === 'qty' ? qtyFmt.format(t.v) : priceFmt.format(t.v / 100);
|
|
48
|
+
}
|
|
49
|
+
pending.clear();
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
applyTick(tick) { // tick = { t, sym, col, v } (prices are integer cents)
|
|
53
|
+
pending.set(tick.sym + '|' + tick.col, tick);
|
|
54
|
+
if (!scheduled) { scheduled = true; requestAnimationFrame(flush); }
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|