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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Walkthrough: benchmark a simple table, end to end
|
|
2
|
+
|
|
3
|
+
This folder is a complete, runnable example. It contains an ordinary table component
|
|
4
|
+
(pretend it's yours) and the one adapter file that connects it to TickBench.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
walkthrough/
|
|
8
|
+
my-table.mjs <- "your" existing component: SimpleTable with mount()/setCell()
|
|
9
|
+
adapter.mjs <- the ONLY file you write: imports my-table.mjs, implements the contract
|
|
10
|
+
README.md <- you are here
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Step 0 — install (once)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install tick-bench-cli playwright-core axe-core
|
|
17
|
+
npx playwright-core install chromium-headless-shell
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Step 1 — your component
|
|
21
|
+
|
|
22
|
+
`my-table.mjs` is a plain class with its own API — it knows nothing about TickBench:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
const table = new SimpleTable({ rows, columns, title: 'Live prices' });
|
|
26
|
+
table.mount(el);
|
|
27
|
+
table.setCell('AAPL', 'bid', '12,345.67');
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Step 2 — the adapter (the only thing you write)
|
|
31
|
+
|
|
32
|
+
One thing people often ask: **where does `applyTick` come from?** Nowhere — you don't
|
|
33
|
+
import it. You *write* it inside the object your `createGrid` returns, and TickBench calls
|
|
34
|
+
it (about 17,000 times) to deliver price updates to your table. Direction of control:
|
|
35
|
+
TickBench imports your file and calls your functions, never the other way round.
|
|
36
|
+
|
|
37
|
+
`adapter.mjs` does three things — mount, tag, route:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { SimpleTable } from './my-table.mjs'; // relative imports work
|
|
41
|
+
|
|
42
|
+
export function createGrid(container, symbols, cols) {
|
|
43
|
+
const table = new SimpleTable({ rows: symbols, columns: cols }); // 1. mount
|
|
44
|
+
table.mount(container);
|
|
45
|
+
|
|
46
|
+
for (const sym of symbols) for (const col of cols) { // 2. tag cells
|
|
47
|
+
const td = table.getCell(sym, col); // so the oracle
|
|
48
|
+
td.dataset.sym = sym; td.dataset.col = col; // can watch them
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { applyTick(tick) { /* 3. route ticks into table.setCell(...) */ } };
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
(Full version with money formatting and rAF batching is in the file.)
|
|
56
|
+
|
|
57
|
+
## Step 3 — run
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx tick-bench-cli bench --impl ./adapter.mjs --name simple-table
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Step 4 — what you get
|
|
64
|
+
|
|
65
|
+
Actual output of this exact example:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
reference simple-table
|
|
69
|
+
catch-up latency p50 (ms) 24.5 24.5
|
|
70
|
+
catch-up latency p95 (ms) 31.6 31.6
|
|
71
|
+
stale cell-frames (%) 17.7 17.48
|
|
72
|
+
long frames >33ms (%) 0.2 0
|
|
73
|
+
worst frame (ms) 150 33.4
|
|
74
|
+
final value errors 0 0
|
|
75
|
+
final format errors 0 0
|
|
76
|
+
XSS triggered (dynamic) false false
|
|
77
|
+
security findings (high) 0 0
|
|
78
|
+
axe violations 0 0
|
|
79
|
+
REGULATED-GRADE PASS true true
|
|
80
|
+
|
|
81
|
+
Results:
|
|
82
|
+
console : table above
|
|
83
|
+
html : ./tickbench-results/report-stream-grid.html <- open this in a browser
|
|
84
|
+
json : ./tickbench-results/report-stream-grid.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Open the HTML file: one card per submission (green PASS / red FAIL with a per-gate
|
|
88
|
+
checklist) and the full metric table. The JSON has every number for CI or charts.
|
|
89
|
+
|
|
90
|
+
A committed copy of this exact run is in [`sample-results/`](sample-results/) — open
|
|
91
|
+
[`sample-results/report-stream-grid.html`](sample-results/report-stream-grid.html) in a
|
|
92
|
+
browser right now, before running anything, to see what you'll get. Screenshot:
|
|
93
|
+
|
|
94
|
+
<p align="center">
|
|
95
|
+
<img src="../../docs/report-screenshot.png" alt="TickBench HTML report for this walkthrough: PASS cards for reference and simple-table with per-gate checklists, plus the metric table." width="90%" />
|
|
96
|
+
</p>
|
|
97
|
+
|
|
98
|
+
How to read the key rows:
|
|
99
|
+
|
|
100
|
+
- **catch-up latency p95 = 31.6 ms** — when a price arrives, the correct value is on
|
|
101
|
+
screen within ~2 frames at the 95th percentile.
|
|
102
|
+
- **stale cell-frames ≈ 17%** — the cost of rAF batching (about one frame of staleness);
|
|
103
|
+
within the 20% budget, and it buys the perfect frame stability on the next row.
|
|
104
|
+
- **final errors = 0** — after the burst, every cell shows exactly the right,
|
|
105
|
+
correctly-formatted amount.
|
|
106
|
+
- **XSS / axe = clean** — the trace injected a malicious symbol name and it never
|
|
107
|
+
executed; the table kept caption/scope/headers.
|
|
108
|
+
|
|
109
|
+
## Step 5 — see it fail (optional, 30 seconds)
|
|
110
|
+
|
|
111
|
+
In `adapter.mjs`, replace `table.setCell(...)` with
|
|
112
|
+
`table.getCell(t.sym, t.col).innerHTML = ...` and rerun. The static scan flags the
|
|
113
|
+
innerHTML sink; if you also interpolate the symbol into that HTML, the dynamic XSS canary
|
|
114
|
+
fires and the verdict flips to FAIL. That's the tool doing its job.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// THE ADAPTER — the only file you write for TickBench. It imports your component
|
|
2
|
+
// (relative imports work), mounts it, tags cells for the oracle, and routes ticks in.
|
|
3
|
+
import { SimpleTable } from './my-table.mjs';
|
|
4
|
+
|
|
5
|
+
export function createGrid(container, symbols, cols) {
|
|
6
|
+
// 1. Mount your component exactly the way your app does.
|
|
7
|
+
const table = new SimpleTable({ rows: symbols, columns: cols, title: 'Live prices' });
|
|
8
|
+
table.mount(container);
|
|
9
|
+
|
|
10
|
+
// 2. Tag rendered cells so the oracle can observe them (your component stays untouched).
|
|
11
|
+
for (const sym of symbols) for (const col of cols) {
|
|
12
|
+
const td = table.getCell(sym, col);
|
|
13
|
+
td.dataset.sym = sym;
|
|
14
|
+
td.dataset.col = col;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 3. Route ticks into your component's own update API.
|
|
18
|
+
// Prices arrive as integer cents; the task spec requires en-US, 2 decimals.
|
|
19
|
+
const priceFmt = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
20
|
+
const qtyFmt = new Intl.NumberFormat('en-US');
|
|
21
|
+
const pending = new Map();
|
|
22
|
+
let scheduled = false;
|
|
23
|
+
const flush = () => {
|
|
24
|
+
scheduled = false;
|
|
25
|
+
for (const [, t] of pending)
|
|
26
|
+
table.setCell(t.sym, t.col, t.col === 'qty' ? qtyFmt.format(t.v) : priceFmt.format(t.v / 100));
|
|
27
|
+
pending.clear();
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
applyTick(tick) {
|
|
31
|
+
pending.set(tick.sym + ' ' + tick.col, tick);
|
|
32
|
+
if (!scheduled) { scheduled = true; requestAnimationFrame(flush); }
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// YOUR EXISTING COMPONENT (stand-in). Note: it knows nothing about TickBench —
|
|
2
|
+
// it has its own ordinary API: mount(el), setCell(row, col, text), getCell(row, col).
|
|
3
|
+
export class SimpleTable {
|
|
4
|
+
constructor({ rows, columns, title = 'Prices' }) {
|
|
5
|
+
this.rows = rows;
|
|
6
|
+
this.columns = columns;
|
|
7
|
+
this.title = title;
|
|
8
|
+
this.cells = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
mount(el) {
|
|
12
|
+
const table = document.createElement('table');
|
|
13
|
+
const caption = document.createElement('caption');
|
|
14
|
+
caption.textContent = this.title;
|
|
15
|
+
table.appendChild(caption);
|
|
16
|
+
|
|
17
|
+
const thead = document.createElement('thead');
|
|
18
|
+
const hr = document.createElement('tr');
|
|
19
|
+
const h0 = document.createElement('th');
|
|
20
|
+
h0.scope = 'col'; h0.textContent = 'Symbol';
|
|
21
|
+
hr.appendChild(h0);
|
|
22
|
+
for (const c of this.columns) {
|
|
23
|
+
const th = document.createElement('th');
|
|
24
|
+
th.scope = 'col'; th.textContent = c;
|
|
25
|
+
hr.appendChild(th);
|
|
26
|
+
}
|
|
27
|
+
thead.appendChild(hr);
|
|
28
|
+
table.appendChild(thead);
|
|
29
|
+
|
|
30
|
+
const tbody = document.createElement('tbody');
|
|
31
|
+
for (const r of this.rows) {
|
|
32
|
+
const tr = document.createElement('tr');
|
|
33
|
+
const th = document.createElement('th');
|
|
34
|
+
th.scope = 'row'; th.textContent = r;
|
|
35
|
+
tr.appendChild(th);
|
|
36
|
+
for (const c of this.columns) {
|
|
37
|
+
const td = document.createElement('td');
|
|
38
|
+
tr.appendChild(td);
|
|
39
|
+
this.cells.set(r + ' ' + c, td);
|
|
40
|
+
}
|
|
41
|
+
tbody.appendChild(tr);
|
|
42
|
+
}
|
|
43
|
+
table.appendChild(tbody);
|
|
44
|
+
el.appendChild(table);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getCell(row, col) { return this.cells.get(row + ' ' + col); }
|
|
48
|
+
setCell(row, col, text) { this.getCell(row, col).textContent = text; }
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>TickBench report — stream-grid</title>
|
|
2
|
+
<style>
|
|
3
|
+
body{background:#0b1220;color:#e2e8f0;font:15px/1.5 ui-sans-serif,system-ui,'Segoe UI',sans-serif;margin:0;padding:40px}
|
|
4
|
+
h1{font-size:26px} .sub{color:#94a3b8;margin-bottom:28px}
|
|
5
|
+
.cards{display:flex;gap:16px;flex-wrap:wrap;margin-bottom:32px}
|
|
6
|
+
.card{background:#0d1626;border:1px solid #24405c;border-radius:10px;padding:18px 22px;min-width:220px}
|
|
7
|
+
.card h2{margin:0 0 8px;font-size:17px}
|
|
8
|
+
.card.pass{border-color:#22d3a5}.card.fail{border-color:#f87171}
|
|
9
|
+
.badge{display:inline-block;font-weight:700;font-size:13px;border-radius:6px;padding:2px 10px;margin-bottom:10px}
|
|
10
|
+
.pass .badge{background:#0f2e26;color:#22d3a5}.fail .badge{background:#301722;color:#f87171}
|
|
11
|
+
ul{list-style:none;margin:0;padding:0;font-size:13.5px} li{padding:1.5px 0} li.ok{color:#86efac} li.bad{color:#fca5a5}
|
|
12
|
+
table{border-collapse:collapse;width:100%;max-width:900px;font-variant-numeric:tabular-nums}
|
|
13
|
+
th,td{text-align:left;padding:8px 14px;border-bottom:1px solid #1e3a52} th{color:#94a3b8;font-weight:600}
|
|
14
|
+
tr:hover td{background:#0d1626}
|
|
15
|
+
.meta{color:#64748b;font-size:13px;margin-top:26px}
|
|
16
|
+
</style></head><body>
|
|
17
|
+
<h1>TickBench report — <code>stream-grid</code></h1>
|
|
18
|
+
<div class="sub">trace <code>(generated in-memory, seed 0x5eed)</code> · seed 24301 · 2026-07-09T03:39:41.775Z · Node v22.22.3</div>
|
|
19
|
+
<div class="cards"><div class="card pass"><h2>reference</h2>
|
|
20
|
+
<div class="badge">PASS</div><ul><li class="ok">✓ Final state correct</li><li class="ok">✓ Staleness within budget</li><li class="ok">✓ Frame stability</li><li class="ok">✓ Security</li><li class="ok">✓ Accessibility</li><li class="ok">✓ No runtime errors</li></ul></div><div class="card pass"><h2>simple-table</h2>
|
|
21
|
+
<div class="badge">PASS</div><ul><li class="ok">✓ Final state correct</li><li class="ok">✓ Staleness within budget</li><li class="ok">✓ Frame stability</li><li class="ok">✓ Security</li><li class="ok">✓ Accessibility</li><li class="ok">✓ No runtime errors</li></ul></div></div><table><thead><tr><th>Metric</th><th>reference</th><th>simple-table</th></tr></thead><tbody><tr><td>Catch-up latency p50 (ms)</td><td>24.5</td><td>24.7</td></tr><tr><td>Catch-up latency p95 (ms)</td><td>31.7</td><td>31.8</td></tr><tr><td>Catch-up latency p99 (ms)</td><td>32.9</td><td>33.3</td></tr><tr><td>Stale cell-frames (%)</td><td>17.44</td><td>17.48</td></tr><tr><td>Long frames >33ms (%)</td><td>0</td><td>0</td></tr><tr><td>Worst frame (ms)</td><td>33.4</td><td>16.8</td></tr><tr><td>Final value errors</td><td>0</td><td>0</td></tr><tr><td>Final format errors</td><td>0</td><td>0</td></tr><tr><td>XSS triggered (dynamic)</td><td>false</td><td>false</td></tr><tr><td>Security findings (high)</td><td>0</td><td>0</td></tr><tr><td>WCAG violations (axe)</td><td>0</td><td>0</td></tr></tbody></table>
|
|
22
|
+
<div class="meta">Regulated-grade thresholds come from the task's <code>oracle.config.json</code>. Metric definitions: docs/metrics.md.</div>
|
|
23
|
+
</body></html>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "tickbench-report/1",
|
|
3
|
+
"generated": "2026-07-09T03:39:41.775Z",
|
|
4
|
+
"task": "stream-grid",
|
|
5
|
+
"trace": "(generated in-memory, seed 0x5eed)",
|
|
6
|
+
"traceSeed": 24301,
|
|
7
|
+
"node": "v22.22.3",
|
|
8
|
+
"results": {
|
|
9
|
+
"reference": {
|
|
10
|
+
"impl": "<package>/tasks/stream-grid/reference/grid.mjs",
|
|
11
|
+
"runtime": {
|
|
12
|
+
"ticksDispatched": 17449,
|
|
13
|
+
"cellsChecked": 160,
|
|
14
|
+
"catchUpMs": {
|
|
15
|
+
"p50": 24.5,
|
|
16
|
+
"p95": 31.700000002980232,
|
|
17
|
+
"p99": 32.900000005960464,
|
|
18
|
+
"n": 10531
|
|
19
|
+
},
|
|
20
|
+
"staleCellFramePct": 17.44,
|
|
21
|
+
"frames": 512,
|
|
22
|
+
"longFramePct": 0,
|
|
23
|
+
"worstFrameMs": 33.4,
|
|
24
|
+
"finalValueErrors": 0,
|
|
25
|
+
"finalFormatErrors": 0,
|
|
26
|
+
"xssTriggered": false,
|
|
27
|
+
"implError": null
|
|
28
|
+
},
|
|
29
|
+
"a11y": [],
|
|
30
|
+
"security": [],
|
|
31
|
+
"verdict": {
|
|
32
|
+
"functional_final_state": true,
|
|
33
|
+
"staleness": true,
|
|
34
|
+
"frame_stability": true,
|
|
35
|
+
"security": true,
|
|
36
|
+
"accessibility": true,
|
|
37
|
+
"no_runtime_error": true,
|
|
38
|
+
"regulated_grade_pass": true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"simple-table": {
|
|
42
|
+
"impl": "./adapter.mjs",
|
|
43
|
+
"runtime": {
|
|
44
|
+
"ticksDispatched": 17449,
|
|
45
|
+
"cellsChecked": 160,
|
|
46
|
+
"catchUpMs": {
|
|
47
|
+
"p50": 24.69999999552965,
|
|
48
|
+
"p95": 31.799999997019768,
|
|
49
|
+
"p99": 33.29999999701977,
|
|
50
|
+
"n": 10485
|
|
51
|
+
},
|
|
52
|
+
"staleCellFramePct": 17.48,
|
|
53
|
+
"frames": 513,
|
|
54
|
+
"longFramePct": 0,
|
|
55
|
+
"worstFrameMs": 16.8,
|
|
56
|
+
"finalValueErrors": 0,
|
|
57
|
+
"finalFormatErrors": 0,
|
|
58
|
+
"xssTriggered": false,
|
|
59
|
+
"implError": null
|
|
60
|
+
},
|
|
61
|
+
"a11y": [],
|
|
62
|
+
"security": [],
|
|
63
|
+
"verdict": {
|
|
64
|
+
"functional_final_state": true,
|
|
65
|
+
"staleness": true,
|
|
66
|
+
"frame_stability": true,
|
|
67
|
+
"security": true,
|
|
68
|
+
"accessibility": true,
|
|
69
|
+
"no_runtime_error": true,
|
|
70
|
+
"regulated_grade_pass": true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tick-bench-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Benchmark your live data grid under real market load: display staleness, update latency, frame stability, money formatting, XSS, and WCAG — replayable seeded tick streams, executable oracles, one pass/fail verdict.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Roman Fedytskyi <fedytskyi@gmail.com>",
|
|
8
|
+
"bin": {
|
|
9
|
+
"tickbench": "src/cli.mjs"
|
|
10
|
+
},
|
|
11
|
+
"main": "src/index.mjs",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"tasks",
|
|
18
|
+
"schemas",
|
|
19
|
+
"docs",
|
|
20
|
+
"examples",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"CITATION.cff"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"gen": "node src/cli.mjs gen --seed 24301 --duration 8000 --out traces/demo.json",
|
|
27
|
+
"bench": "node src/cli.mjs bench --task tasks/stream-grid",
|
|
28
|
+
"bench:smoke": "node src/cli.mjs gen --seed 24301 --duration 2000 --out traces/smoke.json && node src/cli.mjs bench --task tasks/stream-grid --trace traces/smoke.json --out tickbench-results-smoke",
|
|
29
|
+
"test": "node --test",
|
|
30
|
+
"validate": "node src/cli.mjs validate --task tasks/stream-grid"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/RomanFedytskyi/TickBench.git"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"benchmark",
|
|
38
|
+
"fintech",
|
|
39
|
+
"dashboard",
|
|
40
|
+
"data-grid",
|
|
41
|
+
"real-time",
|
|
42
|
+
"streaming",
|
|
43
|
+
"coding-agents",
|
|
44
|
+
"llm",
|
|
45
|
+
"ai-generated-code",
|
|
46
|
+
"web-performance",
|
|
47
|
+
"accessibility",
|
|
48
|
+
"wcag",
|
|
49
|
+
"xss",
|
|
50
|
+
"display-staleness",
|
|
51
|
+
"playwright"
|
|
52
|
+
],
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"axe-core": "^4.10.0",
|
|
58
|
+
"playwright-core": "^1.50.0"
|
|
59
|
+
},
|
|
60
|
+
"types": "src/index.d.ts"
|
|
61
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "tickbench-task/1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["schema", "task", "trace", "thresholds"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"schema": { "const": "tickbench-task/1" },
|
|
8
|
+
"task": { "type": "string" },
|
|
9
|
+
"trace": { "type": "string" },
|
|
10
|
+
"thresholds": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["catchUpP95Ms", "staleCellFramePctMax", "longFramePctMax"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"catchUpP95Ms": { "type": "number" },
|
|
15
|
+
"staleCellFramePctMax": { "type": "number" },
|
|
16
|
+
"longFramePctMax": { "type": "number" },
|
|
17
|
+
"finalErrorsMax": { "type": "number" },
|
|
18
|
+
"axeViolationsMax": { "type": "number" },
|
|
19
|
+
"highSecurityFindingsMax": { "type": "number" }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"validationMutants": { "type": "array", "items": { "type": "string" } }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "tickbench-trace/1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["schema", "seed", "synthetic", "symbols", "cols", "durationMs", "ticks"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"schema": { "const": "tickbench-trace/1" },
|
|
8
|
+
"seed": { "type": "number" },
|
|
9
|
+
"synthetic": { "type": "boolean", "description": "false only for traces recorded from real feeds, which must ship a *.provenance.md" },
|
|
10
|
+
"generated": { "type": ["string", "null"] },
|
|
11
|
+
"symbols": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
|
|
12
|
+
"cols": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
|
|
13
|
+
"durationMs": { "type": "number", "exclusiveMinimum": 0 },
|
|
14
|
+
"ticks": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"items": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": ["t", "sym", "col", "v"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"t": { "type": "number", "minimum": 0, "description": "ms offset from trace start" },
|
|
21
|
+
"sym": { "type": "string" },
|
|
22
|
+
"col": { "type": "string" },
|
|
23
|
+
"v": { "type": "number", "description": "integer cents for price columns; count for qty" }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// TickBench CLI: gen | bench | validate
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join, basename, resolve, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { generateTrace } from './trace/generate.mjs';
|
|
7
|
+
import { runRuntimeOracle } from './oracles/runtime.mjs';
|
|
8
|
+
import { runA11yOracle } from './oracles/a11y.mjs';
|
|
9
|
+
import { scanSecurity } from './oracles/security.mjs';
|
|
10
|
+
import { computeVerdict } from './report/verdict.mjs';
|
|
11
|
+
import { summaryTable } from './report/aggregate.mjs';
|
|
12
|
+
import { renderHtmlReport } from './report/html.mjs';
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const cmd = args[0];
|
|
16
|
+
const opt = (name, dflt) => { const i = args.indexOf('--' + name); return i > -1 ? args[i + 1] : dflt; };
|
|
17
|
+
const flag = name => args.includes('--' + name);
|
|
18
|
+
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const DEFAULT_TASK = join(PKG_ROOT, 'tasks', 'stream-grid');
|
|
20
|
+
|
|
21
|
+
function loadTaskConfig(taskDir) {
|
|
22
|
+
return JSON.parse(readFileSync(join(taskDir, 'oracle.config.json'), 'utf8'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function discoverSubmissions(taskDir) {
|
|
26
|
+
const subs = [{ name: 'reference', path: join(taskDir, 'reference', 'grid.mjs') }];
|
|
27
|
+
const subDir = join(taskDir, 'submissions');
|
|
28
|
+
if (existsSync(subDir)) for (const d of readdirSync(subDir))
|
|
29
|
+
if (existsSync(join(subDir, d, 'grid.mjs'))) subs.push({ name: d, path: join(subDir, d, 'grid.mjs') });
|
|
30
|
+
return subs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadTrace(taskDir, cfg) {
|
|
34
|
+
const requested = opt('trace', null);
|
|
35
|
+
const candidates = requested
|
|
36
|
+
? [requested]
|
|
37
|
+
: [cfg.trace, join(taskDir, cfg.trace), join(PKG_ROOT, cfg.trace)];
|
|
38
|
+
for (const c of candidates) if (c && existsSync(c)) return { trace: JSON.parse(readFileSync(c, 'utf8')), source: c };
|
|
39
|
+
const trace = generateTrace({ seed: 24301 });
|
|
40
|
+
trace.generated = new Date().toISOString();
|
|
41
|
+
return { trace, source: `(generated in-memory, seed 0x${(24301).toString(16)})` };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (cmd === 'gen') {
|
|
45
|
+
const seed = Number(opt('seed', '24301'));
|
|
46
|
+
const trace = generateTrace({ seed, durationMs: Number(opt('duration', '8000')), symbolCount: Number(opt('symbols', '40')) });
|
|
47
|
+
trace.generated = new Date().toISOString();
|
|
48
|
+
const out = opt('out', 'traces/demo.json');
|
|
49
|
+
mkdirSync(dirname(out) || '.', { recursive: true });
|
|
50
|
+
writeFileSync(out, JSON.stringify(trace));
|
|
51
|
+
console.log(`trace: ${trace.ticks.length} ticks over ${trace.durationMs}ms, seed 0x${seed.toString(16)} -> ${out}`);
|
|
52
|
+
} else if (cmd === 'bench') {
|
|
53
|
+
const implOpt = opt('impl', null);
|
|
54
|
+
const taskDir = opt('task', null) ? resolve(opt('task', null)) : implOpt ? DEFAULT_TASK : resolve('tasks/stream-grid');
|
|
55
|
+
const cfg = loadTaskConfig(taskDir);
|
|
56
|
+
let subs;
|
|
57
|
+
if (implOpt) {
|
|
58
|
+
// Standalone mode: benchmark YOUR component from any directory.
|
|
59
|
+
subs = [];
|
|
60
|
+
if (!flag('no-reference')) subs.push({ name: 'reference', path: join(taskDir, 'reference', 'grid.mjs') });
|
|
61
|
+
subs.push({ name: opt('name', basename(implOpt, '.mjs').replace(/\.js$/, '') || 'my-grid'), path: resolve(implOpt) });
|
|
62
|
+
} else {
|
|
63
|
+
subs = discoverSubmissions(taskDir);
|
|
64
|
+
const only = opt('submission', null);
|
|
65
|
+
if (only) subs = subs.filter(s => s.name === only);
|
|
66
|
+
}
|
|
67
|
+
const { trace, source } = loadTrace(taskDir, cfg);
|
|
68
|
+
const outDir = resolve(opt('out', 'tickbench-results'));
|
|
69
|
+
const report = { schema: 'tickbench-report/1', generated: new Date().toISOString(), task: basename(taskDir), trace: source, traceSeed: trace.seed, node: process.version, results: {} };
|
|
70
|
+
for (const sub of subs) {
|
|
71
|
+
console.error('== benchmarking: ' + sub.name);
|
|
72
|
+
const runtime = await runRuntimeOracle({ implPath: sub.path, trace });
|
|
73
|
+
const a11y = await runA11yOracle({ implPath: sub.path, trace });
|
|
74
|
+
const security = scanSecurity(sub.path);
|
|
75
|
+
const verdict = computeVerdict({ runtime, a11y, security }, cfg.thresholds);
|
|
76
|
+
report.results[sub.name] = { impl: sub.path, runtime, a11y, security, verdict };
|
|
77
|
+
}
|
|
78
|
+
mkdirSync(outDir, { recursive: true });
|
|
79
|
+
const jsonFile = join(outDir, `report-${basename(taskDir)}.json`);
|
|
80
|
+
const htmlFile = join(outDir, `report-${basename(taskDir)}.html`);
|
|
81
|
+
writeFileSync(jsonFile, JSON.stringify(report, null, 2));
|
|
82
|
+
writeFileSync(htmlFile, renderHtmlReport(report));
|
|
83
|
+
console.log('\n' + summaryTable(report.results));
|
|
84
|
+
console.log('\nResults:');
|
|
85
|
+
console.log(' console : table above');
|
|
86
|
+
console.log(' html : ' + htmlFile + ' <- open this in a browser');
|
|
87
|
+
console.log(' json : ' + jsonFile);
|
|
88
|
+
} else if (cmd === 'new-submission') {
|
|
89
|
+
const name = args[1] && !args[1].startsWith('--') ? args[1] : opt('name', null);
|
|
90
|
+
if (!name) { console.error('usage: tickbench new-submission <name> [--task DIR]'); process.exit(1); }
|
|
91
|
+
const taskDir = opt('task', null) ? resolve(opt('task', null)) : (existsSync('tasks/stream-grid') ? resolve('tasks/stream-grid') : DEFAULT_TASK);
|
|
92
|
+
const dir = join(taskDir, 'submissions', name);
|
|
93
|
+
mkdirSync(dir, { recursive: true });
|
|
94
|
+
if (!existsSync(join(dir, 'grid.mjs')))
|
|
95
|
+
writeFileSync(join(dir, 'grid.mjs'), '// Paste the model\u2019s unedited output for tasks/<task>/spec.md here.\n// Contract: export function createGrid(container, symbols, cols) -> { applyTick(tick) }\n');
|
|
96
|
+
writeFileSync(join(dir, 'PROVENANCE.md'), `# Provenance: ${name}\n\n- **Model:** <model name and version>\n- **Date:** ${new Date().toISOString().slice(0, 10)}\n- **Mode:** single-shot | agentic (tools/iterations: <n>)\n- **Prompt:** verbatim contents of tasks/<task>/spec.md (attach any extra system prompt)\n- **Post-editing:** none (required for benchmark validity)\n`);
|
|
97
|
+
console.log('created ' + dir + '\n 1. run the task spec through the model\n 2. paste its unedited output into grid.mjs\n 3. fill PROVENANCE.md\n 4. npx tick-bench-cli bench --task ' + taskDir);
|
|
98
|
+
} else if (cmd === 'validate') {
|
|
99
|
+
const taskDir = resolve(opt('task', 'tasks/stream-grid'));
|
|
100
|
+
const cfg = loadTaskConfig(taskDir);
|
|
101
|
+
const subs = discoverSubmissions(taskDir);
|
|
102
|
+
const trace = generateTrace({ seed: 7, durationMs: 2000 });
|
|
103
|
+
let ok = true;
|
|
104
|
+
for (const sub of subs) {
|
|
105
|
+
const runtime = await runRuntimeOracle({ implPath: sub.path, trace });
|
|
106
|
+
const a11y = await runA11yOracle({ implPath: sub.path, trace });
|
|
107
|
+
const security = scanSecurity(sub.path);
|
|
108
|
+
const v = computeVerdict({ runtime, a11y, security }, cfg.thresholds);
|
|
109
|
+
const isMutant = (cfg.validationMutants || []).includes(sub.name);
|
|
110
|
+
const expected = sub.name === 'reference' ? true : isMutant ? false : null;
|
|
111
|
+
if (expected !== null && v.regulated_grade_pass !== expected) {
|
|
112
|
+
ok = false;
|
|
113
|
+
console.error(`FAIL: ${sub.name} expected regulated_grade_pass=${expected}, got ${v.regulated_grade_pass}`);
|
|
114
|
+
} else console.error(`ok: ${sub.name} (${v.regulated_grade_pass ? 'pass' : 'fail'} as ${expected === null ? 'unspecified' : 'expected'})`);
|
|
115
|
+
}
|
|
116
|
+
if (!ok) process.exit(1);
|
|
117
|
+
console.log('task valid: reference passes, all validation mutants are detected');
|
|
118
|
+
} else {
|
|
119
|
+
console.log(`usage:
|
|
120
|
+
tickbench bench --impl ./my-adapter.mjs benchmark YOUR component (from any directory)
|
|
121
|
+
tickbench bench [--task DIR] benchmark a task's reference + all submissions
|
|
122
|
+
tickbench gen [--seed N] [--duration MS] [--symbols N] [--out FILE]
|
|
123
|
+
tickbench validate [--task DIR]\n tickbench new-submission <name> [--task DIR] scaffold a submission for model output
|
|
124
|
+
|
|
125
|
+
bench options: --name LABEL --trace FILE --out DIR --submission NAME --no-reference
|
|
126
|
+
|
|
127
|
+
Results go to ./tickbench-results/: report-<task>.html (open in browser) + .json.`);
|
|
128
|
+
process.exit(cmd ? 1 : 0);
|
|
129
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export interface Tick { t: number; sym: string; col: string; v: number }
|
|
2
|
+
|
|
3
|
+
export interface Trace {
|
|
4
|
+
schema: 'tickbench-trace/1';
|
|
5
|
+
seed: number;
|
|
6
|
+
synthetic: boolean;
|
|
7
|
+
generated: string | null;
|
|
8
|
+
symbols: string[];
|
|
9
|
+
cols: string[];
|
|
10
|
+
durationMs: number;
|
|
11
|
+
ticks: Tick[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TraceOptions {
|
|
15
|
+
seed?: number;
|
|
16
|
+
durationMs?: number;
|
|
17
|
+
symbolCount?: number;
|
|
18
|
+
burst?: { fromMs: number; toMs: number; factor: number };
|
|
19
|
+
xssCanary?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function generateTrace(options?: TraceOptions): Trace;
|
|
23
|
+
export function mulberry32(seed: number): () => number;
|
|
24
|
+
|
|
25
|
+
export interface RuntimeMetrics {
|
|
26
|
+
ticksDispatched: number;
|
|
27
|
+
cellsChecked: number;
|
|
28
|
+
catchUpMs: { p50: number | null; p95: number | null; p99: number | null; n: number };
|
|
29
|
+
staleCellFramePct: number | null;
|
|
30
|
+
frames: number;
|
|
31
|
+
longFramePct: number | null;
|
|
32
|
+
worstFrameMs: number | null;
|
|
33
|
+
finalValueErrors: number;
|
|
34
|
+
finalFormatErrors: number;
|
|
35
|
+
xssTriggered: boolean;
|
|
36
|
+
implError: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function runRuntimeOracle(options: { implPath: string; trace: Trace; quiescenceMs?: number }): Promise<RuntimeMetrics>;
|
|
40
|
+
|
|
41
|
+
export interface A11yViolation { id: string; impact: string | null; nodes: number }
|
|
42
|
+
export function runA11yOracle(options: { implPath: string; trace: Trace; warmupTicks?: number }): Promise<A11yViolation[]>;
|
|
43
|
+
|
|
44
|
+
export interface SecurityFinding { id: string; severity: 'high' | 'medium'; count: number; why: string }
|
|
45
|
+
export interface SecurityRule { id: string; re: RegExp; severity: 'high' | 'medium'; why: string }
|
|
46
|
+
export const RULES: SecurityRule[];
|
|
47
|
+
export function scanSecurity(path: string): SecurityFinding[];
|
|
48
|
+
export function scanSecuritySource(src: string): SecurityFinding[];
|
|
49
|
+
|
|
50
|
+
export interface Thresholds {
|
|
51
|
+
catchUpP95Ms: number;
|
|
52
|
+
staleCellFramePctMax: number;
|
|
53
|
+
longFramePctMax: number;
|
|
54
|
+
finalErrorsMax?: number;
|
|
55
|
+
axeViolationsMax?: number;
|
|
56
|
+
highSecurityFindingsMax?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface Verdict {
|
|
60
|
+
functional_final_state: boolean;
|
|
61
|
+
staleness: boolean;
|
|
62
|
+
frame_stability: boolean;
|
|
63
|
+
security: boolean;
|
|
64
|
+
accessibility: boolean;
|
|
65
|
+
no_runtime_error: boolean;
|
|
66
|
+
regulated_grade_pass: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function computeVerdict(
|
|
70
|
+
input: { runtime: RuntimeMetrics; a11y: A11yViolation[]; security: SecurityFinding[] },
|
|
71
|
+
thresholds: Thresholds
|
|
72
|
+
): Verdict;
|
|
73
|
+
|
|
74
|
+
export interface SubmissionResult { impl?: string; runtime: RuntimeMetrics; a11y: A11yViolation[]; security: SecurityFinding[]; verdict: Verdict }
|
|
75
|
+
export function summaryTable(results: Record<string, SubmissionResult>): string;
|
|
76
|
+
|
|
77
|
+
export function renderHtmlReport(report: { task: string; trace: string; traceSeed: number; generated: string; node: string; results: Record<string, SubmissionResult> }): string;
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { generateTrace } from './trace/generate.mjs';
|
|
2
|
+
export { runRuntimeOracle } from './oracles/runtime.mjs';
|
|
3
|
+
export { runA11yOracle } from './oracles/a11y.mjs';
|
|
4
|
+
export { scanSecurity, scanSecuritySource, RULES } from './oracles/security.mjs';
|
|
5
|
+
export { computeVerdict } from './report/verdict.mjs';
|
|
6
|
+
export { summaryTable } from './report/aggregate.mjs';
|
|
7
|
+
export { renderHtmlReport } from './report/html.mjs';
|