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.
Files changed (35) hide show
  1. package/CITATION.cff +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +187 -0
  4. package/docs/agent-adapters.md +31 -0
  5. package/docs/banner.svg +59 -0
  6. package/docs/connect-your-table.md +104 -0
  7. package/docs/metrics.md +28 -0
  8. package/docs/report-screenshot.png +0 -0
  9. package/docs/task-authoring.md +28 -0
  10. package/examples/my-table-adapter.mjs +57 -0
  11. package/examples/walkthrough/README.md +114 -0
  12. package/examples/walkthrough/adapter.mjs +35 -0
  13. package/examples/walkthrough/my-table.mjs +49 -0
  14. package/examples/walkthrough/sample-results/report-stream-grid.html +23 -0
  15. package/examples/walkthrough/sample-results/report-stream-grid.json +74 -0
  16. package/package.json +61 -0
  17. package/schemas/task.schema.json +24 -0
  18. package/schemas/trace.schema.json +28 -0
  19. package/src/cli.mjs +129 -0
  20. package/src/index.d.ts +77 -0
  21. package/src/index.mjs +7 -0
  22. package/src/oracles/a11y.mjs +31 -0
  23. package/src/oracles/runtime.mjs +93 -0
  24. package/src/oracles/security.mjs +24 -0
  25. package/src/report/aggregate.mjs +17 -0
  26. package/src/report/html.mjs +56 -0
  27. package/src/report/verdict.mjs +16 -0
  28. package/src/trace/generate.mjs +29 -0
  29. package/tasks/stream-grid/oracle.config.json +14 -0
  30. package/tasks/stream-grid/reference/grid.mjs +29 -0
  31. package/tasks/stream-grid/spec.md +9 -0
  32. package/tasks/stream-grid/submissions/claude-fable-5/PROVENANCE.md +10 -0
  33. package/tasks/stream-grid/submissions/claude-fable-5/grid.mjs +72 -0
  34. package/tasks/stream-grid/submissions/naive-baseline/PROVENANCE.md +6 -0
  35. 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
+ [![CI](https://github.com/RomanFedytskyi/TickBench/actions/workflows/ci.yml/badge.svg)](https://github.com/RomanFedytskyi/TickBench/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/tick-bench-cli.svg)](https://www.npmjs.com/package/tick-bench-cli)
5
+ [![npm downloads](https://img.shields.io/npm/dw/tick-bench-cli.svg)](https://www.npmjs.com/package/tick-bench-cli)
6
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/tick-bench-cli.svg)](https://bundlephobia.com/package/tick-bench-cli)
7
+ [![types](https://img.shields.io/npm/types/tick-bench-cli.svg)](https://www.npmjs.com/package/tick-bench-cli)
8
+ [![Node.js >=18](https://img.shields.io/node/v/tick-bench-cli.svg)](https://www.npmjs.com/package/tick-bench-cli)
9
+ [![harness](https://img.shields.io/badge/submissions-any%20framework-blue.svg)](#benchmark-your-own-component)
10
+ [![license](https://img.shields.io/badge/code-MIT-green.svg)](LICENSE)
11
+ [![data](https://img.shields.io/badge/traces-CC--BY--4.0-orange.svg)](docs/task-authoring.md)
12
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.1294564668-blue.svg)](https://doi.org/10.5281/zenodo.21272054)
13
+ [![GitHub last commit](https://img.shields.io/github/last-commit/RomanFedytskyi/TickBench.svg)](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.
@@ -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 &#183; update latency &#183; frame stability &#183; money formatting &#183; XSS &#183; 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">&#9650; 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">&#9660; 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">&#9888; stale 412 ms &#8594; 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`.
@@ -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
+ }