@hackerrank/astra-cli 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -115,7 +115,7 @@ astra \
115
115
  --repeat 3 \
116
116
  --tar # write a bench/ tarball locally
117
117
 
118
- # push results to S3 (tars bench/ then `aws s3 cp` to the URI)
118
+ # push results to S3 (tars bench/ and uploads it + report.html to the URI)
119
119
  astra -m m1,m2 -p tasks/dummy-slugify --repeat 3 \
120
120
  --push s3://hackerrank-astra-bench-results-dev/runs/2025-09-02/
121
121
  ```
@@ -126,6 +126,12 @@ price entry shows **`n/a`** (never `$0`), and estimated costs (native routes) ar
126
126
  marked `~`. `--push` requires an `s3://` URI and the `aws` CLI on PATH; if the
127
127
  upload fails the local tarball is kept.
128
128
 
129
+ Next to the tarball, `--push` also uploads the self-contained `report.html`
130
+ dashboard with `Content-Type: text/html` and prints a **7-day presigned
131
+ "view report" link** at the end of the run. The bucket is private (no public
132
+ read), so that presigned URL is the only way to open the dashboard — e.g.
133
+ clickable straight from a Jenkins console log.
134
+
129
135
  ### HTML report
130
136
 
131
137
  Every matrix run (and every single `-p`/`-t`/`-f` bench run) rebuilds
@@ -175,7 +181,9 @@ credits — that's a quota issue, not a bug.
175
181
  --repeat <n> Attempts per (model,reasoning) in a matrix (default: 1)
176
182
  --bench-root <dir> Root folder for bench runs (default: ./bench)
177
183
  --tar After a matrix, write a bench/ tarball locally
178
- --push <s3-uri> After a matrix, tar bench/ and `aws s3 cp` to the URI
184
+ --push <s3-uri> After a matrix, tar bench/ and `aws s3 cp` it (plus
185
+ report.html) to the URI; prints presigned
186
+ download/view links
179
187
  --report Rebuild bench/summary.json + bench/report.html from
180
188
  whatever runs already exist on disk, then exit
181
189
  -C, --cwd <path> Working directory for commands (default: cwd)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hackerrank/astra-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Minimal zero-dependency AI coding agent for the HackerRank AI Gateway.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/bench.js CHANGED
@@ -455,17 +455,38 @@ function s3ConsoleUrl(s3Uri) {
455
455
  return `https://s3.console.aws.amazon.com/s3/object/${bucket}?prefix=${encodeURIComponent(key)}`;
456
456
  }
457
457
 
458
+ /**
459
+ * Generate a time-limited presigned URL for an S3 object via the AWS CLI
460
+ * (`aws s3 presign`). `expiresIn` defaults to 7 days (604800s). Returns null
461
+ * (rather than throwing) if the AWS CLI is missing or presigning fails, so
462
+ * a presign failure never blocks the run.
463
+ */
464
+ export function presignS3Url(s3Uri, expiresIn = 604800) {
465
+ const res = spawnSync(
466
+ "aws",
467
+ ["s3", "presign", s3Uri, "--expires-in", String(expiresIn)],
468
+ { stdio: ["ignore", "pipe", "pipe"] }
469
+ );
470
+ if (res.error && res.error.code === "ENOENT") return null;
471
+ if (res.status !== 0) return null;
472
+ return (res.stdout?.toString() || "").trim() || null;
473
+ }
474
+
458
475
  /**
459
476
  * Upload a file to S3 via the AWS CLI. Returns { uri, url } where `uri` is the
460
477
  * final s3:// destination and `url` is a browsable https console link.
461
- * Fails gracefully (throws a descriptive error) when the AWS CLI is missing or
462
- * the copy fails; callers should keep the local tarball on failure.
478
+ * Optionally sets the object's Content-Type (e.g. "text/html" so presigned
479
+ * URLs open in a browser instead of downloading). Fails gracefully (throws a
480
+ * descriptive error) when the AWS CLI is missing or the copy fails; callers
481
+ * should keep the local tarball on failure.
463
482
  */
