loki-mode 7.8.2 → 7.9.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/proof-generator.py +690 -0
- package/autonomy/lib/proof-template.html +803 -0
- package/autonomy/lib/proof_redact.py +297 -0
- package/autonomy/loki +313 -2
- package/autonomy/run.sh +36 -0
- package/bin/loki +11 -3
- package/completions/_loki +9 -0
- package/completions/loki.bash +12 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +90 -0
- package/dashboard/static/proofs.html +119 -0
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +233 -170
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<!--
|
|
3
|
+
Loki Mode - Shareable Proof-of-Run template (Slice C).
|
|
4
|
+
|
|
5
|
+
SELF-CONTAINED: zero external network calls. All CSS is inlined below, there
|
|
6
|
+
are NO <script src>, NO CDN links, NO web fonts, NO @import, NO external
|
|
7
|
+
images. The page renders entirely from data embedded at generation time.
|
|
8
|
+
|
|
9
|
+
GENERATOR CONTRACT (read by autonomy/lib/proof-generator.py, Slice A):
|
|
10
|
+
1. The generator takes the REDACTED proof dict (after proof_redact.py has
|
|
11
|
+
run) and serializes it to JSON.
|
|
12
|
+
2. Before substitution it MUST escape the "<" character so a value that
|
|
13
|
+
happens to contain "</script>" or "<!--" cannot break out of the
|
|
14
|
+
embedded <script type="application/json"> block. Recommended:
|
|
15
|
+
payload = json.dumps(proof_dict, ensure_ascii=False).replace("<", "\\u003c")
|
|
16
|
+
3. The generator replaces the proof token (two underscores, the word
|
|
17
|
+
PROOF, JSON, two underscores) with that escaped JSON string. To keep
|
|
18
|
+
the substitution unambiguous the literal token appears EXACTLY ONCE in
|
|
19
|
+
this file: inside the <script id="proof-data"> block below. Do NOT write
|
|
20
|
+
the literal token anywhere else (this comment deliberately spells it out
|
|
21
|
+
instead of repeating it, and the renderer detects an un-substituted
|
|
22
|
+
blob structurally rather than by string-matching the token).
|
|
23
|
+
4. No other token substitution is required. All rendering happens client
|
|
24
|
+
side from the parsed blob. This keeps Bun and bash routes at exact
|
|
25
|
+
parity (one string replace, one template).
|
|
26
|
+
|
|
27
|
+
The renderer below is defensive: every nullable / optional schema field is
|
|
28
|
+
guarded (deployed_url, public_url, diffs, council.enabled false, empty
|
|
29
|
+
reviewers[], findings_link null). It never prints "undefined" and never
|
|
30
|
+
throws on a null.
|
|
31
|
+
|
|
32
|
+
proof.json schema fields rendered (FROZEN v1.0): schema_version, run_id,
|
|
33
|
+
generated_at, loki_version, started_at, wall_clock_sec, spec{source,brief},
|
|
34
|
+
provider{name,model}, iterations{count,succeeded,failed},
|
|
35
|
+
files_changed{count,insertions,deletions,files[]}, diffs[]|null,
|
|
36
|
+
council{enabled,final_verdict,threshold,reviewers[],findings_link},
|
|
37
|
+
quality_gates{passed,total,gates[]},
|
|
38
|
+
cost{usd,input_tokens,output_tokens,cache_read_tokens,cache_creation_tokens},
|
|
39
|
+
deployment{deployed_url,public_url}, redaction{...}, verification{hash,...}.
|
|
40
|
+
-->
|
|
41
|
+
<html lang="en">
|
|
42
|
+
<head>
|
|
43
|
+
<meta charset="utf-8">
|
|
44
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
45
|
+
<title>Loki Mode - Proof of Run</title>
|
|
46
|
+
<!-- Static text-only social meta. No og:image (would break self-containment). -->
|
|
47
|
+
<meta property="og:type" content="website">
|
|
48
|
+
<meta property="og:title" content="Built and verified autonomously by Loki Mode">
|
|
49
|
+
<!-- og:description and twitter:description are filled at generation time with
|
|
50
|
+
the real measured cost + files-changed + council ratio (cost-free variant
|
|
51
|
+
when cost was not collected). The generator replaces the og token below in
|
|
52
|
+
BOTH metas with one str.replace; the token is attribute-escaped. -->
|
|
53
|
+
<meta property="og:description" content="__PROOF_OG_DESCRIPTION__">
|
|
54
|
+
<meta name="twitter:card" content="summary">
|
|
55
|
+
<meta name="twitter:description" content="__PROOF_OG_DESCRIPTION__">
|
|
56
|
+
<style>
|
|
57
|
+
:root {
|
|
58
|
+
--bg: #0f1115;
|
|
59
|
+
--panel: #171a21;
|
|
60
|
+
--panel-2: #1d2129;
|
|
61
|
+
--border: #2a2f3a;
|
|
62
|
+
--text: #e7e9ee;
|
|
63
|
+
--muted: #9aa1ad;
|
|
64
|
+
--faint: #6b7280;
|
|
65
|
+
--accent: #6f7bf7;
|
|
66
|
+
--accent-soft: #2a2f57;
|
|
67
|
+
--green: #34d399;
|
|
68
|
+
--amber: #fbbf24;
|
|
69
|
+
--red: #f87171;
|
|
70
|
+
--mono: ui-monospace, "SF Mono", "Menlo", "Consolas", "Liberation Mono", monospace;
|
|
71
|
+
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
72
|
+
}
|
|
73
|
+
* { box-sizing: border-box; }
|
|
74
|
+
html, body { margin: 0; padding: 0; }
|
|
75
|
+
body {
|
|
76
|
+
background: var(--bg);
|
|
77
|
+
color: var(--text);
|
|
78
|
+
font-family: var(--sans);
|
|
79
|
+
line-height: 1.5;
|
|
80
|
+
-webkit-font-smoothing: antialiased;
|
|
81
|
+
}
|
|
82
|
+
a { color: var(--accent); text-decoration: none; }
|
|
83
|
+
a:hover { text-decoration: underline; }
|
|
84
|
+
.wrap { max-width: 880px; margin: 0 auto; padding: 40px 20px 80px; }
|
|
85
|
+
.topbar {
|
|
86
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
87
|
+
margin-bottom: 28px; font-size: 13px; color: var(--muted);
|
|
88
|
+
}
|
|
89
|
+
.brand { font-weight: 600; color: var(--text); letter-spacing: 0.2px; }
|
|
90
|
+
.brand .dot { color: var(--accent); }
|
|
91
|
+
|
|
92
|
+
/* HERO */
|
|
93
|
+
.hero {
|
|
94
|
+
background: linear-gradient(180deg, var(--panel-2), var(--panel));
|
|
95
|
+
border: 1px solid var(--border);
|
|
96
|
+
border-radius: 14px;
|
|
97
|
+
padding: 28px;
|
|
98
|
+
margin-bottom: 22px;
|
|
99
|
+
}
|
|
100
|
+
.hero h1 {
|
|
101
|
+
font-size: 26px; line-height: 1.25; margin: 0 0 14px;
|
|
102
|
+
font-weight: 650; letter-spacing: -0.2px;
|
|
103
|
+
}
|
|
104
|
+
.hero .sub { color: var(--muted); font-size: 14px; margin: 0; }
|
|
105
|
+
.hero-actions { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 10px; }
|
|
106
|
+
|
|
107
|
+
/* Buttons */
|
|
108
|
+
.btn {
|
|
109
|
+
display: inline-block; padding: 9px 15px; border-radius: 9px;
|
|
110
|
+
font-size: 14px; font-weight: 550; border: 1px solid var(--border);
|
|
111
|
+
background: var(--panel-2); color: var(--text); cursor: pointer;
|
|
112
|
+
}
|
|
113
|
+
.btn.primary { background: var(--accent); border-color: var(--accent); color: #0b0d12; }
|
|
114
|
+
.btn.primary:hover { text-decoration: none; filter: brightness(1.06); }
|
|
115
|
+
.btn:hover { text-decoration: none; border-color: var(--accent); }
|
|
116
|
+
|
|
117
|
+
/* Sections */
|
|
118
|
+
.section { margin-top: 26px; }
|
|
119
|
+
.section > h2 {
|
|
120
|
+
font-size: 12px; text-transform: uppercase; letter-spacing: 1.4px;
|
|
121
|
+
color: var(--faint); margin: 0 0 12px; font-weight: 600;
|
|
122
|
+
}
|
|
123
|
+
.card {
|
|
124
|
+
background: var(--panel); border: 1px solid var(--border);
|
|
125
|
+
border-radius: 12px; padding: 20px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Cost - the hero metric */
|
|
129
|
+
.cost-card { padding: 26px; }
|
|
130
|
+
.cost-top { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
|
|
131
|
+
.cost-usd { font-size: 44px; font-weight: 680; letter-spacing: -1px; font-family: var(--mono); }
|
|
132
|
+
.cost-label { color: var(--muted); font-size: 14px; }
|
|
133
|
+
.token-grid {
|
|
134
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
135
|
+
gap: 1px; margin-top: 20px; background: var(--border);
|
|
136
|
+
border: 1px solid var(--border); border-radius: 10px; overflow: hidden;
|
|
137
|
+
}
|
|
138
|
+
.token-cell { background: var(--panel-2); padding: 13px 15px; }
|
|
139
|
+
.token-cell .k { color: var(--faint); font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; }
|
|
140
|
+
.token-cell .v { font-size: 18px; font-family: var(--mono); margin-top: 4px; }
|
|
141
|
+
|
|
142
|
+
/* Stat row */
|
|
143
|
+
.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 14px; }
|
|
144
|
+
.stat { background: var(--panel); border: 1px solid var(--border); border-radius: 11px; padding: 16px; }
|
|
145
|
+
.stat .k { color: var(--faint); font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; }
|
|
146
|
+
.stat .v { font-size: 22px; font-family: var(--mono); margin-top: 5px; }
|
|
147
|
+
.stat .v small { font-size: 13px; color: var(--muted); }
|
|
148
|
+
.add { color: var(--green); }
|
|
149
|
+
.del { color: var(--red); }
|
|
150
|
+
|
|
151
|
+
/* Diffstat file list */
|
|
152
|
+
.files { margin-top: 14px; }
|
|
153
|
+
.file-row {
|
|
154
|
+
display: flex; align-items: center; gap: 10px; padding: 7px 0;
|
|
155
|
+
border-top: 1px solid var(--border); font-family: var(--mono); font-size: 13px;
|
|
156
|
+
}
|
|
157
|
+
.file-row:first-child { border-top: none; }
|
|
158
|
+
.file-row .path { flex: 1; color: var(--text); word-break: break-all; }
|
|
159
|
+
.file-row .status {
|
|
160
|
+
font-size: 11px; text-transform: uppercase; color: var(--faint);
|
|
161
|
+
border: 1px solid var(--border); border-radius: 5px; padding: 1px 6px;
|
|
162
|
+
}
|
|
163
|
+
.file-row .nums { white-space: nowrap; }
|
|
164
|
+
|
|
165
|
+
/* Diff (inline patches) */
|
|
166
|
+
.diff-file { border-top: 1px solid var(--border); }
|
|
167
|
+
.diff-file:first-child { border-top: none; }
|
|
168
|
+
.diff-file > summary {
|
|
169
|
+
cursor: pointer; padding: 11px 4px; font-family: var(--mono); font-size: 13px;
|
|
170
|
+
color: var(--text); list-style: none; user-select: none;
|
|
171
|
+
}
|
|
172
|
+
.diff-file > summary::-webkit-details-marker { display: none; }
|
|
173
|
+
.diff-file > summary::before { content: "+ "; color: var(--faint); }
|
|
174
|
+
.diff-file[open] > summary::before { content: "- "; }
|
|
175
|
+
.diff-file pre {
|
|
176
|
+
margin: 0 0 12px; padding: 12px 14px; background: var(--panel-2);
|
|
177
|
+
border: 1px solid var(--border); border-radius: 8px; overflow-x: auto;
|
|
178
|
+
font-family: var(--mono); font-size: 12px; line-height: 1.45; color: var(--muted);
|
|
179
|
+
white-space: pre;
|
|
180
|
+
}
|
|
181
|
+
.diff-line-add { color: var(--green); }
|
|
182
|
+
.diff-line-del { color: var(--red); }
|
|
183
|
+
.diff-line-hd { color: var(--accent); }
|
|
184
|
+
|
|
185
|
+
/* Council */
|
|
186
|
+
.verdict-badge {
|
|
187
|
+
display: inline-block; font-size: 13px; font-weight: 600; padding: 4px 11px;
|
|
188
|
+
border-radius: 7px; border: 1px solid var(--border);
|
|
189
|
+
}
|
|
190
|
+
.verdict-approve { color: var(--green); border-color: rgba(52,211,153,0.4); background: rgba(52,211,153,0.08); }
|
|
191
|
+
.verdict-reject { color: var(--red); border-color: rgba(248,113,113,0.4); background: rgba(248,113,113,0.08); }
|
|
192
|
+
.verdict-concern { color: var(--amber); border-color: rgba(251,191,36,0.4); background: rgba(251,191,36,0.08); }
|
|
193
|
+
.verdict-meta { color: var(--muted); font-size: 13px; margin-left: 10px; }
|
|
194
|
+
.reviewer {
|
|
195
|
+
border-top: 1px solid var(--border); padding: 14px 0;
|
|
196
|
+
}
|
|
197
|
+
.reviewer:first-child { border-top: none; padding-top: 4px; }
|
|
198
|
+
.reviewer .head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
199
|
+
.reviewer .role { font-weight: 600; font-size: 14px; }
|
|
200
|
+
.reviewer .vote { font-size: 12px; font-weight: 600; }
|
|
201
|
+
.vote.approve { color: var(--green); }
|
|
202
|
+
.vote.reject { color: var(--red); }
|
|
203
|
+
.vote.concern { color: var(--amber); }
|
|
204
|
+
.reviewer .summary { color: var(--muted); font-size: 14px; margin-top: 6px; white-space: pre-wrap; }
|
|
205
|
+
|
|
206
|
+
/* Gates */
|
|
207
|
+
.gates { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
|
|
208
|
+
.gate {
|
|
209
|
+
font-size: 12px; font-family: var(--mono); padding: 4px 9px; border-radius: 7px;
|
|
210
|
+
border: 1px solid var(--border); display: flex; gap: 7px; align-items: center;
|
|
211
|
+
}
|
|
212
|
+
.gate .mk { font-weight: 700; }
|
|
213
|
+
.gate.pass .mk { color: var(--green); }
|
|
214
|
+
.gate.fail .mk { color: var(--red); }
|
|
215
|
+
.gate.skip .mk { color: var(--faint); }
|
|
216
|
+
|
|
217
|
+
/* Limitations / flagged */
|
|
218
|
+
.note { color: var(--muted); font-size: 14px; }
|
|
219
|
+
.note-list { margin: 0; padding-left: 18px; }
|
|
220
|
+
.note-list li { margin: 5px 0; color: var(--muted); font-size: 14px; }
|
|
221
|
+
|
|
222
|
+
/* Provenance key-value */
|
|
223
|
+
.kv { display: grid; grid-template-columns: 180px 1fr; gap: 8px 16px; font-size: 14px; }
|
|
224
|
+
.kv .k { color: var(--faint); }
|
|
225
|
+
.kv .v { font-family: var(--mono); word-break: break-all; }
|
|
226
|
+
.brief {
|
|
227
|
+
margin-top: 10px; background: var(--panel-2); border: 1px solid var(--border);
|
|
228
|
+
border-radius: 9px; padding: 14px; font-size: 14px; color: var(--text);
|
|
229
|
+
white-space: pre-wrap; word-break: break-word;
|
|
230
|
+
}
|
|
231
|
+
.hash-note { color: var(--faint); font-size: 12px; margin-top: 8px; }
|
|
232
|
+
|
|
233
|
+
/* Copy command */
|
|
234
|
+
.cmd {
|
|
235
|
+
display: flex; align-items: stretch; gap: 0; margin-top: 10px;
|
|
236
|
+
border: 1px solid var(--border); border-radius: 9px; overflow: hidden;
|
|
237
|
+
background: var(--panel-2);
|
|
238
|
+
}
|
|
239
|
+
.cmd code {
|
|
240
|
+
flex: 1; padding: 11px 14px; font-family: var(--mono); font-size: 13px;
|
|
241
|
+
color: var(--text); overflow-x: auto; white-space: nowrap;
|
|
242
|
+
}
|
|
243
|
+
.cmd .copy {
|
|
244
|
+
border: none; border-left: 1px solid var(--border); background: var(--panel);
|
|
245
|
+
color: var(--text); padding: 0 16px; cursor: pointer; font-size: 13px; font-weight: 550;
|
|
246
|
+
}
|
|
247
|
+
.cmd .copy:hover { background: var(--accent-soft); }
|
|
248
|
+
|
|
249
|
+
.footer { margin-top: 50px; text-align: center; color: var(--faint); font-size: 13px; }
|
|
250
|
+
.hide { display: none !important; }
|
|
251
|
+
</style>
|
|
252
|
+
</head>
|
|
253
|
+
<body>
|
|
254
|
+
<div class="wrap">
|
|
255
|
+
<div class="topbar">
|
|
256
|
+
<span class="brand">Loki Mode<span class="dot">.</span> proof of run</span>
|
|
257
|
+
<span id="runIdTop"></span>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<!-- HERO -->
|
|
261
|
+
<section class="hero">
|
|
262
|
+
<h1 id="heroLine"></h1>
|
|
263
|
+
<p class="sub" id="heroSub"></p>
|
|
264
|
+
<div class="hero-actions" id="heroActions"></div>
|
|
265
|
+
</section>
|
|
266
|
+
|
|
267
|
+
<!-- Tier 1: deployed URL + diff -->
|
|
268
|
+
<section class="section" id="secTier1">
|
|
269
|
+
<h2>Verify it yourself</h2>
|
|
270
|
+
<div class="card" id="tier1Card"></div>
|
|
271
|
+
</section>
|
|
272
|
+
|
|
273
|
+
<!-- Tier 2: cost (hero metric) -->
|
|
274
|
+
<section class="section">
|
|
275
|
+
<h2>The bill</h2>
|
|
276
|
+
<div class="card cost-card" id="costCard"></div>
|
|
277
|
+
</section>
|
|
278
|
+
|
|
279
|
+
<!-- Tier 2: wall clock + diffstat -->
|
|
280
|
+
<section class="section">
|
|
281
|
+
<h2>What changed</h2>
|
|
282
|
+
<div class="stat-row" id="statRow"></div>
|
|
283
|
+
<div class="card files" id="filesCard" style="margin-top:14px;"></div>
|
|
284
|
+
</section>
|
|
285
|
+
|
|
286
|
+
<!-- Tier 1: the diff (inline patches, only when --include-diffs) -->
|
|
287
|
+
<section class="section hide" id="secDiff">
|
|
288
|
+
<h2>The diff</h2>
|
|
289
|
+
<div class="card" id="diffCard"></div>
|
|
290
|
+
</section>
|
|
291
|
+
|
|
292
|
+
<!-- Tier 3: council -->
|
|
293
|
+
<section class="section" id="secCouncil">
|
|
294
|
+
<h2>Review verdict</h2>
|
|
295
|
+
<div class="card" id="councilCard"></div>
|
|
296
|
+
</section>
|
|
297
|
+
|
|
298
|
+
<!-- Tier 3: quality gates -->
|
|
299
|
+
<section class="section" id="secGates">
|
|
300
|
+
<h2>Quality gates</h2>
|
|
301
|
+
<div class="card" id="gatesCard"></div>
|
|
302
|
+
</section>
|
|
303
|
+
|
|
304
|
+
<!-- Tier 3: flagged / limitations -->
|
|
305
|
+
<section class="section">
|
|
306
|
+
<h2>What the run flagged</h2>
|
|
307
|
+
<div class="card" id="flaggedCard"></div>
|
|
308
|
+
</section>
|
|
309
|
+
|
|
310
|
+
<!-- Tier 4: spec / before -->
|
|
311
|
+
<section class="section" id="secSpec">
|
|
312
|
+
<h2>The spec (the before)</h2>
|
|
313
|
+
<div class="card" id="specCard"></div>
|
|
314
|
+
</section>
|
|
315
|
+
|
|
316
|
+
<!-- Tier 4: provenance -->
|
|
317
|
+
<section class="section">
|
|
318
|
+
<h2>Provenance</h2>
|
|
319
|
+
<div class="card" id="provCard"></div>
|
|
320
|
+
</section>
|
|
321
|
+
|
|
322
|
+
<!-- CTA -->
|
|
323
|
+
<section class="section">
|
|
324
|
+
<h2>Run this yourself</h2>
|
|
325
|
+
<div class="card" id="ctaCard"></div>
|
|
326
|
+
</section>
|
|
327
|
+
|
|
328
|
+
<div class="footer">
|
|
329
|
+
<a id="madeWith" href="https://github.com/asklokesh/loki-mode" rel="noopener">Made with Loki Mode</a>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<script type="application/json" id="proof-data">__PROOF_JSON__</script>
|
|
334
|
+
<script>
|
|
335
|
+
(function () {
|
|
336
|
+
"use strict";
|
|
337
|
+
|
|
338
|
+
function parseProof() {
|
|
339
|
+
var el = document.getElementById("proof-data");
|
|
340
|
+
if (!el) return null;
|
|
341
|
+
var raw = (el.textContent || "").trim();
|
|
342
|
+
// Detect an un-substituted template structurally (the blob is not JSON)
|
|
343
|
+
// rather than string-matching the placeholder token, so the token stays
|
|
344
|
+
// unique in this file and the generator's global replace cannot clobber
|
|
345
|
+
// this guard.
|
|
346
|
+
if (!raw || raw.charAt(0) !== "{") return null;
|
|
347
|
+
try { return JSON.parse(raw); } catch (e) { return null; }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Safe accessors. Never throw, never print "undefined".
|
|
351
|
+
function g(obj, path, dflt) {
|
|
352
|
+
var cur = obj;
|
|
353
|
+
var parts = path.split(".");
|
|
354
|
+
for (var i = 0; i < parts.length; i++) {
|
|
355
|
+
if (cur === null || cur === undefined) return dflt;
|
|
356
|
+
cur = cur[parts[i]];
|
|
357
|
+
}
|
|
358
|
+
return (cur === null || cur === undefined) ? dflt : cur;
|
|
359
|
+
}
|
|
360
|
+
function num(v, dflt) {
|
|
361
|
+
var n = Number(v);
|
|
362
|
+
return isFinite(n) ? n : (dflt === undefined ? 0 : dflt);
|
|
363
|
+
}
|
|
364
|
+
function esc(s) {
|
|
365
|
+
s = (s === null || s === undefined) ? "" : String(s);
|
|
366
|
+
return s.replace(/&/g, "&").replace(/</g, "<")
|
|
367
|
+
.replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
368
|
+
}
|
|
369
|
+
function fmtInt(n) {
|
|
370
|
+
n = num(n);
|
|
371
|
+
return n.toLocaleString("en-US");
|
|
372
|
+
}
|
|
373
|
+
function fmtUsd(n) {
|
|
374
|
+
n = num(n);
|
|
375
|
+
// Precise odd values, not rounded. Up to 4 decimals, trim to >=2.
|
|
376
|
+
var s = n.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
|
|
377
|
+
if (s.indexOf(".") === -1) s += ".00";
|
|
378
|
+
else if (s.split(".")[1].length === 1) s += "0";
|
|
379
|
+
return "$" + s;
|
|
380
|
+
}
|
|
381
|
+
function fmtDuration(sec) {
|
|
382
|
+
sec = Math.max(0, Math.round(num(sec)));
|
|
383
|
+
if (sec < 60) return sec + "s";
|
|
384
|
+
var m = Math.floor(sec / 60), s = sec % 60;
|
|
385
|
+
if (m < 60) return m + "m " + s + "s";
|
|
386
|
+
var h = Math.floor(m / 60); m = m % 60;
|
|
387
|
+
return h + "h " + m + "m";
|
|
388
|
+
}
|
|
389
|
+
// Treat localhost / 127.* / empty as NOT a publicly clickable live link, but
|
|
390
|
+
// still display localhost as data.
|
|
391
|
+
function isLocalUrl(u) {
|
|
392
|
+
if (!u) return true;
|
|
393
|
+
return /^https?:\/\/(localhost|127\.|0\.0\.0\.0|\[::1\])/i.test(u);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function render(p) {
|
|
397
|
+
renderHero(p);
|
|
398
|
+
renderTier1(p);
|
|
399
|
+
renderCost(p);
|
|
400
|
+
renderStatsAndFiles(p);
|
|
401
|
+
renderDiff(p);
|
|
402
|
+
renderCouncil(p);
|
|
403
|
+
renderGates(p);
|
|
404
|
+
renderFlagged(p);
|
|
405
|
+
renderSpec(p);
|
|
406
|
+
renderProvenance(p);
|
|
407
|
+
renderCta(p);
|
|
408
|
+
var rid = g(p, "run_id", "");
|
|
409
|
+
if (rid) document.getElementById("runIdTop").textContent = "run " + rid;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function councilSummary(p) {
|
|
413
|
+
// Returns {count, total, ok} of reviewers who voted approve, or null if no
|
|
414
|
+
// usable council data.
|
|
415
|
+
if (!g(p, "council.enabled", false)) return null;
|
|
416
|
+
var reviewers = g(p, "council.reviewers", []);
|
|
417
|
+
if (!Array.isArray(reviewers) || reviewers.length === 0) return null;
|
|
418
|
+
var ok = 0;
|
|
419
|
+
for (var i = 0; i < reviewers.length; i++) {
|
|
420
|
+
var v = String(g(reviewers[i], "vote", "")).toUpperCase();
|
|
421
|
+
if (v === "APPROVE" || v === "APPROVED") ok++;
|
|
422
|
+
}
|
|
423
|
+
return { count: ok, total: reviewers.length };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function renderHero(p) {
|
|
427
|
+
// cost.usd is null when cost was not collected for this run. g() coerces
|
|
428
|
+
// null to its default, so read with a null default and branch BEFORE
|
|
429
|
+
// fmtUsd: a skeptic seeing a zero-dollar total assumes the artifact is fake.
|
|
430
|
+
var usdRaw = g(p, "cost.usd", null);
|
|
431
|
+
var fc = num(g(p, "files_changed.count", 0));
|
|
432
|
+
var parts = ["Built and verified autonomously"];
|
|
433
|
+
if (usdRaw !== null) parts.push(fmtUsd(usdRaw));
|
|
434
|
+
else parts.push("cost not recorded");
|
|
435
|
+
parts.push(fc + " file" + (fc === 1 ? "" : "s") + " changed");
|
|
436
|
+
var cs = councilSummary(p);
|
|
437
|
+
if (cs) parts.push(cs.count + "-of-" + cs.total + " reviewers approved");
|
|
438
|
+
document.getElementById("heroLine").textContent = parts.join(" - ");
|
|
439
|
+
|
|
440
|
+
var sub = [];
|
|
441
|
+
var dur = g(p, "wall_clock_sec", null);
|
|
442
|
+
if (dur !== null) sub.push("in " + fmtDuration(dur));
|
|
443
|
+
var prov = g(p, "provider.name", "");
|
|
444
|
+
var model = g(p, "provider.model", "");
|
|
445
|
+
if (prov || model) sub.push("via " + esc(prov) + (model ? " / " + esc(model) : ""));
|
|
446
|
+
document.getElementById("heroSub").innerHTML = sub.join(" · ");
|
|
447
|
+
|
|
448
|
+
var actions = document.getElementById("heroActions");
|
|
449
|
+
var html = "";
|
|
450
|
+
var deployed = g(p, "deployment.deployed_url", null);
|
|
451
|
+
if (deployed) {
|
|
452
|
+
var label = isLocalUrl(deployed) ? "Open the app (local)" : "Open the deployed app";
|
|
453
|
+
html += '<a class="btn primary" href="' + esc(deployed) + '" rel="noopener">' + label + '</a>';
|
|
454
|
+
}
|
|
455
|
+
// Tier-1 "View the diff" action: only when inline patches are embedded.
|
|
456
|
+
if (hasDiffs(p)) {
|
|
457
|
+
html += '<a class="btn" href="#secDiff">View the diff</a>';
|
|
458
|
+
}
|
|
459
|
+
actions.innerHTML = html || '<span class="note">No live URL recorded for this run.</span>';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function renderTier1(p) {
|
|
463
|
+
var card = document.getElementById("tier1Card");
|
|
464
|
+
var rows = [];
|
|
465
|
+
var deployed = g(p, "deployment.deployed_url", null);
|
|
466
|
+
var pub = g(p, "deployment.public_url", null);
|
|
467
|
+
if (deployed) {
|
|
468
|
+
var note = isLocalUrl(deployed) ? ' <span class="note">(local only)</span>' : "";
|
|
469
|
+
rows.push('<div class="kv"><span class="k">Deployed URL</span><span class="v"><a href="' +
|
|
470
|
+
esc(deployed) + '" rel="noopener">' + esc(deployed) + "</a>" + note + "</span></div>");
|
|
471
|
+
} else {
|
|
472
|
+
rows.push('<div class="kv"><span class="k">Deployed URL</span><span class="v note">none recorded</span></div>');
|
|
473
|
+
}
|
|
474
|
+
if (pub) {
|
|
475
|
+
rows.push('<div class="kv"><span class="k">Public URL</span><span class="v"><a href="' +
|
|
476
|
+
esc(pub) + '" rel="noopener">' + esc(pub) + "</a></span></div>");
|
|
477
|
+
}
|
|
478
|
+
var findings = g(p, "council.findings_link", null);
|
|
479
|
+
if (findings) {
|
|
480
|
+
rows.push('<div class="kv" style="margin-top:8px;"><span class="k">Review findings</span><span class="v"><a href="' +
|
|
481
|
+
esc(findings) + '" rel="noopener">' + esc(findings) + "</a></span></div>");
|
|
482
|
+
}
|
|
483
|
+
card.innerHTML = rows.join("");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function renderCost(p) {
|
|
487
|
+
var card = document.getElementById("costCard");
|
|
488
|
+
var usdRaw = g(p, "cost.usd", null);
|
|
489
|
+
var cells = [
|
|
490
|
+
["Input tokens", fmtInt(g(p, "cost.input_tokens", 0))],
|
|
491
|
+
["Output tokens", fmtInt(g(p, "cost.output_tokens", 0))],
|
|
492
|
+
["Cache read", fmtInt(g(p, "cost.cache_read_tokens", 0))],
|
|
493
|
+
["Cache write", fmtInt(g(p, "cost.cache_creation_tokens", 0))]
|
|
494
|
+
];
|
|
495
|
+
var grid = "";
|
|
496
|
+
for (var i = 0; i < cells.length; i++) {
|
|
497
|
+
grid += '<div class="token-cell"><div class="k">' + cells[i][0] +
|
|
498
|
+
'</div><div class="v">' + cells[i][1] + "</div></div>";
|
|
499
|
+
}
|
|
500
|
+
// When cost was not collected, show a muted "not recorded" line instead of
|
|
501
|
+
// a zero-dollar total (which reads as fake/broken to a skeptic).
|
|
502
|
+
var top;
|
|
503
|
+
if (usdRaw !== null) {
|
|
504
|
+
top = '<div class="cost-top"><span class="cost-usd">' + fmtUsd(usdRaw) +
|
|
505
|
+
'</span><span class="cost-label">total cost for this run, itemized below</span></div>';
|
|
506
|
+
} else {
|
|
507
|
+
top = '<div class="cost-top"><span class="cost-label">Cost was not recorded for this run. ' +
|
|
508
|
+
'Token usage is itemized below.</span></div>';
|
|
509
|
+
}
|
|
510
|
+
card.innerHTML = top + '<div class="token-grid">' + grid + "</div>";
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function renderStatsAndFiles(p) {
|
|
514
|
+
var row = document.getElementById("statRow");
|
|
515
|
+
var ins = num(g(p, "files_changed.insertions", 0));
|
|
516
|
+
var del = num(g(p, "files_changed.deletions", 0));
|
|
517
|
+
var fc = num(g(p, "files_changed.count", 0));
|
|
518
|
+
var iters = num(g(p, "iterations.count", 0));
|
|
519
|
+
var isucc = num(g(p, "iterations.succeeded", 0));
|
|
520
|
+
var ifail = num(g(p, "iterations.failed", 0));
|
|
521
|
+
row.innerHTML =
|
|
522
|
+
'<div class="stat"><div class="k">Wall clock</div><div class="v">' + fmtDuration(g(p, "wall_clock_sec", 0)) + "</div></div>" +
|
|
523
|
+
'<div class="stat"><div class="k">Files changed</div><div class="v">' + fc + "</div></div>" +
|
|
524
|
+
'<div class="stat"><div class="k">Lines</div><div class="v"><span class="add">+' + fmtInt(ins) + '</span> <span class="del">-' + fmtInt(del) + "</span></div></div>" +
|
|
525
|
+
'<div class="stat"><div class="k">Iterations</div><div class="v">' + iters +
|
|
526
|
+
' <small>' + isucc + " ok" + (ifail ? " / " + ifail + " failed" : "") + "</small></div></div>";
|
|
527
|
+
|
|
528
|
+
var filesCard = document.getElementById("filesCard");
|
|
529
|
+
var files = g(p, "files_changed.files", []);
|
|
530
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
531
|
+
filesCard.classList.add("hide");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
var rows = "";
|
|
535
|
+
for (var i = 0; i < files.length; i++) {
|
|
536
|
+
var f = files[i];
|
|
537
|
+
rows += '<div class="file-row">' +
|
|
538
|
+
'<span class="status">' + esc(g(f, "status", "M")) + "</span>" +
|
|
539
|
+
'<span class="path">' + esc(g(f, "path", "")) + "</span>" +
|
|
540
|
+
'<span class="nums"><span class="add">+' + fmtInt(g(f, "insertions", 0)) +
|
|
541
|
+
'</span> <span class="del">-' + fmtInt(g(f, "deletions", 0)) + "</span></span></div>";
|
|
542
|
+
}
|
|
543
|
+
filesCard.innerHTML = rows;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function hasDiffs(p) {
|
|
547
|
+
var d = g(p, "diffs", null);
|
|
548
|
+
return Array.isArray(d) && d.length > 0;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Render one redacted patch with light line coloring. All patch text is run
|
|
552
|
+
// through esc() so the <script>-breakout chars that the JSON layer already
|
|
553
|
+
// neutralized (via the generator's < escape) cannot inject markup here
|
|
554
|
+
// either.
|
|
555
|
+
function renderPatch(patch) {
|
|
556
|
+
var lines = String(patch || "").split("\n");
|
|
557
|
+
var out = "";
|
|
558
|
+
for (var i = 0; i < lines.length; i++) {
|
|
559
|
+
var ln = lines[i];
|
|
560
|
+
var cls = "";
|
|
561
|
+
if (ln.charAt(0) === "+" && ln.indexOf("+++") !== 0) cls = "diff-line-add";
|
|
562
|
+
else if (ln.charAt(0) === "-" && ln.indexOf("---") !== 0) cls = "diff-line-del";
|
|
563
|
+
else if (ln.indexOf("@@") === 0 || ln.indexOf("diff ") === 0 ||
|
|
564
|
+
ln.indexOf("+++") === 0 || ln.indexOf("---") === 0) cls = "diff-line-hd";
|
|
565
|
+
out += (cls ? '<span class="' + cls + '">' + esc(ln) + "</span>" : esc(ln)) + "\n";
|
|
566
|
+
}
|
|
567
|
+
return out;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function renderDiff(p) {
|
|
571
|
+
var sec = document.getElementById("secDiff");
|
|
572
|
+
var card = document.getElementById("diffCard");
|
|
573
|
+
if (!hasDiffs(p)) return; // section stays hidden (default class "hide")
|
|
574
|
+
var diffs = g(p, "diffs", []);
|
|
575
|
+
var blocks = "";
|
|
576
|
+
for (var i = 0; i < diffs.length; i++) {
|
|
577
|
+
var path = esc(g(diffs[i], "path", "(unknown file)"));
|
|
578
|
+
var patch = g(diffs[i], "patch", "");
|
|
579
|
+
var openAttr = (i === 0) ? " open" : "";
|
|
580
|
+
blocks += '<details class="diff-file"' + openAttr + '><summary>' + path +
|
|
581
|
+
"</summary><pre>" + renderPatch(patch) + "</pre></details>";
|
|
582
|
+
}
|
|
583
|
+
card.innerHTML = blocks;
|
|
584
|
+
sec.classList.remove("hide");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function voteClass(v) {
|
|
588
|
+
v = String(v || "").toUpperCase();
|
|
589
|
+
if (v.indexOf("APPROVE") === 0) return "approve";
|
|
590
|
+
if (v.indexOf("REJECT") === 0) return "reject";
|
|
591
|
+
if (v.indexOf("CONCERN") === 0) return "concern";
|
|
592
|
+
return "";
|
|
593
|
+
}
|
|
594
|
+
function verdictClass(v) {
|
|
595
|
+
v = String(v || "").toUpperCase();
|
|
596
|
+
if (v.indexOf("APPROVE") === 0 || v === "PASS" || v === "PASSED") return "verdict-approve";
|
|
597
|
+
if (v.indexOf("REJECT") === 0 || v.indexOf("BLOCK") === 0 || v === "FAIL") return "verdict-reject";
|
|
598
|
+
if (v.indexOf("CONCERN") === 0) return "verdict-concern";
|
|
599
|
+
return "";
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function renderCouncil(p) {
|
|
603
|
+
var sec = document.getElementById("secCouncil");
|
|
604
|
+
var card = document.getElementById("councilCard");
|
|
605
|
+
if (!g(p, "council.enabled", false)) {
|
|
606
|
+
card.innerHTML = '<p class="note">Council review was not enabled for this run.</p>';
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
var verdict = g(p, "council.final_verdict", "");
|
|
610
|
+
var threshold = g(p, "council.threshold", null);
|
|
611
|
+
var reviewers = g(p, "council.reviewers", []);
|
|
612
|
+
var head = "";
|
|
613
|
+
if (verdict) {
|
|
614
|
+
head += '<span class="verdict-badge ' + verdictClass(verdict) + '">' + esc(verdict) + "</span>";
|
|
615
|
+
}
|
|
616
|
+
var cs = councilSummary(p);
|
|
617
|
+
if (cs) head += '<span class="verdict-meta">' + cs.count + " of " + cs.total + " approved" +
|
|
618
|
+
(threshold !== null ? " (threshold " + esc(threshold) + ")" : "") + "</span>";
|
|
619
|
+
var body = "";
|
|
620
|
+
if (Array.isArray(reviewers) && reviewers.length > 0) {
|
|
621
|
+
for (var i = 0; i < reviewers.length; i++) {
|
|
622
|
+
var r = reviewers[i];
|
|
623
|
+
var v = g(r, "vote", "");
|
|
624
|
+
var summary = g(r, "summary", "");
|
|
625
|
+
body += '<div class="reviewer"><div class="head">' +
|
|
626
|
+
'<span class="role">' + esc(g(r, "role", "reviewer")) + "</span>" +
|
|
627
|
+
'<span class="vote ' + voteClass(v) + '">' + esc(v || "no vote") + "</span></div>" +
|
|
628
|
+
(summary ? '<div class="summary">' + esc(summary) + "</div>" : "") +
|
|
629
|
+
"</div>";
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
body = '<p class="note">Per-reviewer summaries were not recorded for this run.</p>';
|
|
633
|
+
}
|
|
634
|
+
var findings = g(p, "council.findings_link", null);
|
|
635
|
+
var link = findings ? '<div class="kv" style="margin-top:14px;"><span class="k">Findings</span><span class="v"><a href="' +
|
|
636
|
+
esc(findings) + '" rel="noopener">' + esc(findings) + "</a></span></div>" : "";
|
|
637
|
+
card.innerHTML = (head ? '<div style="margin-bottom:14px;">' + head + "</div>" : "") + body + link;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function renderGates(p) {
|
|
641
|
+
var sec = document.getElementById("secGates");
|
|
642
|
+
var card = document.getElementById("gatesCard");
|
|
643
|
+
var gates = g(p, "quality_gates.gates", []);
|
|
644
|
+
var passed = num(g(p, "quality_gates.passed", 0));
|
|
645
|
+
var total = num(g(p, "quality_gates.total", 0));
|
|
646
|
+
if ((!Array.isArray(gates) || gates.length === 0) && total === 0) {
|
|
647
|
+
sec.classList.add("hide");
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
var head = '<p class="note" style="margin:0 0 12px;">' + passed + " of " + total + " gates passed.</p>";
|
|
651
|
+
var chips = "";
|
|
652
|
+
if (Array.isArray(gates)) {
|
|
653
|
+
for (var i = 0; i < gates.length; i++) {
|
|
654
|
+
var st = String(g(gates[i], "status", "")).toLowerCase();
|
|
655
|
+
var cls = (st === "pass" || st === "passed") ? "pass" : (st === "skip" || st === "skipped") ? "skip" : "fail";
|
|
656
|
+
var mark = cls === "pass" ? "OK" : cls === "skip" ? "-" : "X";
|
|
657
|
+
chips += '<span class="gate ' + cls + '"><span class="mk">' + mark + "</span>" +
|
|
658
|
+
esc(g(gates[i], "name", "gate")) + "</span>";
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
card.innerHTML = head + (chips ? '<div class="gates">' + chips + "</div>" : "");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function renderFlagged(p) {
|
|
665
|
+
var card = document.getElementById("flaggedCard");
|
|
666
|
+
var items = [];
|
|
667
|
+
// Honest limitations, derived from the run's own data. Never hide.
|
|
668
|
+
var ifail = num(g(p, "iterations.failed", 0));
|
|
669
|
+
if (ifail > 0) items.push(ifail + " iteration" + (ifail === 1 ? "" : "s") + " failed before completion.");
|
|
670
|
+
var qp = num(g(p, "quality_gates.passed", 0)), qt = num(g(p, "quality_gates.total", 0));
|
|
671
|
+
if (qt > 0 && qp < qt) items.push((qt - qp) + " quality gate" + ((qt - qp) === 1 ? "" : "s") + " did not pass.");
|
|
672
|
+
var reviewers = g(p, "council.reviewers", []);
|
|
673
|
+
if (Array.isArray(reviewers)) {
|
|
674
|
+
for (var i = 0; i < reviewers.length; i++) {
|
|
675
|
+
var vc = voteClass(g(reviewers[i], "vote", ""));
|
|
676
|
+
if (vc === "reject" || vc === "concern") {
|
|
677
|
+
items.push("Reviewer " + esc(g(reviewers[i], "role", "")) + " raised a " + vc + ".");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
var deployed = g(p, "deployment.deployed_url", null);
|
|
682
|
+
if (!deployed) items.push("No deployed URL was recorded; this run produced code only.");
|
|
683
|
+
else if (isLocalUrl(deployed)) items.push("Deployment is local only (localhost); no public live URL exists for this run.");
|
|
684
|
+
var diffs = g(p, "diffs", null);
|
|
685
|
+
if (diffs === null) items.push("Full diffs are not embedded in this artifact (run without --include-diffs).");
|
|
686
|
+
|
|
687
|
+
if (items.length === 0) {
|
|
688
|
+
card.innerHTML = '<p class="note">Nothing was flagged. All gates passed, all reviewers approved, no failed iterations.</p>';
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
var lis = "";
|
|
692
|
+
for (var j = 0; j < items.length; j++) lis += "<li>" + items[j] + "</li>";
|
|
693
|
+
card.innerHTML = '<ul class="note-list">' + lis + "</ul>";
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function renderSpec(p) {
|
|
697
|
+
var card = document.getElementById("specCard");
|
|
698
|
+
var src = g(p, "spec.source", "");
|
|
699
|
+
var brief = g(p, "spec.brief", "");
|
|
700
|
+
var head = src ? '<div class="kv"><span class="k">Source</span><span class="v">' + esc(src) + "</span></div>" : "";
|
|
701
|
+
var body = brief ? '<div class="brief">' + esc(brief) + "</div>" :
|
|
702
|
+
'<p class="note">No brief recorded.</p>';
|
|
703
|
+
card.innerHTML = head + body;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function renderProvenance(p) {
|
|
707
|
+
var card = document.getElementById("provCard");
|
|
708
|
+
var rows = [
|
|
709
|
+
["Loki version", g(p, "loki_version", "")],
|
|
710
|
+
["Provider", g(p, "provider.name", "")],
|
|
711
|
+
["Model", g(p, "provider.model", "")],
|
|
712
|
+
["Started at", g(p, "started_at", "")],
|
|
713
|
+
["Generated at", g(p, "generated_at", "")],
|
|
714
|
+
["Run id", g(p, "run_id", "")],
|
|
715
|
+
["Schema version", g(p, "schema_version", "")]
|
|
716
|
+
];
|
|
717
|
+
var kv = "";
|
|
718
|
+
for (var i = 0; i < rows.length; i++) {
|
|
719
|
+
if (rows[i][1] === "" || rows[i][1] === null) continue;
|
|
720
|
+
kv += '<div class="k">' + esc(rows[i][0]) + '</div><div class="v">' + esc(rows[i][1]) + "</div>";
|
|
721
|
+
}
|
|
722
|
+
var hash = g(p, "verification.hash", "");
|
|
723
|
+
var algo = g(p, "verification.algo", "sha256");
|
|
724
|
+
if (hash) {
|
|
725
|
+
kv += '<div class="k">Integrity hash</div><div class="v">' + esc(algo) + ":" + esc(hash) + "</div>";
|
|
726
|
+
}
|
|
727
|
+
card.innerHTML = '<div class="kv">' + kv + "</div>" +
|
|
728
|
+
(hash ? '<p class="hash-note">The integrity hash is a ' + esc(algo) +
|
|
729
|
+
" digest of this proof. It proves the artifact has not been altered after it was emitted. It does not, by itself, prove who or what produced the work.</p>" : "");
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function renderCta(p) {
|
|
733
|
+
var card = document.getElementById("ctaCard");
|
|
734
|
+
var src = g(p, "spec.source", "");
|
|
735
|
+
var cmd = "loki start";
|
|
736
|
+
if (src && /^[\w.\-]+\/[\w.\-]+#\d+$/.test(src)) cmd = "loki start " + src;
|
|
737
|
+
var install = "bun install -g loki-mode";
|
|
738
|
+
var html =
|
|
739
|
+
'<p class="note" style="margin-top:0;">Install Loki Mode and take your own spec to a verified build.</p>' +
|
|
740
|
+
cmdBlock("install", install) +
|
|
741
|
+
cmdBlock("run", cmd);
|
|
742
|
+
var secondary = "";
|
|
743
|
+
var deployed = g(p, "deployment.deployed_url", null);
|
|
744
|
+
if (deployed) {
|
|
745
|
+
var label = isLocalUrl(deployed) ? "Open the app (local)" : "Open the deployed app";
|
|
746
|
+
secondary += '<a class="btn" href="' + esc(deployed) + '" rel="noopener">' + label + "</a>";
|
|
747
|
+
}
|
|
748
|
+
if (hasDiffs(p)) {
|
|
749
|
+
secondary += '<a class="btn" href="#secDiff">View the diff</a>';
|
|
750
|
+
}
|
|
751
|
+
if (secondary) {
|
|
752
|
+
html += '<div class="hero-actions" style="margin-top:14px;">' + secondary + "</div>";
|
|
753
|
+
}
|
|
754
|
+
card.innerHTML = html;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function cmdBlock(id, text) {
|
|
758
|
+
return '<div class="cmd"><code id="cmd-' + id + '">' + esc(text) +
|
|
759
|
+
'</code><button class="copy" data-copy="' + esc(text) + '">Copy</button></div>';
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function wireCopy() {
|
|
763
|
+
document.addEventListener("click", function (e) {
|
|
764
|
+
var t = e.target;
|
|
765
|
+
if (!t || !t.getAttribute || t.getAttribute("data-copy") === null) return;
|
|
766
|
+
var text = t.getAttribute("data-copy");
|
|
767
|
+
var done = function () { var o = t.textContent; t.textContent = "Copied"; setTimeout(function () { t.textContent = o; }, 1200); };
|
|
768
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
769
|
+
navigator.clipboard.writeText(text).then(done, function () {});
|
|
770
|
+
} else {
|
|
771
|
+
try {
|
|
772
|
+
var ta = document.createElement("textarea");
|
|
773
|
+
ta.value = text; document.body.appendChild(ta); ta.select();
|
|
774
|
+
document.execCommand("copy"); document.body.removeChild(ta); done();
|
|
775
|
+
} catch (err) {}
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function renderError() {
|
|
781
|
+
var wrap = document.querySelector(".wrap");
|
|
782
|
+
if (wrap) {
|
|
783
|
+
wrap.innerHTML = '<div class="hero"><h1>Proof data not available</h1>' +
|
|
784
|
+
'<p class="sub">This proof artifact did not contain renderable data. ' +
|
|
785
|
+
'It may not have been generated yet.</p></div>';
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
var proof = parseProof();
|
|
790
|
+
wireCopy();
|
|
791
|
+
if (!proof || typeof proof !== "object") {
|
|
792
|
+
renderError();
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
render(proof);
|
|
797
|
+
} catch (e) {
|
|
798
|
+
renderError();
|
|
799
|
+
}
|
|
800
|
+
})();
|
|
801
|
+
</script>
|
|
802
|
+
</body>
|
|
803
|
+
</html>
|