@shield-agent/kya 0.6.0 → 0.8.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/.mcp.json +1 -1
- package/README.md +6 -4
- package/amp/settings.example.json +1 -1
- package/claude/claude_desktop_config.example.json +1 -1
- package/cline/cline_mcp_settings.example.json +1 -1
- package/copilot/mcp-config.example.json +1 -1
- package/dist/certify/context.d.ts +11 -0
- package/dist/certify/context.d.ts.map +1 -0
- package/dist/certify/context.js +49 -0
- package/dist/certify/context.js.map +1 -0
- package/dist/certify/evaluate.d.ts +16 -1
- package/dist/certify/evaluate.d.ts.map +1 -1
- package/dist/certify/evaluate.js +46 -1
- package/dist/certify/evaluate.js.map +1 -1
- package/dist/certify/live.d.ts +3 -0
- package/dist/certify/live.d.ts.map +1 -0
- package/dist/certify/live.js +39 -0
- package/dist/certify/live.js.map +1 -0
- package/dist/certify/render.d.ts.map +1 -1
- package/dist/certify/render.js +1 -0
- package/dist/certify/render.js.map +1 -1
- package/dist/commands/certify.d.ts +1 -2
- package/dist/commands/certify.d.ts.map +1 -1
- package/dist/commands/certify.js +15 -74
- package/dist/commands/certify.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/receipt/enrich.d.ts +21 -0
- package/dist/receipt/enrich.d.ts.map +1 -1
- package/dist/receipt/enrich.js +37 -0
- package/dist/receipt/enrich.js.map +1 -1
- package/dist/receipt/receipt-css.d.ts +16 -0
- package/dist/receipt/receipt-css.d.ts.map +1 -0
- package/dist/receipt/receipt-css.js +508 -0
- package/dist/receipt/receipt-css.js.map +1 -0
- package/dist/receipt/render-receipt.d.ts +3 -1
- package/dist/receipt/render-receipt.d.ts.map +1 -1
- package/dist/receipt/render-receipt.js +214 -397
- package/dist/receipt/render-receipt.js.map +1 -1
- package/droid/README.md +1 -1
- package/droid/mcp.example.json +1 -1
- package/gemini/settings.example.json +1 -1
- package/grok/README.md +1 -1
- package/kilo/kilo.example.json +1 -1
- package/kimi/mcp.example.json +1 -1
- package/kiro/mcp.example.json +1 -1
- package/manifest.json +1 -1
- package/mastracode/mcp.example.json +1 -1
- package/mcp.json +1 -1
- package/openai/codex.config.example.toml +1 -1
- package/opencode/opencode.example.json +1 -1
- package/package.json +1 -1
- package/qwen/settings.example.json +1 -1
- package/server.json +2 -2
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
import { assertNoSecrets, clip, stripEscapes } from "../dash/render.js";
|
|
5
5
|
import { clipMultiline, DIFF_MAX_TOTAL_CHARS } from "../diff-preview.js";
|
|
6
6
|
import { productLabel, readTrail, readTrailSince } from "../trail.js";
|
|
7
|
-
import { loadIdentity, loadOrrCard, loadSandboxes, loadShowbackCard, loadWiredHosts, } from "./enrich.js";
|
|
7
|
+
import { loadCertifyCard, loadIdentity, loadOrrCard, loadSandboxes, loadShowbackCard, loadWiredHosts, } from "./enrich.js";
|
|
8
8
|
import { SHOWBACK_DISCLAIMER, } from "../showback/cost-per-task.js";
|
|
9
9
|
import { computeDashboard } from "./dashboard.js";
|
|
10
|
+
import { receiptCss } from "./receipt-css.js";
|
|
10
11
|
function esc(s) {
|
|
11
12
|
return stripEscapes(s)
|
|
12
13
|
.replace(/&/g, "&")
|
|
@@ -197,6 +198,7 @@ export function buildReceiptModel(sessionId, events, extras) {
|
|
|
197
198
|
sandboxes: extras?.sandboxes,
|
|
198
199
|
wiredHosts: extras?.wiredHosts,
|
|
199
200
|
orr: extras?.orr,
|
|
201
|
+
certify: extras?.certify,
|
|
200
202
|
showback: extras?.showback,
|
|
201
203
|
};
|
|
202
204
|
}
|
|
@@ -215,6 +217,7 @@ export function buildWindowReceiptModel(events, days, extras) {
|
|
|
215
217
|
sandboxes: extras?.sandboxes,
|
|
216
218
|
wiredHosts: extras?.wiredHosts,
|
|
217
219
|
orr: extras?.orr,
|
|
220
|
+
certify: extras?.certify,
|
|
218
221
|
showback: extras?.showback,
|
|
219
222
|
};
|
|
220
223
|
}
|
|
@@ -229,6 +232,7 @@ export function loadReceiptModel(input) {
|
|
|
229
232
|
sandboxes: loadSandboxes(input.cwd),
|
|
230
233
|
wiredHosts: loadWiredHosts(input.cwd),
|
|
231
234
|
orr: loadOrrCard(input.cwd),
|
|
235
|
+
certify: loadCertifyCard(input.cwd),
|
|
232
236
|
showback,
|
|
233
237
|
spend: showback
|
|
234
238
|
? {
|
|
@@ -261,8 +265,8 @@ function projectMeta(project) {
|
|
|
261
265
|
function renderFeed(events, nowMs, live) {
|
|
262
266
|
if (events.length === 0) {
|
|
263
267
|
const hint = live
|
|
264
|
-
? "No events yet
|
|
265
|
-
: "No events yet
|
|
268
|
+
? "No events yet — evaluate a tool call with <code>kya wrap</code> and this page will refresh."
|
|
269
|
+
: "No events yet — evaluate a tool call with <code>kya wrap</code>, then regenerate this receipt.";
|
|
266
270
|
return `<p class="empty">${hint}</p>`;
|
|
267
271
|
}
|
|
268
272
|
const sorted = [...events].sort((a, b) => b.ts.localeCompare(a.ts));
|
|
@@ -541,7 +545,108 @@ function orrPanel(orr, nowMs) {
|
|
|
541
545
|
function usdText(usd) {
|
|
542
546
|
return usd === null ? "USD n/a" : `~$${usd.toFixed(2)}`;
|
|
543
547
|
}
|
|
544
|
-
|
|
548
|
+
/** KPI tile: big tabular number over a small caps label. The tone class is
|
|
549
|
+
* only applied above zero — a zero count never reads as a signal. */
|
|
550
|
+
function kpiTile(label, value, cls = "") {
|
|
551
|
+
const v = typeof value === "number" ? value : esc(value);
|
|
552
|
+
const toned = typeof value === "number" && value > 0 && cls ? ` ${cls}` : "";
|
|
553
|
+
return `<div class="kpi"><span class="kpi-num${toned}">${v}</span><span class="kpi-lab">${label}</span></div>`;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Hero Certify panel: purpose-built rich card for the live Agent Trust
|
|
557
|
+
* Baseline. The section carries the result state (`hero-certify pass|gap|
|
|
558
|
+
* none`) for the accent border; an absent card is a fail-closed neutral
|
|
559
|
+
* state that must never read as a pass.
|
|
560
|
+
*/
|
|
561
|
+
function heroCertifyPanel(certify) {
|
|
562
|
+
if (!certify) {
|
|
563
|
+
return `<section class="panel hero-certify none" aria-label="Certify">
|
|
564
|
+
<h2>Certify — Agent Trust Baseline</h2>
|
|
565
|
+
<div class="orrline">
|
|
566
|
+
<span class="pill">not evaluated</span>
|
|
567
|
+
</div>
|
|
568
|
+
<p class="mute small">No live evaluation available — wrap tool calls and run kya certify to build the baseline.</p>
|
|
569
|
+
</section>`;
|
|
570
|
+
}
|
|
571
|
+
const state = certify.result === "pass" ? "pass" : "gap";
|
|
572
|
+
// topGaps arrives severity-ordered from the loader; cap defensively at 5.
|
|
573
|
+
const gaps = certify.topGaps.length === 0
|
|
574
|
+
? ""
|
|
575
|
+
: `<ul class="rows">
|
|
576
|
+
${certify.topGaps
|
|
577
|
+
.slice(0, 5)
|
|
578
|
+
.map((g) => ` <li><code>${esc(g.id)}</code> <span class="sev ${esc(g.severity)}">${esc(g.severity)}</span></li>`)
|
|
579
|
+
.join("\n")}
|
|
580
|
+
</ul>`;
|
|
581
|
+
// Pill tones reuse the ORR palette: pass is green, gap is amber.
|
|
582
|
+
return `<section class="panel hero-certify ${state}" aria-label="Certify">
|
|
583
|
+
<h2>Certify — Agent Trust Baseline</h2>
|
|
584
|
+
<div class="orrline">
|
|
585
|
+
<span class="pill orr-${state === "pass" ? "green" : "amber"}">${esc(certify.result)}</span>
|
|
586
|
+
</div>
|
|
587
|
+
<div class="kpi-row">
|
|
588
|
+
${kpiTile("Pass", certify.pass, "ok")}
|
|
589
|
+
${kpiTile("Gap", certify.gap, "warn")}
|
|
590
|
+
${kpiTile("Insufficient", certify.insufficientEvidence, "mute")}
|
|
591
|
+
${kpiTile("Attested", certify.attested, "ok")}
|
|
592
|
+
</div>
|
|
593
|
+
${gaps}
|
|
594
|
+
<p class="mute small">live evaluation — window ${certify.windowDays}d, ${certify.trailEvents} trail events · kya certify for the full gap report + signed bundle</p>
|
|
595
|
+
</section>`;
|
|
596
|
+
}
|
|
597
|
+
/** Hero KPI cards: compact, non-interactive rollups (filters live in the
|
|
598
|
+
* Activity zone's chip bar). Numbers keep tabular-nums via .mix-num/.kpi-num. */
|
|
599
|
+
function verdictsHeroCard(c, total) {
|
|
600
|
+
// Base 100% = total events, matching dashboard.ts's verdictMix contract:
|
|
601
|
+
// verdict is a free-form string from untrusted JSONL, so an event may land
|
|
602
|
+
// in no bucket (bogus verdict) while still counting in `never` — only the
|
|
603
|
+
// raw event count keeps every bar inside the base the Overview card uses.
|
|
604
|
+
const pct = (n) => (total > 0 ? Math.round((n / total) * 100) : 0);
|
|
605
|
+
const mixRow = (label, n, fill) => `<div class="mix-row"><span class="mix-lab">${label}</span><span class="mix-bar"><span class="mix-fill ${fill}" style="width:${pct(n)}%"></span></span><span class="mix-num">${n}</span></div>`;
|
|
606
|
+
return `<section class="panel" aria-label="Verdicts">
|
|
607
|
+
<h2>Verdicts</h2>
|
|
608
|
+
<div class="mix">
|
|
609
|
+
${mixRow("Allow", c.allow, "ok")}
|
|
610
|
+
${mixRow("Deny", c.deny, "bad")}
|
|
611
|
+
${mixRow("Review", c.require, "warn")}
|
|
612
|
+
${mixRow("Never", c.never, "bad")}
|
|
613
|
+
</div>
|
|
614
|
+
</section>`;
|
|
615
|
+
}
|
|
616
|
+
const SPARK_BARS = 7;
|
|
617
|
+
const SPARK_H = 24;
|
|
618
|
+
/**
|
|
619
|
+
* Inline 7-bar sparkline (no JS): the last 7 activity buckets, left-padded
|
|
620
|
+
* with zeros, heights normalized so the max bucket is full height. Empty
|
|
621
|
+
* input renders a flat baseline — heights are guarded, never NaN/Infinity.
|
|
622
|
+
*/
|
|
623
|
+
function activitySparkline(activity) {
|
|
624
|
+
const counts = activity.buckets.slice(-SPARK_BARS).map((b) => b.count);
|
|
625
|
+
while (counts.length < SPARK_BARS)
|
|
626
|
+
counts.unshift(0);
|
|
627
|
+
const max = Math.max(0, ...counts);
|
|
628
|
+
const rects = counts
|
|
629
|
+
.map((n, i) => {
|
|
630
|
+
const h = max > 0 ? Math.round((n / max) * SPARK_H) : 0;
|
|
631
|
+
return `<rect x="${i * 10}" y="${SPARK_H - h}" width="8" height="${h}" rx="1"></rect>`;
|
|
632
|
+
})
|
|
633
|
+
.join("");
|
|
634
|
+
const unit = activity.granularity === "hour" ? "hours" : "days";
|
|
635
|
+
return `<svg class="spark" viewBox="0 0 68 ${SPARK_H}" role="img" aria-label="Activity, last ${SPARK_BARS} ${unit}">${rects}<line class="base" x1="0" y1="${SPARK_H - 0.5}" x2="68" y2="${SPARK_H - 0.5}"></line></svg>`;
|
|
636
|
+
}
|
|
637
|
+
function activityHeroCard(agg, db, total) {
|
|
638
|
+
return `<section class="panel" aria-label="Activity">
|
|
639
|
+
<h2>Activity</h2>
|
|
640
|
+
<div class="kpi-row">
|
|
641
|
+
${kpiTile("Events", total)}
|
|
642
|
+
</div>
|
|
643
|
+
${activitySparkline(db.activity)}
|
|
644
|
+
<p class="mute small">observe ${agg.modes.observe} · hold ${agg.modes.hold} · offline ${agg.modes.offline} · IDE ${agg.planes.ide} · runtime ${agg.planes.runtime}</p>
|
|
645
|
+
</section>`;
|
|
646
|
+
}
|
|
647
|
+
/** Hero-styled showback: same fields as the old panel (tokens in/out, est.
|
|
648
|
+
* USD, top runs, disclaimer), omitted entirely when no report exists. */
|
|
649
|
+
function showbackHeroCard(report) {
|
|
545
650
|
if (!report)
|
|
546
651
|
return "";
|
|
547
652
|
const topRuns = [...report.perRun]
|
|
@@ -556,7 +661,11 @@ ${topRuns
|
|
|
556
661
|
</ul>`;
|
|
557
662
|
return `<section class="panel" aria-label="Showback">
|
|
558
663
|
<h2>Showback (observe only)</h2>
|
|
559
|
-
<
|
|
664
|
+
<div class="kpi-row">
|
|
665
|
+
${kpiTile("Tokens in", report.totalTokensIn)}
|
|
666
|
+
${kpiTile("Tokens out", report.totalTokensOut)}
|
|
667
|
+
${kpiTile("Est. USD", usdText(report.estimatedUsd))}
|
|
668
|
+
</div>
|
|
560
669
|
${runs}
|
|
561
670
|
<p class="mute small">${esc(SHOWBACK_DISCLAIMER)}</p>
|
|
562
671
|
</section>`;
|
|
@@ -612,6 +721,12 @@ export function renderReceiptHtml(model) {
|
|
|
612
721
|
// must never resolve via Object.prototype. The id lets tests extract just
|
|
613
722
|
// this script. KNOWN is space-delimited so `indexOf(' '+g+' ')` doubles as a
|
|
614
723
|
// prototype-safe whitelist ('constructor' etc. never match).
|
|
724
|
+
// Filter state persists in the ?f= QUERY param, not the hash: the hash is
|
|
725
|
+
// owned by the pure-CSS :target tabs (#overview/#activity/#system), so a
|
|
726
|
+
// hash-based filter would hide the Activity zone on every chip toggle and
|
|
727
|
+
// every tab click would wipe the filter. Legacy #f= hashes are still parsed
|
|
728
|
+
// on load (read-only); the next save writes the ?f= form and drops the
|
|
729
|
+
// legacy fragment.
|
|
615
730
|
const filterScript = `<script id="kya-filters">
|
|
616
731
|
(function(){
|
|
617
732
|
var feed = document.getElementById('feed');
|
|
@@ -622,9 +737,20 @@ export function renderReceiptHtml(model) {
|
|
|
622
737
|
var chips = document.querySelectorAll('[data-fgroup]');
|
|
623
738
|
function parse(){
|
|
624
739
|
state = Object.create(null);
|
|
625
|
-
var
|
|
626
|
-
|
|
627
|
-
|
|
740
|
+
var raw = null;
|
|
741
|
+
var s = location.search;
|
|
742
|
+
if (s.indexOf('?') === 0) {
|
|
743
|
+
var params = s.slice(1).split('&');
|
|
744
|
+
for (var i = 0; i < params.length; i++) {
|
|
745
|
+
if (params[i].indexOf('f=') === 0) { raw = params[i].slice(2); break; }
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (raw === null) {
|
|
749
|
+
var legacy = location.hash;
|
|
750
|
+
if (legacy.indexOf('#f=') === 0) raw = legacy.slice(3);
|
|
751
|
+
}
|
|
752
|
+
if (raw === null) return;
|
|
753
|
+
raw.split(',').forEach(function(seg){
|
|
628
754
|
var i = seg.indexOf(':');
|
|
629
755
|
if (i < 1) return;
|
|
630
756
|
var g = seg.slice(0, i);
|
|
@@ -640,8 +766,22 @@ export function renderReceiptHtml(model) {
|
|
|
640
766
|
Object.keys(state).forEach(function(g){
|
|
641
767
|
Object.keys(state[g]).forEach(function(v){ parts.push(g + ':' + encodeURIComponent(v)); });
|
|
642
768
|
});
|
|
643
|
-
|
|
644
|
-
|
|
769
|
+
// Rebuild the query rather than overwrite it: the live page carries its
|
|
770
|
+
// loopback token as ?t= and must not lose it on a chip toggle.
|
|
771
|
+
var kept = [];
|
|
772
|
+
var s = location.search;
|
|
773
|
+
if (s.indexOf('?') === 0) {
|
|
774
|
+
var params = s.slice(1).split('&');
|
|
775
|
+
for (var i = 0; i < params.length; i++) {
|
|
776
|
+
if (params[i] && params[i].indexOf('f=') !== 0) kept.push(params[i]);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (parts.length) kept.push('f=' + parts.join(','));
|
|
780
|
+
var q = kept.length ? '?' + kept.join('&') : '';
|
|
781
|
+
// The tab hash rides along untouched; a legacy #f= fragment is not a tab
|
|
782
|
+
// hash and is dropped now that the state lives in the query.
|
|
783
|
+
var h = location.hash.indexOf('#f=') === 0 ? '' : location.hash;
|
|
784
|
+
history.replaceState(null, '', location.pathname + q + h);
|
|
645
785
|
}
|
|
646
786
|
function active(){
|
|
647
787
|
return Object.keys(state).some(function(g){ return Object.keys(state[g]).length > 0; });
|
|
@@ -689,405 +829,71 @@ export function renderReceiptHtml(model) {
|
|
|
689
829
|
})();
|
|
690
830
|
</script>`;
|
|
691
831
|
const livePill = model.live ? `<span class="live" title="Watching trail.jsonl">Live</span>` : "";
|
|
832
|
+
// Zones never render as bare whitespace: with no panels they get a hint.
|
|
833
|
+
const zoneBody = (parts, empty) => parts.some((p) => p.length > 0)
|
|
834
|
+
? parts.filter(Boolean).join("\n ")
|
|
835
|
+
: `<p class="mute small zone-empty">${empty}</p>`;
|
|
836
|
+
const overviewBody = zoneBody([dashboardPanel(dashboard), sessionsPanel(agg, nowMs), reasonsPanel(agg)], "No activity in this window yet — analytics appear once events land.");
|
|
837
|
+
const systemBody = zoneBody([
|
|
838
|
+
wiredHostsPanel(model.wiredHosts),
|
|
839
|
+
sandboxesPanel(model.sandboxes, nowMs),
|
|
840
|
+
orrPanel(model.orr, nowMs),
|
|
841
|
+
toolsHtml,
|
|
842
|
+
], "No system state yet — wire a host, configure a sandbox, or run an ORR.");
|
|
692
843
|
const html = `<!DOCTYPE html>
|
|
693
844
|
<html lang="en">
|
|
694
845
|
<head>
|
|
695
846
|
<meta charset="utf-8"/>
|
|
696
847
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
697
848
|
<title>${esc(model.title)} - ${esc(model.rangeLabel)}</title>
|
|
698
|
-
<style>
|
|
699
|
-
:root {
|
|
700
|
-
color-scheme: dark;
|
|
701
|
-
--bg: #0a0c10;
|
|
702
|
-
--fg: #eef2f7;
|
|
703
|
-
--mute: #8b95a8;
|
|
704
|
-
--line: #1c2330;
|
|
705
|
-
--card: #10141c;
|
|
706
|
-
--ok: #3ecf8e;
|
|
707
|
-
--bad: #ff5d5d;
|
|
708
|
-
--warn: #f0b429;
|
|
709
|
-
--rail: #2a3344;
|
|
710
|
-
--day: #6b7280;
|
|
711
|
-
}
|
|
712
|
-
@media (prefers-color-scheme: light) {
|
|
713
|
-
:root {
|
|
714
|
-
color-scheme: light;
|
|
715
|
-
--bg: #f3f5f8;
|
|
716
|
-
--fg: #0f172a;
|
|
717
|
-
--mute: #64748b;
|
|
718
|
-
--line: #e2e8f0;
|
|
719
|
-
--card: #ffffff;
|
|
720
|
-
--ok: #059669;
|
|
721
|
-
--bad: #dc2626;
|
|
722
|
-
--warn: #d97706;
|
|
723
|
-
--rail: #cbd5e1;
|
|
724
|
-
--day: #64748b;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
* { box-sizing: border-box; }
|
|
728
|
-
body {
|
|
729
|
-
margin: 0;
|
|
730
|
-
font-family: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
731
|
-
background: var(--bg);
|
|
732
|
-
color: var(--fg);
|
|
733
|
-
line-height: 1.4;
|
|
734
|
-
}
|
|
735
|
-
main { max-width: 42rem; margin: 0 auto; padding: 1.35rem 1rem 3rem; }
|
|
736
|
-
header.bar {
|
|
737
|
-
position: sticky; top: 0; z-index: 3;
|
|
738
|
-
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
739
|
-
backdrop-filter: blur(10px);
|
|
740
|
-
padding: 0.85rem 0 0.75rem;
|
|
741
|
-
border-bottom: 1px solid var(--line);
|
|
742
|
-
margin-bottom: 1rem;
|
|
743
|
-
}
|
|
744
|
-
.title-row {
|
|
745
|
-
display: flex; align-items: baseline; justify-content: space-between;
|
|
746
|
-
gap: 0.75rem; flex-wrap: wrap;
|
|
747
|
-
}
|
|
748
|
-
h1 {
|
|
749
|
-
margin: 0; font-size: 1.2rem; font-weight: 600;
|
|
750
|
-
letter-spacing: -0.025em;
|
|
751
|
-
}
|
|
752
|
-
.range { color: var(--mute); font-size: 0.85rem; display: inline-flex; align-items: center; gap: 0.5rem; }
|
|
753
|
-
.live {
|
|
754
|
-
display: inline-flex; align-items: center; gap: 0.35rem;
|
|
755
|
-
font-size: 0.7rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
|
|
756
|
-
color: var(--ok); border: 1px solid color-mix(in srgb, var(--ok) 40%, transparent);
|
|
757
|
-
border-radius: 999px; padding: 0.12rem 0.5rem;
|
|
758
|
-
}
|
|
759
|
-
.live::before {
|
|
760
|
-
content: ""; width: 0.4rem; height: 0.4rem; border-radius: 50%;
|
|
761
|
-
background: var(--ok);
|
|
762
|
-
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent);
|
|
763
|
-
animation: pulse 1.6s ease-in-out infinite;
|
|
764
|
-
}
|
|
765
|
-
.live.off { color: var(--mute); border-color: var(--line); }
|
|
766
|
-
.live.off::before { background: var(--mute); box-shadow: none; animation: none; }
|
|
767
|
-
@keyframes pulse {
|
|
768
|
-
0%, 100% { opacity: 1; }
|
|
769
|
-
50% { opacity: 0.45; }
|
|
770
|
-
}
|
|
771
|
-
@media (prefers-reduced-motion: reduce) {
|
|
772
|
-
.live::before { animation: none; }
|
|
773
|
-
}
|
|
774
|
-
.stats {
|
|
775
|
-
display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.7rem;
|
|
776
|
-
}
|
|
777
|
-
.stat {
|
|
778
|
-
font-size: 0.75rem; font-variant-numeric: tabular-nums;
|
|
779
|
-
color: var(--mute); background: var(--card);
|
|
780
|
-
border: 1px solid var(--line); border-radius: 999px;
|
|
781
|
-
padding: 0.2rem 0.6rem;
|
|
782
|
-
}
|
|
783
|
-
.stat b { color: var(--fg); font-weight: 650; margin-left: 0.25rem; }
|
|
784
|
-
.stat.bad b { color: var(--bad); }
|
|
785
|
-
.stat.warn b { color: var(--warn); }
|
|
786
|
-
.stat.ok b { color: var(--ok); }
|
|
787
|
-
button.stat {
|
|
788
|
-
font-family: inherit; cursor: pointer;
|
|
789
|
-
appearance: none; -webkit-appearance: none;
|
|
790
|
-
}
|
|
791
|
-
button.stat:hover { border-color: var(--mute); }
|
|
792
|
-
.stat[aria-pressed="true"] {
|
|
793
|
-
color: var(--fg);
|
|
794
|
-
border-color: color-mix(in srgb, var(--ok) 55%, var(--line));
|
|
795
|
-
background: color-mix(in srgb, var(--ok) 12%, var(--card));
|
|
796
|
-
}
|
|
797
|
-
.stat[hidden] { display: none; }
|
|
798
|
-
.clear-filters { color: var(--mute); font-size: 0.72rem; }
|
|
799
|
-
/* Analytics dashboard: 2×2 card grid, collapses to one column on narrow. */
|
|
800
|
-
.db-grid {
|
|
801
|
-
display: grid; grid-template-columns: 1fr 1fr; gap: 0.9rem 1.1rem;
|
|
802
|
-
}
|
|
803
|
-
@media (max-width: 700px) {
|
|
804
|
-
.db-grid { grid-template-columns: 1fr; }
|
|
805
|
-
}
|
|
806
|
-
.db-card { min-width: 0; }
|
|
807
|
-
.db-card h3 {
|
|
808
|
-
margin: 0 0 0.4rem; font-size: 0.68rem; font-weight: 700;
|
|
809
|
-
letter-spacing: 0.06em; text-transform: uppercase; color: var(--mute);
|
|
810
|
-
}
|
|
811
|
-
.db-card h3 .mute { text-transform: none; letter-spacing: 0.02em; font-weight: 600; }
|
|
812
|
-
.db-card .sub {
|
|
813
|
-
margin: 0.35rem 0 0.15rem; font-size: 0.66rem; font-weight: 700;
|
|
814
|
-
letter-spacing: 0.05em; text-transform: uppercase; color: var(--day);
|
|
815
|
-
}
|
|
816
|
-
/* Filter-linked bar row: block variant of .stat's hover/active contract. */
|
|
817
|
-
.db-item {
|
|
818
|
-
display: grid; grid-template-columns: 6.5rem 1fr 2.4rem;
|
|
819
|
-
gap: 0.5rem; align-items: center; width: 100%;
|
|
820
|
-
font-family: inherit; font-size: 0.76rem; font-variant-numeric: tabular-nums;
|
|
821
|
-
color: var(--mute); text-align: left;
|
|
822
|
-
background: transparent; border: 1px solid transparent; border-radius: 8px;
|
|
823
|
-
padding: 0.14rem 0.3rem; cursor: pointer;
|
|
824
|
-
appearance: none; -webkit-appearance: none;
|
|
825
|
-
}
|
|
826
|
-
.db-item:hover { border-color: var(--line); background: color-mix(in srgb, var(--bg) 55%, var(--card)); }
|
|
827
|
-
.db-item[aria-pressed="true"] {
|
|
828
|
-
color: var(--fg);
|
|
829
|
-
border-color: color-mix(in srgb, var(--ok) 55%, var(--line));
|
|
830
|
-
background: color-mix(in srgb, var(--ok) 12%, var(--card));
|
|
831
|
-
}
|
|
832
|
-
.db-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
833
|
-
.db-bar {
|
|
834
|
-
height: 0.55rem; border-radius: 4px; overflow: hidden;
|
|
835
|
-
background: color-mix(in srgb, var(--bg) 55%, var(--card));
|
|
836
|
-
}
|
|
837
|
-
.db-fill { display: block; height: 100%; border-radius: 4px; }
|
|
838
|
-
.db-fill.ok { background: var(--ok); }
|
|
839
|
-
.db-fill.warn { background: var(--warn); }
|
|
840
|
-
.db-fill.bad { background: var(--bad); }
|
|
841
|
-
.db-num { text-align: right; color: var(--fg); }
|
|
842
|
-
/* Vertical mini-bar timeline (not clickable — no time filter exists). */
|
|
843
|
-
.db-tl { display: flex; align-items: stretch; gap: 2px; height: 4.6rem; }
|
|
844
|
-
.db-tl .col {
|
|
845
|
-
flex: 1 1 0; min-width: 0; display: flex; flex-direction: column;
|
|
846
|
-
align-items: center; justify-content: flex-end; gap: 0.15rem;
|
|
847
|
-
}
|
|
848
|
-
.db-vbar {
|
|
849
|
-
width: 100%; min-height: 2px; border-radius: 2px 2px 0 0;
|
|
850
|
-
background: color-mix(in srgb, var(--ok) 75%, var(--mute));
|
|
851
|
-
}
|
|
852
|
-
.db-tl .lab {
|
|
853
|
-
font-size: 0.52rem; color: var(--mute); white-space: nowrap;
|
|
854
|
-
overflow: hidden; max-width: 100%; font-variant-numeric: tabular-nums;
|
|
855
|
-
}
|
|
856
|
-
/* utility: must beat .ev's display:grid (same specificity, later rule would win) */
|
|
857
|
-
.filtered-out { display: none !important; }
|
|
858
|
-
.identity {
|
|
859
|
-
margin-top: 0.45rem; color: var(--mute); font-size: 0.78rem;
|
|
860
|
-
word-break: break-word;
|
|
861
|
-
}
|
|
862
|
-
.modeb {
|
|
863
|
-
display: inline-block; font-size: 0.6rem; font-weight: 700;
|
|
864
|
-
color: var(--mute); border: 1px solid var(--line); border-radius: 4px;
|
|
865
|
-
padding: 0 0.28rem; line-height: 1.1rem; vertical-align: baseline;
|
|
866
|
-
}
|
|
867
|
-
.panel {
|
|
868
|
-
background: var(--card); border: 1px solid var(--line);
|
|
869
|
-
border-radius: 12px; padding: 0.7rem 0.9rem 0.8rem; margin-bottom: 1rem;
|
|
870
|
-
}
|
|
871
|
-
.panel h2 {
|
|
872
|
-
margin: 0 0 0.5rem; font-size: 0.72rem; font-weight: 700;
|
|
873
|
-
letter-spacing: 0.07em; text-transform: uppercase; color: var(--day);
|
|
874
|
-
}
|
|
875
|
-
.panel .sub { margin: 0 0 0.45rem; color: var(--mute); font-size: 0.8rem; }
|
|
876
|
-
.panel .line { margin: 0.25rem 0; font-size: 0.84rem; word-break: break-word; }
|
|
877
|
-
.panel .mute { color: var(--mute); }
|
|
878
|
-
.panel .small { font-size: 0.74rem; }
|
|
879
|
-
.rows {
|
|
880
|
-
list-style: none; margin: 0; padding: 0;
|
|
881
|
-
display: flex; flex-direction: column; gap: 0.3rem;
|
|
882
|
-
font-size: 0.82rem;
|
|
883
|
-
}
|
|
884
|
-
.rows li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.45rem; min-width: 0; }
|
|
885
|
-
.rows li.mute { color: var(--mute); }
|
|
886
|
-
.rows .sid, .rows code {
|
|
887
|
-
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
888
|
-
font-size: 0.78rem; word-break: break-all;
|
|
889
|
-
}
|
|
890
|
-
.rows .cnt { color: var(--mute); font-size: 0.76rem; }
|
|
891
|
-
.rows .rel { margin-left: auto; color: var(--mute); font-size: 0.74rem; white-space: nowrap; }
|
|
892
|
-
.rows .hint {
|
|
893
|
-
font-size: 0.72rem; border-bottom: 1px dotted var(--mute); cursor: help;
|
|
894
|
-
}
|
|
895
|
-
.wdot {
|
|
896
|
-
width: 0.5rem; height: 0.5rem; border-radius: 50%; flex: none;
|
|
897
|
-
align-self: center; background: var(--mute);
|
|
898
|
-
}
|
|
899
|
-
.wdot.allow { background: var(--ok); }
|
|
900
|
-
.wdot.hold { background: var(--warn); }
|
|
901
|
-
.wdot.deny, .wdot.never { background: var(--bad); }
|
|
902
|
-
.chiprow { display: flex; flex-wrap: wrap; gap: 0.35rem; }
|
|
903
|
-
.chip {
|
|
904
|
-
font-size: 0.76rem; color: var(--mute);
|
|
905
|
-
background: color-mix(in srgb, var(--bg) 55%, var(--card));
|
|
906
|
-
border: 1px solid var(--line); border-radius: 999px; padding: 0.15rem 0.55rem;
|
|
907
|
-
}
|
|
908
|
-
.chip code {
|
|
909
|
-
font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
|
|
910
|
-
color: var(--fg); font-size: 0.74rem;
|
|
911
|
-
}
|
|
912
|
-
.tbl { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
|
|
913
|
-
.tbl th {
|
|
914
|
-
text-align: left; font-size: 0.66rem; font-weight: 700; letter-spacing: 0.06em;
|
|
915
|
-
text-transform: uppercase; color: var(--mute); padding: 0.15rem 0.5rem 0.3rem 0;
|
|
916
|
-
}
|
|
917
|
-
.tbl td { padding: 0.2rem 0.5rem 0.2rem 0; border-top: 1px solid var(--line); }
|
|
918
|
-
.tbl code {
|
|
919
|
-
font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace; font-size: 0.76rem;
|
|
920
|
-
}
|
|
921
|
-
.pill {
|
|
922
|
-
display: inline-block; font-size: 0.7rem; font-weight: 700;
|
|
923
|
-
letter-spacing: 0.04em; text-transform: uppercase;
|
|
924
|
-
border-radius: 999px; padding: 0.1rem 0.55rem;
|
|
925
|
-
border: 1px solid var(--line); color: var(--mute);
|
|
926
|
-
}
|
|
927
|
-
.pill.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
|
|
928
|
-
.pill.orr-green { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
|
|
929
|
-
.pill.orr-amber { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
|
|
930
|
-
.pill.orr-red { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, transparent); }
|
|
931
|
-
.orrline { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.55rem; }
|
|
932
|
-
.orrline .disp { font-size: 0.84rem; font-weight: 600; }
|
|
933
|
-
.alert {
|
|
934
|
-
display: flex; flex-wrap: wrap; gap: 0.45rem 0.75rem; align-items: baseline;
|
|
935
|
-
background: color-mix(in srgb, var(--bad) 12%, var(--card));
|
|
936
|
-
border: 1px solid color-mix(in srgb, var(--bad) 35%, var(--line));
|
|
937
|
-
border-radius: 10px; padding: 0.65rem 0.85rem; margin-bottom: 1rem;
|
|
938
|
-
font-size: 0.85rem;
|
|
939
|
-
}
|
|
940
|
-
.alert strong { color: var(--bad); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
941
|
-
.alert code {
|
|
942
|
-
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
943
|
-
font-size: 0.78rem;
|
|
944
|
-
}
|
|
945
|
-
.feed {
|
|
946
|
-
background: var(--card);
|
|
947
|
-
border: 1px solid var(--line);
|
|
948
|
-
border-radius: 12px;
|
|
949
|
-
overflow: hidden;
|
|
950
|
-
}
|
|
951
|
-
.day {
|
|
952
|
-
padding: 0.55rem 0.9rem 0.35rem;
|
|
953
|
-
font-size: 0.68rem; font-weight: 700; letter-spacing: 0.08em;
|
|
954
|
-
text-transform: uppercase; color: var(--day);
|
|
955
|
-
background: color-mix(in srgb, var(--bg) 55%, var(--card));
|
|
956
|
-
border-bottom: 1px solid var(--line);
|
|
957
|
-
}
|
|
958
|
-
.ev {
|
|
959
|
-
display: grid; grid-template-columns: 1.1rem 1fr; gap: 0;
|
|
960
|
-
border-bottom: 1px solid var(--line);
|
|
961
|
-
}
|
|
962
|
-
.ev:last-child { border-bottom: none; }
|
|
963
|
-
.rail {
|
|
964
|
-
position: relative;
|
|
965
|
-
display: flex; justify-content: center;
|
|
966
|
-
padding-top: 1.05rem;
|
|
967
|
-
}
|
|
968
|
-
.rail::before {
|
|
969
|
-
content: ""; position: absolute; top: 0; bottom: 0; left: 50%;
|
|
970
|
-
width: 1px; background: var(--rail); transform: translateX(-50%);
|
|
971
|
-
}
|
|
972
|
-
.tick {
|
|
973
|
-
width: 0.55rem; height: 0.55rem; border-radius: 50%;
|
|
974
|
-
background: var(--mute); position: relative; z-index: 1;
|
|
975
|
-
box-shadow: 0 0 0 3px var(--card);
|
|
976
|
-
}
|
|
977
|
-
.ev.ok .tick { background: var(--ok); }
|
|
978
|
-
.ev.bad .tick { background: var(--bad); }
|
|
979
|
-
.ev.warn .tick { background: var(--warn); }
|
|
980
|
-
.ev.never { background: color-mix(in srgb, var(--bad) 7%, transparent); }
|
|
981
|
-
.main { padding: 0.7rem 0.85rem 0.7rem 0.35rem; min-width: 0; }
|
|
982
|
-
.top {
|
|
983
|
-
display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 0.55rem;
|
|
984
|
-
}
|
|
985
|
-
@media (max-width: 480px) {
|
|
986
|
-
.when { flex-basis: 100%; margin-left: 0; margin-top: 0.1rem; }
|
|
987
|
-
}
|
|
988
|
-
.verdict {
|
|
989
|
-
font-size: 0.68rem; font-weight: 750; letter-spacing: 0.05em;
|
|
990
|
-
min-width: 2.6rem;
|
|
991
|
-
}
|
|
992
|
-
.ev.ok .verdict { color: var(--ok); }
|
|
993
|
-
.ev.bad .verdict { color: var(--bad); }
|
|
994
|
-
.ev.warn .verdict { color: var(--warn); }
|
|
995
|
-
.tool {
|
|
996
|
-
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
997
|
-
font-size: 0.84rem; word-break: break-word; flex: 1 1 auto;
|
|
998
|
-
}
|
|
999
|
-
.when {
|
|
1000
|
-
margin-left: auto; color: var(--mute); font-size: 0.72rem;
|
|
1001
|
-
font-variant-numeric: tabular-nums; white-space: nowrap;
|
|
1002
|
-
}
|
|
1003
|
-
.when .rel { opacity: 0.85; margin-left: 0.35rem; }
|
|
1004
|
-
.summary {
|
|
1005
|
-
margin-top: 0.2rem;
|
|
1006
|
-
font-size: 0.84rem;
|
|
1007
|
-
color: var(--fg);
|
|
1008
|
-
word-break: break-word;
|
|
1009
|
-
}
|
|
1010
|
-
details.diff {
|
|
1011
|
-
margin-top: 0.35rem;
|
|
1012
|
-
border: 1px solid var(--line);
|
|
1013
|
-
border-radius: 8px;
|
|
1014
|
-
background: color-mix(in srgb, var(--bg) 55%, var(--card));
|
|
1015
|
-
overflow: hidden;
|
|
1016
|
-
}
|
|
1017
|
-
details.diff > summary {
|
|
1018
|
-
cursor: pointer;
|
|
1019
|
-
list-style: none;
|
|
1020
|
-
padding: 0.35rem 0.55rem;
|
|
1021
|
-
font-size: 0.72rem;
|
|
1022
|
-
font-weight: 650;
|
|
1023
|
-
letter-spacing: 0.04em;
|
|
1024
|
-
text-transform: uppercase;
|
|
1025
|
-
color: var(--mute);
|
|
1026
|
-
}
|
|
1027
|
-
details.diff > summary::-webkit-details-marker { display: none; }
|
|
1028
|
-
details.diff pre {
|
|
1029
|
-
margin: 0;
|
|
1030
|
-
padding: 0.45rem 0.55rem 0.6rem;
|
|
1031
|
-
border-top: 1px solid var(--line);
|
|
1032
|
-
font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
|
|
1033
|
-
font-size: 0.72rem;
|
|
1034
|
-
line-height: 1.35;
|
|
1035
|
-
white-space: pre-wrap;
|
|
1036
|
-
word-break: break-word;
|
|
1037
|
-
color: var(--fg);
|
|
1038
|
-
}
|
|
1039
|
-
.meta {
|
|
1040
|
-
margin-top: 0.18rem; color: var(--mute); font-size: 0.76rem;
|
|
1041
|
-
display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: baseline;
|
|
1042
|
-
}
|
|
1043
|
-
.dot { opacity: 0.55; }
|
|
1044
|
-
.empty { margin: 0; padding: 1.25rem 1rem; color: var(--mute); font-size: 0.9rem; }
|
|
1045
|
-
.tools {
|
|
1046
|
-
margin-top: 1.1rem; display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center;
|
|
1047
|
-
}
|
|
1048
|
-
.tools .label {
|
|
1049
|
-
font-size: 0.68rem; font-weight: 700; letter-spacing: 0.06em;
|
|
1050
|
-
text-transform: uppercase; color: var(--mute); margin-right: 0.25rem;
|
|
1051
|
-
}
|
|
1052
|
-
.tools code {
|
|
1053
|
-
font-family: "IBM Plex Mono", ui-monospace, Menlo, monospace;
|
|
1054
|
-
font-size: 0.72rem; color: var(--mute);
|
|
1055
|
-
background: var(--card); border: 1px solid var(--line);
|
|
1056
|
-
border-radius: 6px; padding: 0.18rem 0.4rem;
|
|
1057
|
-
}
|
|
1058
|
-
</style>
|
|
849
|
+
<style>${receiptCss()}</style>
|
|
1059
850
|
</head>
|
|
1060
851
|
<body>
|
|
1061
852
|
<main>
|
|
1062
|
-
<
|
|
1063
|
-
<
|
|
1064
|
-
<
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
<
|
|
1072
|
-
<
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
${chips.products}
|
|
1077
|
-
${chips.projects}
|
|
1078
|
-
</header>
|
|
853
|
+
<div class="topstick">
|
|
854
|
+
<header class="bar">
|
|
855
|
+
<div class="title-row">
|
|
856
|
+
<h1>${esc(model.title)}</h1>
|
|
857
|
+
<div class="range">${esc(model.rangeLabel)}${livePill}</div>
|
|
858
|
+
</div>
|
|
859
|
+
${identityLine(model.identity)}
|
|
860
|
+
</header>
|
|
861
|
+
<nav class="tabs" aria-label="Report sections">
|
|
862
|
+
<a class="tab" id="tab-overview" href="#overview">Overview</a>
|
|
863
|
+
<a class="tab" id="tab-activity" href="#activity">Activity</a>
|
|
864
|
+
<a class="tab" id="tab-system" href="#system">System</a>
|
|
865
|
+
</nav>
|
|
866
|
+
</div>
|
|
1079
867
|
${blockedBanner}
|
|
1080
|
-
|
|
1081
|
-
|
|
868
|
+
<div class="hero">
|
|
869
|
+
${heroCertifyPanel(model.certify)}
|
|
870
|
+
${verdictsHeroCard(c, events.length)}
|
|
871
|
+
${activityHeroCard(agg, dashboard, events.length)}
|
|
872
|
+
${showbackHeroCard(model.showback)}
|
|
873
|
+
</div>
|
|
874
|
+
<section class="zone" id="overview" aria-labelledby="tab-overview">
|
|
875
|
+
${overviewBody}
|
|
876
|
+
</section>
|
|
877
|
+
<section class="zone" id="activity" aria-labelledby="tab-activity">
|
|
878
|
+
<div class="feed-head">
|
|
879
|
+
<div class="stats" aria-label="Counts">
|
|
880
|
+
<button type="button" class="stat ok" data-fgroup="verdict" data-fvalue="ALLOW" aria-pressed="false" title="Filter: Allow">Allow<b>${c.allow}</b></button>
|
|
881
|
+
<button type="button" class="stat bad" data-fgroup="verdict" data-fvalue="DENY" aria-pressed="false" title="Filter: Deny">Deny<b>${c.deny}</b></button>
|
|
882
|
+
<button type="button" class="stat warn" data-fgroup="verdict" data-fvalue="REQUIRE_APPROVE" aria-pressed="false" title="Filter: Review">Review<b>${c.require}</b></button>
|
|
883
|
+
<button type="button" class="stat" data-fgroup="never" data-fvalue="1" aria-pressed="false" title="Filter: Never">Never<b>${c.never}</b></button>
|
|
884
|
+
${chips.modes}${chips.planes}
|
|
885
|
+
<button type="button" class="stat clear-filters" id="clear-filters" hidden>Clear filters ×</button>
|
|
886
|
+
</div>
|
|
887
|
+
${chips.products}
|
|
888
|
+
${chips.projects}
|
|
889
|
+
</div>
|
|
890
|
+
<section class="feed" id="feed" aria-label="Activity feed">
|
|
1082
891
|
${renderFeed(events, nowMs, model.live)}
|
|
892
|
+
</section>
|
|
893
|
+
</section>
|
|
894
|
+
<section class="zone" id="system" aria-labelledby="tab-system">
|
|
895
|
+
${systemBody}
|
|
1083
896
|
</section>
|
|
1084
|
-
${sessionsPanel(agg, nowMs)}
|
|
1085
|
-
${reasonsPanel(agg)}
|
|
1086
|
-
${wiredHostsPanel(model.wiredHosts)}
|
|
1087
|
-
${sandboxesPanel(model.sandboxes, nowMs)}
|
|
1088
|
-
${orrPanel(model.orr, nowMs)}
|
|
1089
|
-
${showbackPanel(model.showback)}
|
|
1090
|
-
${toolsHtml}
|
|
1091
897
|
</main>
|
|
1092
898
|
${filterScript}
|
|
1093
899
|
${liveScript}
|
|
@@ -1137,6 +943,17 @@ export function renderReceiptMarkdown(model) {
|
|
|
1137
943
|
if (agg.projects.length >= 2) {
|
|
1138
944
|
lines.push("## Projects", ...agg.projects.map((p) => `- ${mdText(p.label)} × ${p.count}`), "");
|
|
1139
945
|
}
|
|
946
|
+
if (model.certify) {
|
|
947
|
+
const c = model.certify;
|
|
948
|
+
lines.push("## Certify", `result: ${c.result} · counts: ${c.pass} pass · ${c.gap} gap · ${c.insufficientEvidence} insufficient · ${c.attested} attested`);
|
|
949
|
+
if (c.topGaps.length > 0) {
|
|
950
|
+
lines.push("Top gaps:");
|
|
951
|
+
for (const g of c.topGaps) {
|
|
952
|
+
lines.push(`- \`${mdInline(g.id)}\` — ${mdText(g.severity)}`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
lines.push(`live evaluation — window ${c.windowDays}d, ${c.trailEvents} trail events · kya certify for the full gap report + signed bundle`, "");
|
|
956
|
+
}
|
|
1140
957
|
const db = computeDashboard(model.events);
|
|
1141
958
|
if (db.verdictMix.total > 0) {
|
|
1142
959
|
const pct = (n) => Math.round((n / db.verdictMix.total) * 100);
|