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
@@ -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">&#10003; Final state correct</li><li class="ok">&#10003; Staleness within budget</li><li class="ok">&#10003; Frame stability</li><li class="ok">&#10003; Security</li><li class="ok">&#10003; Accessibility</li><li class="ok">&#10003; No runtime errors</li></ul></div><div class="card pass"><h2>simple-table</h2>
21
+ <div class="badge">PASS</div><ul><li class="ok">&#10003; Final state correct</li><li class="ok">&#10003; Staleness within budget</li><li class="ok">&#10003; Frame stability</li><li class="ok">&#10003; Security</li><li class="ok">&#10003; Accessibility</li><li class="ok">&#10003; 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 &gt;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';