464
- export function pushToS3(file, s3Uri) {
483
+ export function pushToS3(file, s3Uri, { contentType } = {}) {
465
484
  if (!/^s3:\/\//.test(s3Uri)) throw new Error(`--push destination must be an s3:// URI, got: ${s3Uri}`);
466
485
  // If the URI ends with "/", upload into it under the file's basename.
467
486
  const dest = s3Uri.endsWith("/") ? s3Uri + path.basename(file) : s3Uri;
468
- const res = spawnSync("aws", ["s3", "cp", file, dest], {
487
+ const cmd = ["s3", "cp", file, dest];
488
+ if (contentType) cmd.push("--content-type", contentType);
489
+ const res = spawnSync("aws", cmd, {
469
490
  stdio: ["ignore", "pipe", "pipe"],
470
491
  });
471
492
  if (res.error && res.error.code === "ENOENT") {
@@ -474,5 +495,5 @@ export function pushToS3(file, s3Uri) {
474
495
  if (res.status !== 0) {
475
496
  throw new Error(`aws s3 cp failed: ${res.stderr?.toString() || "unknown"}`);
476
497
  }
477
- return { uri: dest, url: s3ConsoleUrl(dest) };
498
+ return { uri: dest, url: s3ConsoleUrl(dest), presigned: presignS3Url(dest) };
478
499
  }
package/src/cli.js CHANGED
@@ -28,8 +28,9 @@
28
28
  * comma-separated to sweep levels in bench mode).
29
29
  * Sent to the model and recorded in the bench name.
30
30
  * --repeat <n> Attempts per (model,reasoning) in a matrix (1)
31
- * --push After a matrix, tar bench/ and upload to
32
- * s3://astra-bench-results/<timestamp>/
31
+ * --push After a matrix, tar bench/ and upload it (plus
32
+ * report.html) to s3://astra-bench-results/<timestamp>/;
33
+ * prints presigned download/view links
33
34
  * --push-uri <s3-uri> Override the S3 push destination
34
35
  * --tar After a matrix, write a bench/ tarball locally
35
36
  * --report Rebuild bench/summary.json + bench/report.html
@@ -365,6 +366,12 @@ async function main() {
365
366
  const metrics = collectMetrics({ agent: benchAgent, model, reasoning });
366
367
  const paths = writeMetrics({ runDir: alloc.dir, root: alloc.root, metrics });
367
368
  log(`[astra] bench done: exit=${res.exit_status} · metrics → ${paths.runMetrics}`);
369
+ try {
370
+ const report = refreshReport(alloc.root);
371
+ log(`[astra] report → ${report.html}`);
372
+ } catch (err) {
373
+ log(`[astra] report generation skipped: ${err.message}`);
374
+ }
368
375
  };
369
376
  await runRepl(agent, { model, autoRun: !!args.yolo, fresh: !resumeDoc, reasoning, runBench });
370
377
  process.exit(0);
@@ -465,6 +472,30 @@ function archiveAndPush(args, root, links) {
465
472
  const pushed = pushToS3(tarball, dest);
466
473
  links.push(["s3 uri", pushed.uri]);
467
474
  links.push(["s3 url", pushed.url]);
475
+ if (pushed.presigned) {
476
+ links.push(["download (presigned, 7d)", pushed.presigned]);
477
+ }
478
+ // Also push the self-contained HTML dashboard next to the tarball and
479
+ // emit a presigned view link: the bucket is private (no public read),
480
+ // so the presigned URL is the only way to open the report — e.g. by
481
+ // clicking it straight from the Jenkins console output. Uploaded with
482
+ // Content-Type: text/html so browsers render it instead of downloading.
483
+ const reportHtml = path.join(benchRoot(root), "report.html");
484
+ if (fs.existsSync(reportHtml)) {
485
+ const htmlDest = dest.endsWith("/")
486
+ ? dest + "report.html"
487
+ : dest.slice(0, dest.lastIndexOf("/") + 1) + "report.html";
488
+ try {
489
+ const html = pushToS3(reportHtml, htmlDest, { contentType: "text/html" });
490
+ if (html.presigned) {
491
+ links.push(["view report (presigned, 7d)", html.presigned]);
492
+ } else {
493
+ links.push(["report (s3 url)", html.url]);
494
+ }
495
+ } catch (err) {
496
+ console.error(`\x1b[31m[astra] S3 report push failed: ${err.message}\x1b[0m`);
497
+ }
498
+ }
468
499
  } catch (err) {
469
500
  console.error(`\x1b[31m[astra] S3 push failed: ${err.message}\x1b[0m`);
470
501
  console.error(`\x1b[33m[astra] tarball kept locally: ${tarball}\x1b[0m`);
package/src/report.html CHANGED
@@ -4,41 +4,15 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>astra bench report</title>
7
+ <link rel="stylesheet" href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&display=swap" />
7
8
  <style>
8
- :root {
9
- --bg: #0f1115;
10
- --panel: #171a21;
11
- --panel-2: #1d212b;
12
- --border: #262b36;
13
- --text: #e6e8ee;
14
- --muted: #8b93a7;
15
- --accent: #5b9dff;
16
- --good: #3ecf8e;
17
- --bad: #ef5a6f;
18
- --warn: #e8b339;
19
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
20
- }
21
- * { box-sizing: border-box; }
22
- body { margin: 0; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.45; }
23
- a { color: var(--accent); }
24
- .wrap { max-width: 1180px; margin: 0 auto; padding: 28px 20px 64px; }
25
- header.top { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; flex-wrap: wrap; gap: 8px; }
26
- header.top h1 { font-size: 20px; margin: 0; font-weight: 650; letter-spacing: .2px; }
27
- header.top .meta { color: var(--muted); font-size: 12.5px; }
28
- .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin: 20px 0 28px; }
29
- .kpi { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; }
30
- .kpi .v { font-size: 22px; font-weight: 700; }
31
- .kpi .l { color: var(--muted); font-size: 11.5px; text-transform: uppercase; letter-spacing: .06em; margin-top: 2px; }
32
- section { margin: 34px 0; }
33
- section > h2 { font-size: 15px; margin: 0 0 12px; font-weight: 650; color: var(--text); display: flex; align-items: center; gap: 8px; }
34
- section > h2 .sub { color: var(--muted); font-weight: 400; font-size: 12px; }
35
- .grid2 { display: grid; grid-template-columns: 1.1fr 1fr; gap: 16px; }
36
- .grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
37
- @media (max-width: 900px) { .grid2, .grid3 { grid-template-columns: 1fr; } }
38
- .panel { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; }
39
- .panel h3 { margin: 0 0 10px; font-size: 12.5px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .05em; }
9
+ :root { --ink:#11131a; --muted:#69707d; --wash:#f5f6f8; --line:#e4e7eb; --lime:#a7ff63; --blue:#5285f7; --purple:#9160d8; --green:#39ae76; --orange:#f3a021; --red:#d85668; font-family:Satoshi,"Avenir Next",Arial,sans-serif; }
10
+ * { box-sizing:border-box; } body { margin:0; background:#fff; color:var(--ink); font-size:14px; line-height:1.45; } .wrap { max-width:1240px; margin:auto; padding:28px 32px 68px; }
11
+ header.top { display:flex; justify-content:space-between; gap:20px; align-items:flex-start; padding-bottom:28px; border-bottom:1px solid var(--line); } header.top h1 { font-size:32px; letter-spacing:-.045em; line-height:1; margin:8px 0 0; font-weight:700; } .eyebrow { display:inline-block; padding:5px 14px; border-radius:99px; background:var(--lime); font-size:12px; font-weight:700; } header.top .meta { color:var(--muted); font-size:12px; text-align:right; }
12
+ .kpis { display:grid; grid-template-columns:repeat(4,1fr); margin:24px 0 52px; } .kpi { min-height:96px; padding:8px 18px; border-left:1px solid var(--line); } .kpi:first-child { border:0; padding-left:0; } .kpi .v { font-size:28px; letter-spacing:-.045em; font-weight:700; } .kpi .l { color:var(--muted); font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; margin-top:8px; }
13
+ section { margin:56px 0; } section > h2 { font-size:20px; letter-spacing:-.035em; margin:0 0 22px; } section > h2 .sub { color:var(--muted); font-size:13px; font-weight:400; letter-spacing:0; } .grid2 { display:grid; grid-template-columns:1.1fr 1fr; gap:44px; } .panel { padding:0; } .panel h3 { color:var(--muted); letter-spacing:.07em; text-transform:uppercase; font-size:12px; margin:0 0 16px; }
40
14
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
41
- th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--border); white-space: nowrap; }
15
+ th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
42
16
  th { color: var(--muted); font-weight: 600; font-size: 11.5px; text-transform: uppercase; letter-spacing: .03em; cursor: pointer; user-select: none; }
43
17
  th:hover { color: var(--text); }
44
18
  th .arrow { opacity: .5; font-size: 10px; margin-left: 3px; }
@@ -52,7 +26,7 @@
52
26
  .muted { color: var(--muted); }
53
27
  .bar-row { display: flex; align-items: center; gap: 8px; margin: 6px 0; }
54
28
  .bar-row .lbl { width: 150px; flex: 0 0 150px; font-size: 12px; overflow: hidden; text-overflow: ellipsis; }
55
- .bar-track { flex: 1; height: 14px; background: var(--panel-2); border-radius: 6px; overflow: hidden; display: flex; }
29
+ .bar-track { flex: 1; height: 9px; background: var(--wash); border-radius: 99px; overflow: hidden; display: flex; }
56
30
  .bar-seg { height: 100%; }
57
31
  .bar-val { width: 64px; flex: 0 0 64px; text-align: right; font-size: 12px; font-variant-numeric: tabular-nums; }
58
32
  .legend { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 10px; font-size: 11.5px; color: var(--muted); }
@@ -61,7 +35,7 @@
61
35
  .heat-cell { display: inline-block; min-width: 44px; padding: 3px 6px; border-radius: 5px; font-size: 12px; }
62
36
  details.run-detail { margin: 0; }
63
37
  details.run-detail > summary { display: none; }
64
- .timeline { margin: 0 0 18px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
38
+ .timeline { margin: 0 0 18px; background:var(--wash); padding:0 14px; overflow: hidden; }
65
39
  .timeline .step { padding: 10px 14px; border-bottom: 1px solid var(--border); }
66
40
  .timeline .step:last-child { border-bottom: none; }
67
41
  .timeline .step .head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; margin-bottom: 4px; }
@@ -71,35 +45,30 @@
71
45
  .timeline .step pre { background: var(--panel-2); border-radius: 6px; padding: 8px 10px; margin: 4px 0; font-size: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-word; }
72
46
  .rc0 { color: var(--good); } .rcN { color: var(--bad); }
73
47
  .toolbar { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
74
- .toolbar select, .toolbar input { background: var(--panel-2); border: 1px solid var(--border); color: var(--text); border-radius: 6px; padding: 5px 9px; font-size: 12.5px; }
48
+ .toolbar select, .toolbar input { background: var(--wash); border:0; color:var(--ink); border-radius:4px; padding:7px 9px; font-size:12px; } .model-filter { align-items:end; } .model-filter > span { color:var(--muted); font-size:11px; } .model-menu { position:relative; } .model-menu summary { cursor:pointer; list-style:none; min-width:220px; padding:9px 32px 9px 11px; background:var(--wash); border-radius:5px; font-size:12px; position:relative; } .model-menu summary::-webkit-details-marker { display:none; } .model-menu summary::after { content:"⌄"; position:absolute; right:11px; color:var(--muted); } .model-options { position:absolute; z-index:2; top:calc(100% + 6px); left:0; width:260px; max-height:240px; overflow:auto; padding:6px; background:#fff; border:1px solid var(--line); box-shadow:0 10px 24px rgba(17,19,26,.1); } .model-option { display:flex; gap:9px; align-items:center; padding:8px; font-size:12px; cursor:pointer; } .model-option:hover { background:var(--wash); } .model-option input { accent-color:var(--blue); }
75
49
  .toolbar label { color: var(--muted); font-size: 12px; }
76
50
  footer { color: var(--muted); font-size: 11.5px; margin-top: 40px; text-align: center; }
77
51
  svg text { fill: var(--muted); font-size: 10.5px; }
78
- .axis line, .axis path { stroke: var(--border); }
52
+ .axis line, .axis path { stroke: var(--line); }
79
53
  .n-a { color: var(--muted); }
80
54
  </style>
81
55
  </head>
82
56
  <body>
83
57
  <div class="wrap">
84
58
  <header class="top">
85
- <h1>astra bench report</h1>
86
- <div class="meta" id="generated"></div>
59
+ <div><h1>Benchmark report</h1><div class="meta" id="generated"></div></div>
87
60
  </header>
88
61
 
89
62
  <div class="kpis" id="kpis"></div>
90
63
 
91
64
  <section>
92
- <h2>Leaderboard <span class="sub">one row per model · reasoning</span></h2>
93
- <div class="grid2">
94
- <div class="panel">
95
- <h3>Solve rate</h3>
96
- <div id="chart-solve"></div>
97
- </div>
98
- <div class="panel">
99
- <h3>Outcomes</h3>
100
- <div id="chart-outcomes"></div>
101
- <div class="legend" id="legend-outcomes"></div>
102
- </div>
65
+ <h2>Performance <span class="sub">run volume and reliability by model</span></h2>
66
+ <div class="toolbar model-filter">
67
+ <span>Models</span>
68
+ <details class="model-menu">
69
+ <summary id="model-filter-label">All models</summary>
70
+ <div id="leaderboard-model-options" class="model-options"></div>
71
+ </details>
103
72
  </div>
104
73
  <div class="panel" style="margin-top:16px;">
105
74
  <table id="tbl-leaderboard"></table>
@@ -107,22 +76,29 @@
107
76
  </section>
108
77
 
109
78
  <section>
110
- <h2>Cost &amp; tokens</h2>
79
+ <h2>Efficiency <span class="sub">spend and token composition</span></h2>
111
80
  <div class="grid2">
112
81
  <div class="panel">
113
- <h3>Cost vs. solve rate</h3>
82
+ <h3>Cost vs. average duration</h3>
114
83
  <div id="chart-scatter"></div>
115
84
  </div>
116
85
  <div class="panel">
117
- <h3>Token mix (prompt / completion / reasoning / cached)</h3>
118
- <div id="chart-tokens"></div>
119
- <div class="legend" id="legend-tokens"></div>
86
+ <h3>Cost vs. average steps</h3>
87
+ <div id="chart-scatter-steps"></div>
120
88
  </div>
121
89
  </div>
122
90
  </section>
123
91
 
92
+ <section>
93
+ <h2>Token mix <span class="sub">prompt, completion, reasoning, and cached tokens</span></h2>
94
+ <div class="panel">
95
+ <div id="chart-tokens"></div>
96
+ <div class="legend" id="legend-tokens"></div>
97
+ </div>
98
+ </section>
99
+
124
100
  <section id="section-series">
125
- <h2>Per-step trend <span class="sub">median tokens &amp; cumulative cost across the trajectory</span></h2>
101
+ <h2>Trajectory <span class="sub">where context and cost accumulate</span></h2>
126
102
  <div class="grid2">
127
103
  <div class="panel">
128
104
  <h3>Prompt tokens (context) vs. step</h3>
@@ -136,31 +112,6 @@
136
112
  <div class="legend" id="legend-series"></div>
137
113
  </section>
138
114
 
139
- <section id="section-matrix">
140
- <h2>Model &times; task <span class="sub">pass@k</span></h2>
141
- <div class="panel">
142
- <table id="tbl-matrix" class="heat-table"></table>
143
- </div>
144
- </section>
145
-
146
- <section>
147
- <h2>Runs <span class="sub">every attempt · click a row to expand the command timeline</span></h2>
148
- <div class="toolbar">
149
- <label>Model
150
- <select id="filter-model"><option value="">all</option></select>
151
- </label>
152
- <label>Task
153
- <select id="filter-task"><option value="">all</option></select>
154
- </label>
155
- <label>Outcome
156
- <select id="filter-outcome"><option value="">all</option></select>
157
- </label>
158
- </div>
159
- <div class="panel">
160
- <table id="tbl-runs"></table>
161
- </div>
162
- </section>
163
-
164
115
  <footer>generated by <span class="mono">astra bench --report</span> · schema <span id="schema"></span></footer>
165
116
  </div>
166
117
 
@@ -169,8 +120,19 @@
169
120
  </script>
170
121
  <script>
171
122
  (function () {
172
- var DATA = window.DATA || { kpis: {}, leaderboard: [], matrix: [], runs: [], step_series: [] };
173
- var PALETTE = ["#5b9dff", "#3ecf8e", "#e8b339", "#ef5a6f", "#b57bff", "#33c2c2", "#ff8a5b", "#a3d977"];
123
+ function loadReportData() {
124
+ try {
125
+ var request = new XMLHttpRequest();
126
+ request.open("GET", "summary.json", false);
127
+ request.send(null);
128
+ if ((request.status >= 200 && request.status < 300) || request.status === 0) return JSON.parse(request.responseText);
129
+ } catch (_) {
130
+ // Direct file access can block JSON reads; use the embedded snapshot then.
131
+ }
132
+ return window.__ASTRA_EMBEDDED_DATA__ || { kpis: {}, leaderboard: [], matrix: [], runs: [], step_series: [] };
133
+ }
134
+ var DATA = loadReportData();
135
+ var PALETTE = ["#5285f7", "#9160d8", "#39ae76", "#f3a021", "#d85668", "#37a8ad"];
174
136
  var colorCache = {};
175
137
  function colorFor(key) {
176
138
  if (!colorCache[key]) {
@@ -209,21 +171,19 @@
209
171
  }
210
172
 
211
173
  // ---------------------------------------------------------------- header
212
- document.getElementById("generated").textContent = "generated " + (DATA.generated_at || "");
174
+ document.getElementById("generated").textContent = DATA.generated_at
175
+ ? "Generated " + new Date(DATA.generated_at).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" })
176
+ : "";
213
177
  document.getElementById("schema").textContent = DATA.schema || "";
214
178
 
215
179
  // ---------------------------------------------------------------- KPIs
216
180
  (function renderKpis() {
217
181
  var k = DATA.kpis || {};
218
182
  var cards = [
219
- ["Runs", fmt(k.runs)],
220
- ["Models", fmt(k.models)],
221
- ["Tasks", fmt(k.tasks)],
222
- ["Solved", fmt(k.solved) + " / " + fmt(k.runs)],
223
- ["Solve rate", pct(k.solved_rate)],
224
- ["Total tokens", fmt(k.tokens)],
183
+ ["Models Benchmarked", fmt(k.models)],
184
+ ["Total duration", fmt(k.elapsed_seconds) + "s"],
225
185
  ["Total cost", fmtUsd(k.cost_usd) + (k.cost_source === "estimated" ? "~" : "")],
226
- ["Elapsed", fmt(k.elapsed_seconds) + "s"],
186
+ ["Tokens used", fmt(k.tokens)],
227
187
  ];
228
188
  var host = document.getElementById("kpis");
229
189
  cards.forEach(function (c) {
@@ -266,20 +226,22 @@
266
226
  draw();
267
227
  }
268
228
 
269
- // ---------------------------------------------------------------- leaderboard chart: solve rate bars
229
+ // ---------------------------------------------------------------- leaderboard chart: run volume bars
270
230
  (function () {
271
231
  var host = document.getElementById("chart-solve");
232
+ if (!host) return;
233
+ var maxRuns = Math.max(1, Math.max.apply(null, (DATA.leaderboard || []).map(function (g) { return g.runs || 0; })));
272
234
  (DATA.leaderboard || []).forEach(function (g) {
273
235
  var row = el("div", { class: "bar-row" });
274
236
  row.appendChild(el("div", { class: "lbl mono" }, esc(g.slug)));
275
237
  var track = el("div", { class: "bar-track" });
276
238
  var seg = el("div", {
277
239
  class: "bar-seg",
278
- style: "width:" + Math.round((g.solved_rate || 0) * 100) + "%;background:" + colorFor(g.slug),
240
+ style: "width:" + Math.round((g.runs || 0) / maxRuns * 100) + "%;background:" + colorFor(g.slug),
279
241
  });
280
242
  track.appendChild(seg);
281
243
  row.appendChild(track);
282
- row.appendChild(el("div", { class: "bar-val" }, pct(g.solved_rate) + " (" + g.solved + "/" + g.runs + ")"));
244
+ row.appendChild(el("div", { class: "bar-val" }, fmt(g.runs) + " runs"));
283
245
  host.appendChild(row);
284
246
  });
285
247
  })();
@@ -288,6 +250,7 @@
288
250
  (function () {
289
251
  var host = document.getElementById("chart-outcomes");
290
252
  var legend = document.getElementById("legend-outcomes");
253
+ if (!host || !legend) return;
291
254
  var outcomeColors = { Submitted: "#3ecf8e", LimitsExceeded: "#e8b339", TimeExceeded: "#e8b339", RepeatedFormatError: "#ef5a6f", ContextWindowExceeded: "#ef5a6f", Error: "#ef5a6f", Unknown: "#8b93a7" };
292
255
  function colorForOutcome(k) { return outcomeColors[k] || "#8b93a7"; }
293
256
  var allOutcomes = {};
@@ -313,18 +276,31 @@
313
276
  // ---------------------------------------------------------------- leaderboard table
314
277
  (function () {
315
278
  var container = document.getElementById("tbl-leaderboard");
279
+ var options = document.getElementById("leaderboard-model-options");
280
+ var filterLabel = document.getElementById("model-filter-label");
316
281
  var columns = [
317
282
  { key: "model", label: "Model", render: function (r) { return esc(r.model); } },
318
283
  { key: "reasoning", label: "Reasoning", render: function (r) { return esc(r.reasoning); } },
319
284
  { key: "runs", label: "Runs", render: function (r) { return fmt(r.runs); } },
320
- { key: "solved_rate", label: "Solved", render: function (r) { return r.solved + "/" + r.runs + " (" + pct(r.solved_rate) + ")"; } },
321
- { key: "avg_steps", label: "Avg steps", render: function (r) { return fmt1(r.avg_steps); } },
322
- { key: "avg_elapsed_seconds", label: "Avg time", render: function (r) { return fmt1(r.avg_elapsed_seconds) + "s"; } },
323
- { key: "avg_tokens", label: "Avg tokens", render: function (r) { return fmt(Math.round(r.avg_tokens)); } },
285
+ { key: "sum_steps", label: "Total steps", render: function (r) { return fmt(r.sum_steps); } },
286
+ { key: "sum_elapsed_seconds", label: "Time taken", render: function (r) { return fmt(r.sum_elapsed_seconds) + "s"; } },
287
+ { key: "sum_tokens", label: "Tokens", render: function (r) { return fmt(r.sum_tokens); } },
324
288
  { key: "cost_usd", label: "Cost", render: function (r) { return fmtUsd(r.cost_usd) + (r.cost_source === "estimated" ? "~" : ""); } },
325
- { key: "avg_n_failed_commands", label: "Fail/run", render: function (r) { return fmt1(r.avg_n_failed_commands); } },
326
289
  ];
327
- sortableTable(container, columns, DATA.leaderboard || [], { defaultKey: "solved_rate" });
290
+ (DATA.leaderboard || []).forEach(function (r) {
291
+ var option = el("label", { class: "model-option" });
292
+ option.appendChild(el("input", { type: "checkbox", value: r.slug }));
293
+ option.appendChild(document.createTextNode(r.model + " · " + r.reasoning));
294
+ options.appendChild(option);
295
+ });
296
+ function draw() {
297
+ var selected = Array.prototype.slice.call(options.querySelectorAll("input:checked")).map(function (o) { return o.value; });
298
+ var rows = selected.length ? (DATA.leaderboard || []).filter(function (r) { return selected.indexOf(r.slug) !== -1; }) : DATA.leaderboard || [];
299
+ filterLabel.textContent = selected.length ? selected.length + " model" + (selected.length === 1 ? "" : "s") + " selected" : "All models";
300
+ sortableTable(container, columns, rows, { defaultKey: "sum_tokens" });
301
+ }
302
+ options.addEventListener("change", draw);
303
+ draw();
328
304
  })();
329
305
 
330
306
  // ---------------------------------------------------------------- token mix stacked bar
@@ -354,29 +330,30 @@
354
330
  });
355
331
  })();
356
332
 
357
- // ---------------------------------------------------------------- scatter: cost vs solve rate
333
+ // ---------------------------------------------------------------- scatter: cost vs duration
358
334
  (function () {
359
335
  var host = document.getElementById("chart-scatter");
360
336
  var W = host.clientWidth || 460, H = 260, pad = { l: 42, r: 16, t: 14, b: 30 };
361
337
  var svg = svgEl("svg", { width: "100%", height: H, viewBox: "0 0 " + W + " " + H });
362
338
  var data = (DATA.leaderboard || []).filter(function (g) { return g.cost_usd != null; });
363
339
  var maxCost = Math.max(0.001, Math.max.apply(null, data.map(function (g) { return g.cost_usd; }).concat([0.001])));
340
+ var maxDuration = Math.max(1, Math.max.apply(null, data.map(function (g) { return g.avg_elapsed_seconds || 0; }).concat([1])));
364
341
  function x(v) { return pad.l + (v / maxCost) * (W - pad.l - pad.r); }
365
- function y(v) { return H - pad.b - v * (H - pad.t - pad.b); }
342
+ function y(v) { return H - pad.b - (v / maxDuration) * (H - pad.t - pad.b); }
366
343
  // axes
367
344
  svg.appendChild(svgEl("line", { x1: pad.l, x2: W - pad.r, y1: H - pad.b, y2: H - pad.b, stroke: "#262b36" }));
368
345
  svg.appendChild(svgEl("line", { x1: pad.l, x2: pad.l, y1: pad.t, y2: H - pad.b, stroke: "#262b36" }));
369
346
  [0, 0.25, 0.5, 0.75, 1].forEach(function (t) {
370
- var ty = y(t);
347
+ var ty = y(maxDuration * t);
371
348
  var lab = svgEl("text", { x: pad.l - 8, y: ty + 3, "text-anchor": "end" });
372
- lab.textContent = Math.round(t * 100) + "%";
349
+ lab.textContent = Math.round(maxDuration * t) + "s";
373
350
  svg.appendChild(lab);
374
351
  });
375
352
  var lab2 = svgEl("text", { x: W - pad.r, y: H - pad.b + 20, "text-anchor": "end" });
376
353
  lab2.textContent = "cost \u2192 (max " + fmtUsd(maxCost) + ")";
377
354
  svg.appendChild(lab2);
378
355
  data.forEach(function (g) {
379
- var cx = x(g.cost_usd), cy = y(g.solved_rate);
356
+ var cx = x(g.cost_usd), cy = y(g.avg_elapsed_seconds || 0);
380
357
  var r = 6 + Math.min(10, (g.avg_tokens || 0) / 4000);
381
358
  var c = svgEl("circle", { cx: cx, cy: cy, r: r, fill: colorFor(g.slug), "fill-opacity": 0.85 });
382
359
  svg.appendChild(c);
@@ -387,6 +364,38 @@
387
364
  host.appendChild(svg);
388
365
  })();
389
366
 
367
+ // ---------------------------------------------------------------- scatter: cost vs average steps
368
+ (function () {
369
+ var host = document.getElementById("chart-scatter-steps");
370
+ var W = host.clientWidth || 460, H = 260, pad = { l: 42, r: 16, t: 14, b: 30 };
371
+ var svg = svgEl("svg", { width: "100%", height: H, viewBox: "0 0 " + W + " " + H });
372
+ var data = (DATA.leaderboard || []).filter(function (g) { return g.cost_usd != null; });
373
+ var maxCost = Math.max(0.001, Math.max.apply(null, data.map(function (g) { return g.cost_usd; }).concat([0.001])));
374
+ var maxSteps = Math.max(1, Math.max.apply(null, data.map(function (g) { return g.avg_steps || 0; }).concat([1])));
375
+ function x(v) { return pad.l + (v / maxCost) * (W - pad.l - pad.r); }
376
+ function y(v) { return H - pad.b - (v / maxSteps) * (H - pad.t - pad.b); }
377
+ svg.appendChild(svgEl("line", { x1: pad.l, x2: W - pad.r, y1: H - pad.b, y2: H - pad.b, stroke: "#e4e7eb" }));
378
+ svg.appendChild(svgEl("line", { x1: pad.l, x2: pad.l, y1: pad.t, y2: H - pad.b, stroke: "#e4e7eb" }));
379
+ [0, 0.25, 0.5, 0.75, 1].forEach(function (t) {
380
+ var ty = y(maxSteps * t);
381
+ var lab = svgEl("text", { x: pad.l - 8, y: ty + 3, "text-anchor": "end" });
382
+ lab.textContent = fmt1(maxSteps * t);
383
+ svg.appendChild(lab);
384
+ });
385
+ var lab2 = svgEl("text", { x: W - pad.r, y: H - pad.b + 20, "text-anchor": "end" });
386
+ lab2.textContent = "cost \u2192 (max " + fmtUsd(maxCost) + ")";
387
+ svg.appendChild(lab2);
388
+ data.forEach(function (g) {
389
+ var cx = x(g.cost_usd), cy = y(g.avg_steps || 0);
390
+ var r = 6 + Math.min(10, (g.avg_tokens || 0) / 4000);
391
+ svg.appendChild(svgEl("circle", { cx: cx, cy: cy, r: r, fill: colorFor(g.slug), "fill-opacity": 0.85 }));
392
+ var label = svgEl("text", { x: cx + r + 4, y: cy + 3 });
393
+ label.textContent = g.slug;
394
+ svg.appendChild(label);
395
+ });
396
+ host.appendChild(svg);
397
+ })();
398
+
390
399
  // ---------------------------------------------------------------- per-step line charts
391
400
  function lineChart(host, series, valueFn, opts) {
392
401
  opts = opts || {};
@@ -444,6 +453,7 @@
444
453
  (function () {
445
454
  var matrix = DATA.matrix || [];
446
455
  var container = document.getElementById("tbl-matrix");
456
+ if (!container) return;
447
457
  if (matrix.length === 0) {
448
458
  document.getElementById("section-matrix").style.display = "none";
449
459
  return;
@@ -481,6 +491,7 @@
481
491
  var fModel = document.getElementById("filter-model");
482
492
  var fTask = document.getElementById("filter-task");
483
493
  var fOutcome = document.getElementById("filter-outcome");
494
+ if (!container || !fModel || !fTask || !fOutcome) return;
484
495
 
485
496
  function uniq(fn) {
486
497
  var seen = {}, out = [];
package/src/report.js CHANGED
@@ -101,7 +101,9 @@ export function buildSummary(runs) {
101
101
  solved_rate: rs.length ? solved / rs.length : 0,
102
102
  outcomes,
103
103
  avg_steps: avg(rs, (r) => r.steps),
104
+ sum_steps: sum(rs, (r) => r.steps),
104
105
  avg_elapsed_seconds: avg(rs, (r) => r.elapsed_seconds),
106
+ sum_elapsed_seconds: sum(rs, (r) => r.elapsed_seconds),
105
107
  avg_tokens: avg(rs, (r) => r.tokens.total),
106
108
  sum_tokens: sum(rs, (r) => r.tokens.total),
107
109
  tokens: {
@@ -221,7 +223,7 @@ export function renderReportHtml(summary) {
221
223
  if (!tpl.includes("/*__ASTRA_DATA__*/")) {
222
224
  throw new Error("src/report.html is missing the /*__ASTRA_DATA__*/ injection marker");
223
225
  }
224
- return tpl.replace("/*__ASTRA_DATA__*/", `window.DATA = ${data};`);
226
+ return tpl.replace("/*__ASTRA_DATA__*/", `window.__ASTRA_EMBEDDED_DATA__ = ${data};`);
225
227
  }
226
228
 
227
229
  function toRunDoc(run) {