@iris-eval/mcp-server 0.5.1 → 0.7.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/README.md +100 -34
- package/dist/config/defaults.js +3 -1
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +33 -7
- package/dist/dashboard/assets/index-CKs2Wbd_.js +10 -0
- package/dist/dashboard/assets/{index-UffZ-aEJ.css → index-D0cFfBqn.css} +1 -1
- package/dist/dashboard/index.html +4 -3
- package/dist/dashboard/routes/health.js +10 -3
- package/dist/dashboard/routes/moments.js +1 -1
- package/dist/dashboard/routes/preferences.d.ts +1 -0
- package/dist/dashboard/routes/preferences.js +31 -3
- package/dist/dashboard/routes/rules.d.ts +18 -0
- package/dist/dashboard/routes/rules.js +160 -6
- package/dist/dashboard/routes/traces.js +27 -3
- package/dist/dashboard/seed-demo-data.js +11 -0
- package/dist/dashboard/server.js +13 -3
- package/dist/dashboard/session-auth.d.ts +8 -0
- package/dist/dashboard/session-auth.js +237 -0
- package/dist/dashboard/validation.d.ts +10 -4
- package/dist/dashboard/validation.js +73 -11
- package/dist/eval/engine.d.ts +79 -1
- package/dist/eval/engine.js +216 -82
- package/dist/eval/rules/relevance.d.ts +13 -0
- package/dist/eval/rules/relevance.js +185 -21
- package/dist/eval/rules/safety.d.ts +19 -0
- package/dist/eval/rules/safety.js +236 -24
- package/dist/index.js +102 -16
- package/dist/middleware/rate-limit.d.ts +25 -0
- package/dist/middleware/rate-limit.js +54 -2
- package/dist/self-test.d.ts +14 -0
- package/dist/self-test.js +97 -13
- package/dist/storage/demo-guard.d.ts +8 -0
- package/dist/storage/demo-guard.js +53 -0
- package/dist/storage/sqlite-adapter.d.ts +6 -0
- package/dist/storage/sqlite-adapter.js +72 -1
- package/dist/tools/delete-rule.js +49 -11
- package/dist/tools/deploy-rule.d.ts +33 -0
- package/dist/tools/deploy-rule.js +130 -27
- package/dist/tools/evaluate-output.js +54 -33
- package/dist/tools/evaluate-with-llm-judge.js +10 -3
- package/dist/tools/get-traces.d.ts +27 -0
- package/dist/tools/get-traces.js +60 -8
- package/dist/tools/list-rules.js +2 -2
- package/dist/tools/log-trace.js +4 -3
- package/dist/tools/strict-input.d.ts +1 -0
- package/dist/tools/strict-input.js +27 -2
- package/dist/tools/trace-link.d.ts +7 -0
- package/dist/tools/trace-link.js +39 -0
- package/dist/tools/verify-citations.d.ts +19 -0
- package/dist/tools/verify-citations.js +41 -4
- package/dist/types/eval.d.ts +52 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/query.d.ts +25 -0
- package/package.json +8 -1
- package/server.json +2 -2
- package/dist/dashboard/assets/index-VI_nbMfN.js +0 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/jetbrains-mono-cyrillic-ext.woff2)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/jetbrains-mono-cyrillic.woff2)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/jetbrains-mono-greek.woff2)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/jetbrains-mono-vietnamese.woff2)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/jetbrains-mono-latin-ext.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/jetbrains-mono-latin.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Manrope;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/manrope-cyrillic-ext.woff2)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Manrope;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/manrope-cyrillic.woff2)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Manrope;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/manrope-greek.woff2)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Manrope;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/manrope-vietnamese.woff2)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Manrope;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/manrope-latin-ext.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Manrope;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/manrope-latin.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Space Grotesk;font-style:normal;font-weight:500 700;font-display:swap;src:url(/fonts/space-grotesk-vietnamese.woff2)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Space Grotesk;font-style:normal;font-weight:500 700;font-display:swap;src:url(/fonts/space-grotesk-latin-ext.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Space Grotesk;font-style:normal;font-weight:500 700;font-display:swap;src:url(/fonts/space-grotesk-latin.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--iris-50:#f0fdfa;--iris-100:#ccfbf1;--iris-200:#99f6e4;--iris-300:#5eead4;--iris-400:#2dd4bf;--iris-500:#14b8a6;--iris-600:#0d9488;--iris-700:#0f766e;--iris-800:#115e59;--iris-900:#134e4a;--iris-950:#042f2e;--eval-pass:#22c55e;--eval-warn:#eab308;--eval-fail:#ef4444;--eval-tool:#3b82f6;--eval-llm:#a855f7;--eval-skipped:#71717a}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}:root,[data-theme=dark]{--bg-base:#050508;--bg-raised:#08080e;--bg-surface:#0d0d15;--bg-card:#101018;--bg-card-hover:#16161f;--border-subtle:#ffffff0d;--border-default:#ffffff14;--border-strong:#ffffff24;--border-glow:#14b8a680;--text-primary:#f0f0f5;--text-secondary:#9494a8;--text-muted:#5e5e72;--text-accent:var(--iris-400);--glow-primary:#14b8a61f;--glow-strong:#14b8a640;--shadow-sm:0 1px 2px #0000004d;--shadow-md:0 4px 6px #0006;--shadow-lg:0 10px 15px #00000080}[data-theme=light]{--bg-base:#fafcfc;--bg-raised:#f1f5f5;--bg-surface:#e8eded;--bg-card:#fff;--bg-card-hover:#f4f8f8;--border-subtle:#0000000a;--border-default:#00000014;--border-strong:#00000024;--border-glow:#0d948859;--text-primary:#0a0f0e;--text-secondary:#3d5250;--text-muted:#7a908e;--text-accent:var(--iris-700);--glow-primary:#0d94880f;--glow-strong:#0d94881f;--shadow-sm:0 1px 2px #0000000f;--shadow-md:0 4px 6px #00000014;--shadow-lg:0 10px 15px #0000001a}:root{--font-display:"Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-body:"Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-mono:"JetBrains Mono", "Fira Code", ui-monospace, monospace;--font-sans:var(--font-body);--text-caption-xs:11px;--text-caption:12px;--text-body-sm:13px;--text-body:14px;--text-body-lg:15px;--text-heading-sm:16px;--text-heading:20px;--text-display-sm:28px;--text-display:40px;--font-size-xs:var(--text-caption);--font-size-sm:var(--text-body-sm);--font-size-base:var(--text-body);--font-size-lg:var(--text-body-lg);--font-size-xl:var(--text-heading-sm);--font-size-2xl:var(--text-heading);--font-size-3xl:var(--text-display-sm);--leading-body:1.5;--leading-heading:1.2;--leading-display:1.1;--leading-mono:1.4;--space-0_5:2px;--space-1:4px;--space-1_5:6px;--space-2:8px;--space-2_5:10px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px;--space-16:64px;--space-20:80px;--space-24:96px}:root,[data-density=compact]{--density-row:32px;--density-padding:var(--space-3);--density-body:var(--text-body-sm)}[data-density=comfortable]{--density-row:44px;--density-padding:var(--space-4);--density-body:var(--text-body)}:root{--sidebar-width-expanded:256px;--sidebar-width-collapsed:64px;--header-height:56px;--page-toolbar-height:40px;--radius-xs:4px;--radius-sm:6px;--radius:8px;--radius-lg:12px;--radius-xl:16px;--radius-pill:999px;--border-radius:var(--radius);--border-radius-sm:var(--radius-xs);--border-radius-lg:var(--radius-lg);--transition-instant:.1s ease;--transition-fast:.15s ease;--transition-base:.2s ease;--transition-slow:.3s ease;--ease-iris:cubic-bezier(.25, .4, .25, 1);--bg-primary:var(--bg-base);--bg-secondary:var(--bg-raised);--bg-tertiary:var(--bg-surface);--bg-hover:var(--bg-card-hover);--border-color:var(--border-default);--accent-primary:var(--iris-500);--accent-primary-hover:var(--iris-400);--accent-success:var(--eval-pass);--accent-error:var(--eval-fail);--accent-warning:var(--eval-warn);--accent-tool:var(--eval-tool);--accent-llm:var(--eval-llm)}html{transition:background-color var(--transition-base), color var(--transition-base)}.iris-btn{appearance:none;justify-content:center;align-items:center;gap:var(--space-1_5);border-radius:var(--radius-sm);font-family:inherit;font-size:var(--text-body-sm);font-weight:500;line-height:var(--leading-body);padding:var(--space-1_5) var(--space-3);cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color var(--transition-instant), border-color var(--transition-instant), color var(--transition-instant), transform var(--transition-instant);border:1px solid #0000;text-decoration:none;display:inline-flex}.iris-btn:disabled,.iris-btn[aria-busy=true]{opacity:.5;cursor:default}.iris-btn:not(:disabled):active{transform:translateY(1px)}.iris-btn--ghost{border-color:var(--border-default);color:var(--text-secondary);background:0 0}.iris-btn--ghost:not(:disabled):hover{background:var(--bg-card-hover);border-color:var(--border-strong);color:var(--text-primary)}.iris-btn--primary{background:var(--iris-600);border-color:var(--iris-600);color:#fff;font-weight:600}.iris-btn--primary:not(:disabled):hover{background:var(--iris-500);border-color:var(--iris-500)}.iris-btn--danger{border-color:var(--border-default);color:var(--eval-fail);background:0 0}.iris-btn--danger:not(:disabled):hover{border-color:var(--eval-fail);background:color-mix(in srgb, var(--eval-fail) 10%, transparent)}.iris-btn--danger-solid{background:var(--eval-fail);border-color:var(--eval-fail);color:#fff;font-weight:600}.iris-btn--danger-solid:not(:disabled):hover{background:#dc2626;border-color:#dc2626}.iris-btn--sm{padding:var(--space-0_5) var(--space-2_5);font-size:var(--text-caption)}.iris-btn--mono{font-family:var(--font-mono)}.iris-kbd{font-size:var(--text-caption);font-family:var(--font-mono);background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xs);padding:2px var(--space-2);color:var(--text-muted)}.iris-card{background:var(--bg-card);border:1px solid var(--border-default);border-radius:var(--radius-lg);transition:background-color var(--transition-fast), border-color var(--transition-fast)}.iris-card--hover:hover,.iris-card--hover:focus-within{border-color:var(--border-strong)}.iris-stack{gap:var(--space-3);flex-direction:column;display:flex}.iris-stack--lg{gap:var(--space-6)}.iris-row{align-items:center;gap:var(--space-3);display:flex}.iris-grid-kpis{gap:var(--space-3);grid-template-columns:repeat(auto-fit,minmax(180px,1fr));display:grid}.iris-grid-split{gap:var(--space-3);grid-template-columns:repeat(auto-fit,minmax(340px,1fr));display:grid}.iris-grid-gauge{gap:var(--space-3);grid-template-columns:minmax(280px,1fr) minmax(0,1.8fr);align-items:stretch;display:grid}@media (width<=960px){.iris-grid-gauge{grid-template-columns:1fr}}.iris-num{font-variant-numeric:tabular-nums}.iris-num--right{text-align:right}.iris-error-box{background:color-mix(in srgb, var(--eval-fail) 12%, transparent);border:1px solid var(--eval-fail);border-radius:var(--radius);padding:var(--space-4);color:var(--eval-fail);gap:var(--space-2);flex-direction:column;display:flex}.iris-backdrop{z-index:110;background:oklch(0% 0 0/.55);justify-content:center;align-items:flex-start;display:flex;position:fixed;inset:0}.iris-modal{background:var(--bg-card);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);flex-direction:column;display:flex;overflow:hidden}.moment-card{gap:var(--space-3);padding:var(--space-4);color:var(--text-primary);border-left:3px solid var(--moment-sig-color,transparent);grid-template-columns:24px 48px 1fr auto;align-items:start;display:grid}.moment-card:hover,.moment-card:focus-within{background:var(--bg-card-hover);border-left-color:var(--moment-sig-color,transparent)}.moment-card--archived{opacity:.5}.moment-card--selected{background:var(--bg-card-hover);border-color:var(--iris-500);border-left-color:var(--moment-sig-color,transparent)}.moment-card__checkbox-wrap{padding-top:var(--space-1);justify-content:center;align-items:center;display:flex}.moment-card__checkbox{cursor:pointer;width:16px;height:16px;accent-color:var(--iris-500)}.moment-card__rail{align-items:center;gap:var(--space-1);flex-direction:column;display:flex}.moment-card__glyph{width:32px;height:32px;font-size:var(--text-body);font-family:var(--font-mono);color:var(--bg-base);border-radius:50%;justify-content:center;align-items:center;font-weight:700;display:flex}.moment-card__body-link{color:inherit;gap:var(--space-2);flex-direction:column;min-width:0;text-decoration:none;display:flex}.moment-card__body-link:hover{color:inherit}.moment-card__header{align-items:center;gap:var(--space-2);flex-wrap:wrap;display:flex}.moment-card__agent{font-size:var(--text-body-sm);color:var(--text-primary);font-weight:600}.moment-card__sig{font-size:var(--text-caption);font-family:var(--font-mono);color:var(--text-secondary);text-overflow:ellipsis;white-space:nowrap;font-weight:500;overflow:hidden}.moment-card__verdict{font-size:var(--text-caption);font-family:var(--font-mono);letter-spacing:.05em;font-weight:700}.moment-card__tag{font-size:var(--text-caption);font-family:var(--font-mono);color:var(--text-muted);background:var(--bg-surface);border:1px solid var(--border-default);padding:0 var(--space-2);border-radius:var(--radius-xs)}.moment-card__tag--new{letter-spacing:.05em;color:var(--text-accent);background:var(--glow-primary);border-color:var(--iris-500);font-weight:700}.moment-card__preview{font-size:var(--text-caption);font-family:var(--font-mono);color:var(--text-muted);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.moment-card__chips{gap:var(--space-1);flex-wrap:wrap;display:flex}.moment-card__chip{font-size:var(--text-caption);font-family:var(--font-mono);padding:1px var(--space-1_5);border-radius:var(--radius-xs);background:color-mix(in srgb, var(--eval-fail) 12%, transparent);color:var(--eval-fail)}.moment-card__meta{align-items:flex-end;gap:var(--space-1);font-size:var(--text-caption);font-family:var(--font-mono);color:var(--text-muted);flex-direction:column;display:flex}.view-tabs{align-items:center;gap:var(--space-1);border-bottom:1px solid var(--border-subtle);margin-bottom:var(--space-5);display:flex}.view-tabs__tab{align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);font-family:var(--font-display);font-size:var(--text-body-sm);letter-spacing:-.01em;color:var(--text-muted);cursor:pointer;transition:color var(--transition-fast), border-color var(--transition-fast);border-bottom:2px solid #0000;margin-bottom:-1px;font-weight:600;text-decoration:none;display:inline-flex}.view-tabs__tab:hover{color:var(--text-primary)}.view-tabs__tab[aria-selected=true]{color:var(--text-primary);border-bottom-color:var(--iris-500)}.view-tabs__hint{font-size:var(--text-caption-xs);color:var(--text-muted);font-family:var(--font-mono);padding-left:var(--space-3);margin-left:auto}.cmdk-backdrop{padding-top:15vh}.cmdk__panel{width:min(640px,100% - 32px);max-height:70vh}.cmdk__input-wrap{padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--border-default);align-items:center;gap:var(--space-2);display:flex}.cmdk__prompt{font-family:var(--font-mono);color:var(--text-muted);font-size:var(--text-body-sm)}.cmdk__input{color:var(--text-primary);font-size:var(--text-body);background:0 0;border:none;outline:none;flex:1;font-family:inherit}.cmdk__list{padding:var(--space-2);flex:1;overflow:auto}.cmdk__section-title{font-size:var(--text-caption);font-family:var(--font-mono);color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;padding:var(--space-2) var(--space-3)}.cmdk__item{justify-content:space-between;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-3);border-radius:var(--radius-sm);cursor:pointer;color:var(--text-secondary);display:flex}.cmdk__item:hover,.cmdk__item[aria-selected=true]{background:var(--bg-card-hover);color:var(--text-primary)}.cmdk__item-body{flex-direction:column;gap:2px;min-width:0;display:flex}.cmdk__item-title{font-size:var(--text-body-sm);font-weight:500}.cmdk__item-subtitle{font-size:var(--text-caption);color:var(--text-muted);font-family:var(--font-mono);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.cmdk__empty{padding:var(--space-6);text-align:center;color:var(--text-muted);font-size:var(--text-body-sm)}.cmdk__empty-clear{color:var(--text-accent);cursor:pointer;font-size:inherit;background:0 0;border:none;font-family:inherit;text-decoration:underline}.cmdk__footer{gap:var(--space-3);font-size:var(--text-caption);color:var(--text-muted);font-family:var(--font-mono);padding:var(--space-2) var(--space-4);border-top:1px solid var(--border-default);background:var(--bg-surface);display:flex}.cmdk__footer-count{margin-left:auto}.confirm-dialog__panel{width:min(440px,100% - 32px);padding:var(--space-5);gap:var(--space-3);margin-top:20vh}.confirm-dialog__title{font-family:var(--font-display);font-size:var(--text-heading-sm);letter-spacing:-.01em;color:var(--text-primary);margin:0;font-weight:600}.confirm-dialog__body{font-size:var(--text-body-sm);color:var(--text-secondary);line-height:var(--leading-body);margin:0}.confirm-dialog__error{font-size:var(--text-body-sm);color:var(--eval-fail);margin:0}.confirm-dialog__actions{justify-content:flex-end;gap:var(--space-2);margin-top:var(--space-2);display:flex}.stat-tile{padding:var(--space-4) var(--space-5);gap:var(--space-2);flex-direction:column;min-height:110px;display:flex}.stat-tile__header{align-items:center;gap:var(--space-2);font-size:var(--text-caption-xs);font-weight:600;font-family:var(--font-body);text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);display:flex}.stat-tile__value-row{align-items:baseline;gap:var(--space-2);display:flex}.stat-tile__value{font-family:var(--font-display);font-size:var(--text-display-sm);font-weight:700;line-height:var(--leading-display);letter-spacing:-.02em;font-variant-numeric:tabular-nums}.stat-tile__delta{font-family:var(--font-mono);font-size:var(--text-caption);font-weight:500}.stat-tile__sub{font-size:var(--text-caption);color:var(--text-secondary)}.section-header{padding-top:var(--space-3);margin-bottom:var(--space-1);flex-direction:column;gap:2px;display:flex}.section-header__top{justify-content:space-between;align-items:baseline;gap:var(--space-3);flex-wrap:wrap;display:flex}.section-header__title{font-family:var(--font-display);font-size:var(--text-heading-sm);letter-spacing:-.015em;color:var(--text-primary);margin:0;font-weight:600}.section-header__trailing{font-family:var(--font-mono);font-size:var(--text-caption);color:var(--text-muted)}.section-header__question{font-size:var(--text-caption);color:var(--text-muted);margin:0}.detail-back{color:var(--text-muted);font-size:var(--text-body-sm);align-items:center;gap:var(--space-1);width:fit-content;transition:color var(--transition-instant);text-decoration:none;display:inline-flex}.detail-back:hover{color:var(--text-primary)}.detail-card{padding:var(--space-5)}.detail-card__grid{gap:var(--space-4);font-size:var(--text-body-sm);grid-template-columns:repeat(auto-fit,minmax(200px,1fr));display:grid}.detail-card__label{color:var(--text-muted);font-size:var(--text-caption);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.05em}.detail-section{gap:var(--space-3);flex-direction:column;display:flex}.detail-section__title{font-family:var(--font-display);font-size:var(--text-heading-sm);color:var(--text-primary);letter-spacing:-.01em;font-weight:600;line-height:var(--leading-heading);margin:0}.eval-card{padding:var(--space-4);gap:var(--space-3);flex-direction:column;display:flex}.eval-card__badges{align-items:center;gap:var(--space-3);display:flex}.eval-card__rules{gap:var(--space-2);flex-direction:column;display:flex}.eval-card__rule{align-items:center;gap:var(--space-2);font-size:var(--text-body-sm);padding:var(--space-1) var(--space-2);background:var(--bg-base);border-radius:var(--radius-xs);display:flex}.eval-card__rule-mark{width:16px}.eval-card__rule-name{color:var(--text-secondary);font-size:var(--text-caption)}.eval-card__rule-message{color:var(--text-muted);font-size:var(--text-caption);flex:1}.eval-card__suggestions{font-size:var(--text-body-sm)}.eval-card__suggestions-label{color:var(--text-muted);margin-bottom:var(--space-1)}.eval-card__suggestions-list{padding-left:var(--space-5);color:var(--text-secondary);margin:0}.rule-card{padding:var(--space-4);gap:var(--space-3);grid-template-columns:1fr auto;align-items:start;display:grid}.rule-card__body{gap:var(--space-2);flex-direction:column;min-width:0;display:flex}.rule-card__name-row{gap:var(--space-2);flex-wrap:wrap;align-items:baseline;display:flex}.rule-card__name{font-size:var(--text-body);font-weight:600;font-family:var(--font-mono);color:var(--text-primary)}.rule-card__description{font-size:var(--text-body-sm);color:var(--text-secondary);margin:0}.rule-card__meta{gap:var(--space-3);font-size:var(--text-caption);color:var(--text-muted);font-family:var(--font-mono);flex-wrap:wrap;display:flex}.rule-card__badge{font-size:var(--text-caption);font-family:var(--font-mono);padding:1px var(--space-1_5);border-radius:var(--radius-xs);background:var(--bg-surface);color:var(--text-secondary)}.rule-card__source-link{color:var(--text-accent);font-size:var(--text-caption);font-family:var(--font-mono);text-decoration:underline}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{width:100%;height:100%}body{font-family:var(--font-body);font-size:var(--text-body);color:var(--text-primary);background-color:var(--bg-base);line-height:var(--leading-body);-webkit-font-smoothing:antialiased;font-feature-settings:"cv11", "ss01";font-variant-numeric:tabular-nums}h1,h2,h3,h4,.display{font-family:var(--font-display);letter-spacing:-.01em;font-weight:600}a{color:var(--text-accent);text-decoration:none}a:hover{color:var(--iris-300)}button{cursor:pointer;font-family:inherit}input,select{font-family:inherit;font-size:inherit}code,pre{font-family:var(--font-mono)}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,[role=button]:focus-visible{outline:2px solid var(--iris-500);outline-offset:2px;border-radius:var(--radius-xs)}.iris-sr-reveal{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.iris-sr-reveal:focus-within{width:auto;height:auto;margin:var(--space-3) 0 0 0;padding:var(--space-3) var(--space-4);clip:auto;white-space:normal;background:var(--bg-card);color:var(--text-primary);border:1px solid var(--iris-500);border-radius:var(--radius-sm);position:static;overflow:visible}.iris-sr-reveal:focus-within>li{padding:var(--space-1) 0;list-style:none}::selection{background:var(--iris-600);color:#fff}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--bg-base)}::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:var(--radius-xs)}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}html{scrollbar-color:var(--border-strong) transparent;scrollbar-width:thin}@keyframes pulse-ring{0%{opacity:.5;transform:scale(1)}to{opacity:0;transform:scale(2.5)}}.pulse-dot{position:relative}.pulse-dot:after{content:"";background:var(--iris-500);border-radius:50%;animation:2s ease-out infinite pulse-ring;position:absolute;inset:-2px}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@media (width<=767px){aside[aria-label=Main\ navigation]{width:160px}main{overflow-x:auto}}@media print{body{color:#000!important;background:#fff!important}aside[aria-label=Main\ navigation],header,[role=region][aria-label=Welcome],[role=region][aria-label=Bulk\ actions],[role=status],[role=dialog]{display:none!important}body,#root,main{height:auto!important;overflow:visible!important}main{padding:0!important}tr,pre,code{page-break-inside:avoid}h1,h2,h3{page-break-after:avoid}[aria-label*=violation],[aria-label*=spike],[aria-label*=collision],[aria-label*=Pass],[aria-label*=Fail]{border:1px solid #000!important}a{color:#000!important;text-decoration:underline!important}a[href^=http]:after{content:" (" attr(href) ")";color:#555;font-size:80%}}
|
|
1
|
+
@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/jetbrains-mono-cyrillic-ext.woff2)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/jetbrains-mono-cyrillic.woff2)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/jetbrains-mono-greek.woff2)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/jetbrains-mono-vietnamese.woff2)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/jetbrains-mono-latin-ext.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/jetbrains-mono-latin.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Manrope;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/manrope-cyrillic-ext.woff2)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Manrope;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/manrope-cyrillic.woff2)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Manrope;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/manrope-greek.woff2)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Manrope;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/manrope-vietnamese.woff2)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Manrope;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/manrope-latin-ext.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Manrope;font-style:normal;font-weight:400 700;font-display:swap;src:url(/fonts/manrope-latin.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Space Grotesk;font-style:normal;font-weight:500 700;font-display:swap;src:url(/fonts/space-grotesk-vietnamese.woff2)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Space Grotesk;font-style:normal;font-weight:500 700;font-display:swap;src:url(/fonts/space-grotesk-latin-ext.woff2)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Space Grotesk;font-style:normal;font-weight:500 700;font-display:swap;src:url(/fonts/space-grotesk-latin.woff2)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--iris-50:#f0fdfa;--iris-100:#ccfbf1;--iris-200:#99f6e4;--iris-300:#5eead4;--iris-400:#2dd4bf;--iris-500:#14b8a6;--iris-600:#0d9488;--iris-700:#0f766e;--iris-800:#115e59;--iris-900:#134e4a;--iris-950:#042f2e;--eval-pass:#22c55e;--eval-warn:#eab308;--eval-fail:#ef4444;--eval-tool:#3b82f6;--eval-llm:#a855f7;--eval-skipped:#71717a}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}:root,[data-theme=dark]{--bg-base:#050508;--bg-raised:#08080e;--bg-surface:#0d0d15;--bg-card:#101018;--bg-card-hover:#16161f;--border-subtle:#ffffff0d;--border-default:#ffffff14;--border-strong:#ffffff24;--border-glow:#14b8a680;--text-primary:#f0f0f5;--text-secondary:#9494a8;--text-muted:#5e5e72;--text-accent:var(--iris-400);--glow-primary:#14b8a61f;--glow-strong:#14b8a640;--shadow-sm:0 1px 2px #0000004d;--shadow-md:0 4px 6px #0006;--shadow-lg:0 10px 15px #00000080}[data-theme=light]{--bg-base:#fafcfc;--bg-raised:#f1f5f5;--bg-surface:#e8eded;--bg-card:#fff;--bg-card-hover:#f4f8f8;--border-subtle:#0000000a;--border-default:#00000014;--border-strong:#00000024;--border-glow:#0d948859;--text-primary:#0a0f0e;--text-secondary:#3d5250;--text-muted:#7a908e;--text-accent:var(--iris-700);--glow-primary:#0d94880f;--glow-strong:#0d94881f;--shadow-sm:0 1px 2px #0000000f;--shadow-md:0 4px 6px #00000014;--shadow-lg:0 10px 15px #0000001a}:root{--font-display:"Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-body:"Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-mono:"JetBrains Mono", "Fira Code", ui-monospace, monospace;--font-sans:var(--font-body);--text-caption-xs:11px;--text-caption:12px;--text-body-sm:13px;--text-body:14px;--text-body-lg:15px;--text-heading-sm:16px;--text-heading:20px;--text-display-sm:28px;--text-display:40px;--font-size-xs:var(--text-caption);--font-size-sm:var(--text-body-sm);--font-size-base:var(--text-body);--font-size-lg:var(--text-body-lg);--font-size-xl:var(--text-heading-sm);--font-size-2xl:var(--text-heading);--font-size-3xl:var(--text-display-sm);--leading-body:1.5;--leading-heading:1.2;--leading-display:1.1;--leading-mono:1.4;--space-0_5:2px;--space-1:4px;--space-1_5:6px;--space-2:8px;--space-2_5:10px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--space-8:32px;--space-10:40px;--space-12:48px;--space-16:64px;--space-20:80px;--space-24:96px}:root,[data-density=compact]{--density-row:32px;--density-padding:var(--space-3);--density-body:var(--text-body-sm)}[data-density=comfortable]{--density-row:44px;--density-padding:var(--space-4);--density-body:var(--text-body)}:root{--sidebar-width-expanded:256px;--sidebar-width-collapsed:64px;--header-height:56px;--page-toolbar-height:40px;--radius-xs:4px;--radius-sm:6px;--radius:8px;--radius-lg:12px;--radius-xl:16px;--radius-pill:999px;--border-radius:var(--radius);--border-radius-sm:var(--radius-xs);--border-radius-lg:var(--radius-lg);--transition-instant:.1s ease;--transition-fast:.15s ease;--transition-base:.2s ease;--transition-slow:.3s ease;--ease-iris:cubic-bezier(.25, .4, .25, 1);--bg-primary:var(--bg-base);--bg-secondary:var(--bg-raised);--bg-tertiary:var(--bg-surface);--bg-hover:var(--bg-card-hover);--border-color:var(--border-default);--accent-primary:var(--iris-500);--accent-primary-hover:var(--iris-400);--accent-success:var(--eval-pass);--accent-error:var(--eval-fail);--accent-warning:var(--eval-warn);--accent-tool:var(--eval-tool);--accent-llm:var(--eval-llm)}html{transition:background-color var(--transition-base), color var(--transition-base)}.iris-btn{appearance:none;justify-content:center;align-items:center;gap:var(--space-1_5);border-radius:var(--radius-sm);font-family:inherit;font-size:var(--text-body-sm);font-weight:500;line-height:var(--leading-body);padding:var(--space-1_5) var(--space-3);cursor:pointer;-webkit-user-select:none;user-select:none;transition:background-color var(--transition-instant), border-color var(--transition-instant), color var(--transition-instant), transform var(--transition-instant);border:1px solid #0000;text-decoration:none;display:inline-flex}.iris-btn:disabled,.iris-btn[aria-busy=true]{opacity:.5;cursor:default}.iris-btn:not(:disabled):active{transform:translateY(1px)}.iris-btn--ghost{border-color:var(--border-default);color:var(--text-secondary);background:0 0}.iris-btn--ghost:not(:disabled):hover{background:var(--bg-card-hover);border-color:var(--border-strong);color:var(--text-primary)}.iris-btn--primary{background:var(--iris-600);border-color:var(--iris-600);color:#fff;font-weight:600}.iris-btn--primary:not(:disabled):hover{background:var(--iris-500);border-color:var(--iris-500)}.iris-btn--danger{border-color:var(--border-default);color:var(--eval-fail);background:0 0}.iris-btn--danger:not(:disabled):hover{border-color:var(--eval-fail);background:color-mix(in srgb, var(--eval-fail) 10%, transparent)}.iris-btn--danger-solid{background:var(--eval-fail);border-color:var(--eval-fail);color:#fff;font-weight:600}.iris-btn--danger-solid:not(:disabled):hover{background:#dc2626;border-color:#dc2626}.iris-btn--sm{padding:var(--space-0_5) var(--space-2_5);font-size:var(--text-caption)}.iris-btn--mono{font-family:var(--font-mono)}.iris-kbd{font-size:var(--text-caption);font-family:var(--font-mono);background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xs);padding:2px var(--space-2);color:var(--text-muted)}.iris-card{background:var(--bg-card);border:1px solid var(--border-default);border-radius:var(--radius-lg);transition:background-color var(--transition-fast), border-color var(--transition-fast)}.iris-card--hover:hover,.iris-card--hover:focus-within{border-color:var(--border-strong)}.iris-stack{gap:var(--space-3);flex-direction:column;display:flex}.iris-stack--lg{gap:var(--space-6)}.iris-row{align-items:center;gap:var(--space-3);display:flex}.iris-grid-kpis{gap:var(--space-3);grid-template-columns:repeat(auto-fit,minmax(180px,1fr));display:grid}.iris-grid-split{gap:var(--space-3);grid-template-columns:repeat(auto-fit,minmax(340px,1fr));display:grid}.iris-grid-gauge{gap:var(--space-3);grid-template-columns:minmax(280px,1fr) minmax(0,1.8fr);align-items:stretch;display:grid}@media (width<=960px){.iris-grid-gauge{grid-template-columns:1fr}}.iris-num{font-variant-numeric:tabular-nums}.iris-num--right{text-align:right}.iris-error-box{background:color-mix(in srgb, var(--eval-fail) 12%, transparent);border:1px solid var(--eval-fail);border-radius:var(--radius);padding:var(--space-4);color:var(--eval-fail);gap:var(--space-2);flex-direction:column;display:flex}.iris-backdrop{z-index:110;background:oklch(0% 0 0/.55);justify-content:center;align-items:flex-start;display:flex;position:fixed;inset:0}.iris-modal{background:var(--bg-card);border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);flex-direction:column;display:flex;overflow:hidden}.moment-card{gap:var(--space-3);padding:var(--space-4);color:var(--text-primary);border-left:3px solid var(--moment-sig-color,transparent);grid-template-columns:24px 48px 1fr auto;align-items:start;display:grid}.moment-card:hover,.moment-card:focus-within{background:var(--bg-card-hover);border-left-color:var(--moment-sig-color,transparent)}.moment-card--archived{opacity:.5}.moment-card--selected{background:var(--bg-card-hover);border-color:var(--iris-500);border-left-color:var(--moment-sig-color,transparent)}.moment-card__checkbox-wrap{padding-top:var(--space-1);justify-content:center;align-items:center;display:flex}.moment-card__checkbox{cursor:pointer;width:16px;height:16px;accent-color:var(--iris-500)}.moment-card__rail{align-items:center;gap:var(--space-1);flex-direction:column;display:flex}.moment-card__glyph{width:32px;height:32px;font-size:var(--text-body);font-family:var(--font-mono);color:var(--bg-base);border-radius:50%;justify-content:center;align-items:center;font-weight:700;display:flex}.moment-card__body-link{color:inherit;gap:var(--space-2);flex-direction:column;min-width:0;text-decoration:none;display:flex}.moment-card__body-link:hover{color:inherit}.moment-card__header{align-items:center;gap:var(--space-2);flex-wrap:wrap;display:flex}.moment-card__agent{font-size:var(--text-body-sm);color:var(--text-primary);font-weight:600}.moment-card__sig{font-size:var(--text-caption);font-family:var(--font-mono);color:var(--text-secondary);text-overflow:ellipsis;white-space:nowrap;font-weight:500;overflow:hidden}.moment-card__verdict{font-size:var(--text-caption);font-family:var(--font-mono);letter-spacing:.05em;font-weight:700}.moment-card__tag{font-size:var(--text-caption);font-family:var(--font-mono);color:var(--text-muted);background:var(--bg-surface);border:1px solid var(--border-default);padding:0 var(--space-2);border-radius:var(--radius-xs)}.moment-card__tag--new{letter-spacing:.05em;color:var(--text-accent);background:var(--glow-primary);border-color:var(--iris-500);font-weight:700}.moment-card__preview{font-size:var(--text-caption);font-family:var(--font-mono);color:var(--text-muted);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.moment-card__chips{gap:var(--space-1);flex-wrap:wrap;display:flex}.moment-card__chip{font-size:var(--text-caption);font-family:var(--font-mono);padding:1px var(--space-1_5);border-radius:var(--radius-xs);background:color-mix(in srgb, var(--eval-fail) 12%, transparent);color:var(--eval-fail)}.moment-card__meta{align-items:flex-end;gap:var(--space-1);font-size:var(--text-caption);font-family:var(--font-mono);color:var(--text-muted);flex-direction:column;display:flex}.view-tabs{align-items:center;gap:var(--space-1);border-bottom:1px solid var(--border-subtle);margin-bottom:var(--space-5);display:flex}.view-tabs__tab{align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);font-family:var(--font-display);font-size:var(--text-body-sm);letter-spacing:-.01em;color:var(--text-muted);cursor:pointer;transition:color var(--transition-fast), border-color var(--transition-fast);border-bottom:2px solid #0000;margin-bottom:-1px;font-weight:600;text-decoration:none;display:inline-flex}.view-tabs__tab:hover{color:var(--text-primary)}.view-tabs__tab[aria-selected=true]{color:var(--text-primary);border-bottom-color:var(--iris-500)}.view-tabs__hint{font-size:var(--text-caption-xs);color:var(--text-muted);font-family:var(--font-mono);padding-left:var(--space-3);margin-left:auto}.cmdk-backdrop{padding-top:15vh}.cmdk__panel{width:min(640px,100% - 32px);max-height:70vh}.cmdk__input-wrap{padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--border-default);align-items:center;gap:var(--space-2);display:flex}.cmdk__prompt{font-family:var(--font-mono);color:var(--text-muted);font-size:var(--text-body-sm)}.cmdk__input{color:var(--text-primary);font-size:var(--text-body);background:0 0;border:none;outline:none;flex:1;font-family:inherit}.cmdk__list{padding:var(--space-2);flex:1;overflow:auto}.cmdk__section-title{font-size:var(--text-caption);font-family:var(--font-mono);color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;padding:var(--space-2) var(--space-3)}.cmdk__item{justify-content:space-between;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-3);border-radius:var(--radius-sm);cursor:pointer;color:var(--text-secondary);display:flex}.cmdk__item:hover,.cmdk__item[aria-selected=true]{background:var(--bg-card-hover);color:var(--text-primary)}.cmdk__item-body{flex-direction:column;gap:2px;min-width:0;display:flex}.cmdk__item-title{font-size:var(--text-body-sm);font-weight:500}.cmdk__item-subtitle{font-size:var(--text-caption);color:var(--text-muted);font-family:var(--font-mono);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.cmdk__empty{padding:var(--space-6);text-align:center;color:var(--text-muted);font-size:var(--text-body-sm)}.cmdk__empty-clear{color:var(--text-accent);cursor:pointer;font-size:inherit;background:0 0;border:none;font-family:inherit;text-decoration:underline}.cmdk__footer{gap:var(--space-3);font-size:var(--text-caption);color:var(--text-muted);font-family:var(--font-mono);padding:var(--space-2) var(--space-4);border-top:1px solid var(--border-default);background:var(--bg-surface);display:flex}.cmdk__footer-count{margin-left:auto}.confirm-dialog__panel{width:min(440px,100% - 32px);padding:var(--space-5);gap:var(--space-3);margin-top:20vh}.confirm-dialog__title{font-family:var(--font-display);font-size:var(--text-heading-sm);letter-spacing:-.01em;color:var(--text-primary);margin:0;font-weight:600}.confirm-dialog__body{font-size:var(--text-body-sm);color:var(--text-secondary);line-height:var(--leading-body);margin:0}.confirm-dialog__error{font-size:var(--text-body-sm);color:var(--eval-fail);margin:0}.confirm-dialog__actions{justify-content:flex-end;gap:var(--space-2);margin-top:var(--space-2);display:flex}.stat-tile{padding:var(--space-4) var(--space-5);gap:var(--space-2);flex-direction:column;min-height:110px;display:flex}.stat-tile__header{align-items:center;gap:var(--space-2);font-size:var(--text-caption-xs);font-weight:600;font-family:var(--font-body);text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);display:flex}.stat-tile__value-row{align-items:baseline;gap:var(--space-2);display:flex}.stat-tile__value{font-family:var(--font-display);font-size:var(--text-display-sm);font-weight:700;line-height:var(--leading-display);letter-spacing:-.02em;font-variant-numeric:tabular-nums}.stat-tile__delta{font-family:var(--font-mono);font-size:var(--text-caption);font-weight:500}.stat-tile__sub{font-size:var(--text-caption);color:var(--text-secondary)}.section-header{padding-top:var(--space-3);margin-bottom:var(--space-1);flex-direction:column;gap:2px;display:flex}.section-header__top{justify-content:space-between;align-items:baseline;gap:var(--space-3);flex-wrap:wrap;display:flex}.section-header__title{font-family:var(--font-display);font-size:var(--text-heading-sm);letter-spacing:-.015em;color:var(--text-primary);margin:0;font-weight:600}.section-header__trailing{font-family:var(--font-mono);font-size:var(--text-caption);color:var(--text-muted)}.section-header__question{font-size:var(--text-caption);color:var(--text-muted);margin:0}.detail-back{color:var(--text-muted);font-size:var(--text-body-sm);align-items:center;gap:var(--space-1);width:fit-content;transition:color var(--transition-instant);text-decoration:none;display:inline-flex}.detail-back:hover{color:var(--text-primary)}.detail-card{padding:var(--space-5)}.detail-card__grid{gap:var(--space-4);font-size:var(--text-body-sm);grid-template-columns:repeat(auto-fit,minmax(200px,1fr));display:grid}.detail-card__label{color:var(--text-muted);font-size:var(--text-caption);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.05em}.detail-section{gap:var(--space-3);flex-direction:column;display:flex}.detail-section__title{font-family:var(--font-display);font-size:var(--text-heading-sm);color:var(--text-primary);letter-spacing:-.01em;font-weight:600;line-height:var(--leading-heading);margin:0}.eval-card{padding:var(--space-4);gap:var(--space-3);flex-direction:column;display:flex}.eval-card__badges{align-items:center;gap:var(--space-3);display:flex}.eval-card__rules{gap:var(--space-2);flex-direction:column;display:flex}.eval-card__rule{align-items:center;gap:var(--space-2);font-size:var(--text-body-sm);padding:var(--space-1) var(--space-2);background:var(--bg-base);border-radius:var(--radius-xs);display:flex}.eval-card__rule-mark{width:16px}.eval-card__rule-name{color:var(--text-secondary);font-size:var(--text-caption)}.eval-card__rule-message{color:var(--text-muted);font-size:var(--text-caption);flex:1}.eval-card__suggestions{font-size:var(--text-body-sm)}.eval-card__suggestions-label{color:var(--text-muted);margin-bottom:var(--space-1)}.eval-card__suggestions-list{padding-left:var(--space-5);color:var(--text-secondary);margin:0}.rule-card{padding:var(--space-4);gap:var(--space-3);grid-template-columns:1fr auto;align-items:start;display:grid}.rule-card__body{gap:var(--space-2);flex-direction:column;min-width:0;display:flex}.rule-card__name-row{gap:var(--space-2);flex-wrap:wrap;align-items:baseline;display:flex}.rule-card__name{font-size:var(--text-body);font-weight:600;font-family:var(--font-mono);color:var(--text-primary)}.rule-card__description{font-size:var(--text-body-sm);color:var(--text-secondary);margin:0}.rule-card__meta{gap:var(--space-3);font-size:var(--text-caption);color:var(--text-muted);font-family:var(--font-mono);flex-wrap:wrap;display:flex}.rule-card__badge{font-size:var(--text-caption);font-family:var(--font-mono);padding:1px var(--space-1_5);border-radius:var(--radius-xs);background:var(--bg-surface);color:var(--text-secondary)}.rule-card__source-link{color:var(--text-accent);font-size:var(--text-caption);font-family:var(--font-mono);text-decoration:underline}.rule-card__actions{align-items:flex-end;gap:var(--space-2);flex-direction:column;display:flex}.rule-card__toggle{align-items:center;gap:var(--space-2);display:flex}.rule-card__state{font-size:var(--text-caption);font-family:var(--font-mono);color:var(--text-muted)}.rule-card__state--on{color:var(--eval-pass)}.rule-card__toggle-error{font-size:var(--text-caption);color:var(--eval-fail);text-align:right;max-width:260px}.iris-switch{appearance:none;border-radius:var(--radius-pill);border:1px solid var(--border-strong);background:var(--bg-surface);cursor:pointer;width:36px;height:20px;transition:background-color var(--transition-instant), border-color var(--transition-instant);padding:0;position:relative}.iris-switch__knob{background:var(--text-muted);width:14px;height:14px;transition:transform var(--transition-instant), background-color var(--transition-instant);border-radius:50%;position:absolute;top:2px;left:2px}.iris-switch--on{background:var(--iris-600);border-color:var(--iris-600)}.iris-switch--on .iris-switch__knob{background:#fff;transform:translate(16px)}.iris-switch:disabled,.iris-switch[aria-busy=true]{opacity:.5;cursor:default}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{width:100%;height:100%}body{font-family:var(--font-body);font-size:var(--text-body);color:var(--text-primary);background-color:var(--bg-base);line-height:var(--leading-body);-webkit-font-smoothing:antialiased;font-feature-settings:"cv11", "ss01";font-variant-numeric:tabular-nums}h1,h2,h3,h4,.display{font-family:var(--font-display);letter-spacing:-.01em;font-weight:600}a{color:var(--text-accent);text-decoration:none}a:hover{color:var(--iris-300)}button{cursor:pointer;font-family:inherit}input,select{font-family:inherit;font-size:inherit}code,pre{font-family:var(--font-mono)}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,[role=button]:focus-visible{outline:2px solid var(--iris-500);outline-offset:2px;border-radius:var(--radius-xs)}.iris-sr-reveal{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.iris-sr-reveal:focus-within{width:auto;height:auto;margin:var(--space-3) 0 0 0;padding:var(--space-3) var(--space-4);clip:auto;white-space:normal;background:var(--bg-card);color:var(--text-primary);border:1px solid var(--iris-500);border-radius:var(--radius-sm);position:static;overflow:visible}.iris-sr-reveal:focus-within>li{padding:var(--space-1) 0;list-style:none}::selection{background:var(--iris-600);color:#fff}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--bg-base)}::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:var(--radius-xs)}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}html{scrollbar-color:var(--border-strong) transparent;scrollbar-width:thin}@keyframes pulse-ring{0%{opacity:.5;transform:scale(1)}to{opacity:0;transform:scale(2.5)}}.pulse-dot{position:relative}.pulse-dot:after{content:"";background:var(--iris-500);border-radius:50%;animation:2s ease-out infinite pulse-ring;position:absolute;inset:-2px}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@media (width<=767px){aside[aria-label=Main\ navigation]{width:160px}main{overflow-x:auto}}@media print{body{color:#000!important;background:#fff!important}aside[aria-label=Main\ navigation],header,[role=region][aria-label=Welcome],[role=region][aria-label=Bulk\ actions],[role=status],[role=dialog]{display:none!important}body,#root,main{height:auto!important;overflow:visible!important}main{padding:0!important}tr,pre,code{page-break-inside:avoid}h1,h2,h3{page-break-after:avoid}[aria-label*=violation],[aria-label*=spike],[aria-label*=collision],[aria-label*=Pass],[aria-label*=Fail]{border:1px solid #000!important}a{color:#000!important;text-decoration:underline!important}a[href^=http]:after{content:" (" attr(href) ")";color:#555;font-size:80%}}
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
<
|
|
7
|
+
<!-- Stop shipping agents on vibes is filled from .claims.json brand.tagline at build time (vite.config.ts) — never restate the tagline here. -->
|
|
8
|
+
<title>Iris — Stop shipping agents on vibes</title>
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-CKs2Wbd_.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D0cFfBqn.css">
|
|
10
11
|
</head>
|
|
11
12
|
<body>
|
|
12
13
|
<div id="root"></div>
|
|
@@ -11,13 +11,20 @@ export function registerHealthRoutes(router, storage, version) {
|
|
|
11
11
|
* Cloud too) and reports server-level stats. This is the ONE
|
|
12
12
|
* place storage is called with an explicit LOCAL_TENANT rather
|
|
13
13
|
* than a resolved tenantId; deliberate, documented here so it
|
|
14
|
-
* stays the only exception.
|
|
15
|
-
|
|
14
|
+
* stays the only exception.
|
|
15
|
+
*
|
|
16
|
+
* trace_count is ALL-TIME. It used to read the dashboard
|
|
17
|
+
* summary's 1-hour window, so /health said 0 while /api/v1/traces
|
|
18
|
+
* said 253 on the same demo database (#373 item 1) — a liveness
|
|
19
|
+
* field that disagrees with the data it fronts is worse than
|
|
20
|
+
* none. queryTraces' `total` is the unfiltered COUNT(*) for the
|
|
21
|
+
* tenant; limit 1 keeps the row fetch negligible. */
|
|
22
|
+
const { total } = await storage.queryTraces(LOCAL_TENANT, { limit: 1, offset: 0 });
|
|
16
23
|
res.json({
|
|
17
24
|
status: 'ok',
|
|
18
25
|
version: serverVersion,
|
|
19
26
|
uptime_seconds,
|
|
20
|
-
trace_count:
|
|
27
|
+
trace_count: total,
|
|
21
28
|
storage: 'connected',
|
|
22
29
|
});
|
|
23
30
|
}
|
|
@@ -47,7 +47,7 @@ export function registerMomentRoutes(router, storage) {
|
|
|
47
47
|
sort_order: query.sort_order,
|
|
48
48
|
});
|
|
49
49
|
// Hydrate moments by fetching evals per trace. Acceptable for limit ≤ 200;
|
|
50
|
-
// batching is a
|
|
50
|
+
// batching is a later optimization once we have moment-volume data.
|
|
51
51
|
const moments = [];
|
|
52
52
|
for (const trace of traceResult.traces) {
|
|
53
53
|
const evals = await storage.getEvalsByTraceId(tenantId, trace.trace_id);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import type { PreferenceStore } from '../../preferences.js';
|
|
3
|
+
export declare function preferencesDisplayPath(absolutePath: string): string;
|
|
3
4
|
export declare function registerPreferencesRoutes(router: Router, store: PreferenceStore): void;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { basename, isAbsolute, relative, sep } from 'node:path';
|
|
3
|
+
import { irisHome } from '../../utils/iris-home.js';
|
|
2
4
|
/* PATCH body schema. Every field optional + strict (rejects unknown keys
|
|
3
5
|
* to catch typos at the API boundary). Inner schemas mirror preferences.ts. */
|
|
4
6
|
const MomentFiltersPatchSchema = z
|
|
@@ -31,20 +33,46 @@ const PatchSchema = z
|
|
|
31
33
|
notificationsLastSeen: z.string().datetime({ offset: true }).optional(),
|
|
32
34
|
})
|
|
33
35
|
.strict();
|
|
36
|
+
/*
|
|
37
|
+
* The preferences file's location, in a form safe to show in a browser.
|
|
38
|
+
*
|
|
39
|
+
* Two constraints pull against each other. The welcome banner used to
|
|
40
|
+
* hardcode `~/.iris/preferences.json`, which is wrong whenever IRIS_HOME
|
|
41
|
+
* points elsewhere and always wrong in --demo mode (demo-preferences.json)
|
|
42
|
+
* (#377 item 2). But the absolute path must NOT go over the wire: it
|
|
43
|
+
* embeds the OS username (install-path disclosure, CWE-209 — #334, the
|
|
44
|
+
* reason store.path was removed from these responses in the first place).
|
|
45
|
+
*
|
|
46
|
+
* So the response carries a DISPLAY path: the real file name under the
|
|
47
|
+
* real home, with the home spelled the way the user configured it —
|
|
48
|
+
* `$IRIS_HOME/…` when the env var is set, `~/.iris/…` otherwise. A store
|
|
49
|
+
* pointed outside the iris home (embedders, tests) shows the bare file
|
|
50
|
+
* name rather than any directory.
|
|
51
|
+
*/
|
|
52
|
+
export function preferencesDisplayPath(absolutePath) {
|
|
53
|
+
const home = irisHome();
|
|
54
|
+
const rel = relative(home, absolutePath);
|
|
55
|
+
const insideHome = rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
56
|
+
if (!insideHome)
|
|
57
|
+
return basename(absolutePath);
|
|
58
|
+
const prefix = process.env.IRIS_HOME ? '$IRIS_HOME' : '~/.iris';
|
|
59
|
+
return `${prefix}/${rel.split(sep).join('/')}`;
|
|
60
|
+
}
|
|
34
61
|
export function registerPreferencesRoutes(router, store) {
|
|
35
62
|
/*
|
|
36
63
|
* Responses deliberately omit store.path: the absolute path embeds the
|
|
37
64
|
* OS username (install-path disclosure, CWE-209 — same class as PR
|
|
38
|
-
* #286) and the frontend never read it.
|
|
65
|
+
* #286) and the frontend never read it. `displayPath` is the
|
|
66
|
+
* username-free form (see preferencesDisplayPath).
|
|
39
67
|
*/
|
|
40
68
|
router.get('/preferences', (_req, res) => {
|
|
41
|
-
res.json({ preferences: store.read() });
|
|
69
|
+
res.json({ preferences: store.read(), displayPath: preferencesDisplayPath(store.path) });
|
|
42
70
|
});
|
|
43
71
|
router.patch('/preferences', (req, res) => {
|
|
44
72
|
try {
|
|
45
73
|
const patch = PatchSchema.parse(req.body);
|
|
46
74
|
const updated = store.patch(patch);
|
|
47
|
-
res.json({ preferences: updated });
|
|
75
|
+
res.json({ preferences: updated, displayPath: preferencesDisplayPath(store.path) });
|
|
48
76
|
}
|
|
49
77
|
catch (err) {
|
|
50
78
|
if (err instanceof z.ZodError) {
|
|
@@ -2,9 +2,27 @@ import { Router } from 'express';
|
|
|
2
2
|
import type { IStorageAdapter } from '../../types/query.js';
|
|
3
3
|
import type { CustomRuleStore } from '../../custom-rule-store.js';
|
|
4
4
|
import type { EvalEngine } from '../../eval/engine.js';
|
|
5
|
+
import type { EvalType } from '../../types/eval.js';
|
|
5
6
|
interface RoutesOptions {
|
|
6
7
|
customRuleStore: CustomRuleStore;
|
|
7
8
|
evalEngine: EvalEngine;
|
|
8
9
|
}
|
|
10
|
+
/** Built-in rule metadata as the dashboard sees it — derived from the engine, never restated. */
|
|
11
|
+
export interface BuiltInRuleMeta {
|
|
12
|
+
name: string;
|
|
13
|
+
category: EvalType;
|
|
14
|
+
description: string;
|
|
15
|
+
weight: number;
|
|
16
|
+
critical: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function listBuiltInRules(): BuiltInRuleMeta[];
|
|
9
19
|
export declare function registerRuleRoutes(router: Router, storage: IStorageAdapter, opts: RoutesOptions): void;
|
|
20
|
+
/** Verdict of the proposed rule against the caller's own sample text. */
|
|
21
|
+
export interface SamplePreview {
|
|
22
|
+
passed: boolean;
|
|
23
|
+
score: number;
|
|
24
|
+
message: string;
|
|
25
|
+
skipped: boolean;
|
|
26
|
+
skipReason?: string;
|
|
27
|
+
}
|
|
10
28
|
export {};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { createCustomRule } from '../../eval/rules/custom.js';
|
|
3
|
+
import { rulesByType } from '../../eval/rules/index.js';
|
|
3
4
|
import { requireTenant } from '../../middleware/tenant.js';
|
|
5
|
+
import { DuplicateRuleNameError, replacedRulesWarning, retireSameNamedRules } from '../../tools/deploy-rule.js';
|
|
6
|
+
import { strictBody } from '../validation.js';
|
|
4
7
|
const SeveritySchema = z.enum(['low', 'medium', 'high', 'critical']);
|
|
5
8
|
const EvalTypeSchema = z.enum(['completeness', 'relevance', 'safety', 'cost', 'custom']);
|
|
6
9
|
const RuleTypeSchema = z.enum([
|
|
@@ -13,28 +16,82 @@ const RuleTypeSchema = z.enum([
|
|
|
13
16
|
'json_schema',
|
|
14
17
|
'cost_threshold',
|
|
15
18
|
]);
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
/*
|
|
20
|
+
* Strict at every level the dashboard owns. `config` stays a free-form
|
|
21
|
+
* record on purpose (each rule type reads its own keys and the store
|
|
22
|
+
* validates them per type); everything around it has a fixed key set, so
|
|
23
|
+
* a misspelled `wieght` is rejected instead of silently dropped (#376).
|
|
24
|
+
*
|
|
25
|
+
* `definition.name` is optional: the server always overwrites it with the
|
|
26
|
+
* top-level rule name (#377 item 3), so requiring a value that is then
|
|
27
|
+
* discarded only invited confusion.
|
|
28
|
+
*/
|
|
29
|
+
const DefinitionSchema = strictBody({
|
|
30
|
+
name: z
|
|
31
|
+
.string()
|
|
32
|
+
.min(1)
|
|
33
|
+
.max(80)
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Optional — always replaced by the top-level rule name on deploy'),
|
|
18
36
|
type: RuleTypeSchema,
|
|
19
37
|
config: z.record(z.string(), z.unknown()),
|
|
20
38
|
weight: z.number().positive().optional(),
|
|
21
39
|
});
|
|
22
|
-
const DeploySchema =
|
|
40
|
+
const DeploySchema = strictBody({
|
|
23
41
|
name: z.string().min(1).max(80).regex(/^[a-z0-9._-]+$/i, 'Use letters, digits, dot, dash, underscore'),
|
|
24
42
|
description: z.string().max(500).optional(),
|
|
25
43
|
evalType: EvalTypeSchema,
|
|
26
44
|
severity: SeveritySchema.optional(),
|
|
27
45
|
definition: DefinitionSchema,
|
|
28
46
|
sourceMomentId: z.string().optional(),
|
|
47
|
+
/**
|
|
48
|
+
* Same contract as deploy_rule's `replace`: a name that is already
|
|
49
|
+
* deployed is refused (409) unless this is true, in which case the
|
|
50
|
+
* existing same-named rule(s) are deleted and this one takes their place.
|
|
51
|
+
*/
|
|
52
|
+
replace: z.boolean().default(false),
|
|
29
53
|
});
|
|
30
|
-
|
|
54
|
+
/*
|
|
55
|
+
* Bounded well under the 1mb body limit. The sample is evaluated exactly
|
|
56
|
+
* once, on the main thread, inside the same regex sandbox every other
|
|
57
|
+
* evaluation uses — the cap keeps one request's worst case small.
|
|
58
|
+
*/
|
|
59
|
+
const MAX_SAMPLE_OUTPUT_CHARS = 100_000;
|
|
60
|
+
const PreviewSchema = strictBody({
|
|
31
61
|
definition: DefinitionSchema,
|
|
32
62
|
evalType: EvalTypeSchema.default('custom'),
|
|
33
63
|
/** Window in days; default 7. Hard cap at 30 to keep memory bounded. */
|
|
34
64
|
windowDays: z.coerce.number().int().min(1).max(30).default(7),
|
|
35
65
|
/** Trace cap; default 1000. */
|
|
36
66
|
maxTraces: z.coerce.number().int().min(1).max(5000).default(1000),
|
|
67
|
+
/**
|
|
68
|
+
* Dry-run text. When present the rule is ALSO evaluated against this
|
|
69
|
+
* exact output and the verdict is returned under `sample` — the
|
|
70
|
+
* "validation against sample output" deploy_rule's description has
|
|
71
|
+
* promised since v0.4. The endpoint used to accept the key and ignore it
|
|
72
|
+
* (#373 item 2): a caller got back the historical replay only, with no
|
|
73
|
+
* hint that their sample was never looked at.
|
|
74
|
+
*/
|
|
75
|
+
sampleOutput: z.string().max(MAX_SAMPLE_OUTPUT_CHARS).optional(),
|
|
76
|
+
});
|
|
77
|
+
const ToggleSchema = strictBody({
|
|
78
|
+
enabled: z.boolean(),
|
|
37
79
|
});
|
|
80
|
+
export function listBuiltInRules() {
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const [category, rules] of Object.entries(rulesByType)) {
|
|
83
|
+
for (const rule of rules) {
|
|
84
|
+
out.push({
|
|
85
|
+
name: rule.name,
|
|
86
|
+
category,
|
|
87
|
+
description: rule.description,
|
|
88
|
+
weight: rule.weight,
|
|
89
|
+
critical: rule.critical === true,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
38
95
|
export function registerRuleRoutes(router, storage, opts) {
|
|
39
96
|
/*
|
|
40
97
|
* Every /rules/custom route resolves the tenant via requireTenant(req)
|
|
@@ -44,6 +101,19 @@ export function registerRuleRoutes(router, storage, opts) {
|
|
|
44
101
|
* each tenant's rules will live in their own file (per-tenant partition)
|
|
45
102
|
* and writes will only ever touch the resolved tenant's data.
|
|
46
103
|
*/
|
|
104
|
+
/*
|
|
105
|
+
* The engine's own rule roster. The dashboard used to carry a hand-
|
|
106
|
+
* copied name → category map and classify "safety" by name substrings
|
|
107
|
+
* (pii / injection / blocklist / stub), which is how a
|
|
108
|
+
* no_hallucination_markers failure — a safety rule since v0.5.0 — drilled
|
|
109
|
+
* through to the wrong filter on two charts. Serving the roster from
|
|
110
|
+
* `rulesByType` means a rule added to a bundle is categorised correctly
|
|
111
|
+
* on the dashboard without a second edit. Not tenant-scoped: built-ins
|
|
112
|
+
* are process-global.
|
|
113
|
+
*/
|
|
114
|
+
router.get('/rules/builtin', (_req, res) => {
|
|
115
|
+
res.json({ rules: listBuiltInRules() });
|
|
116
|
+
});
|
|
47
117
|
router.get('/rules/custom', (req, res) => {
|
|
48
118
|
const tenantId = requireTenant(req);
|
|
49
119
|
const rules = opts.customRuleStore.list(tenantId);
|
|
@@ -60,6 +130,15 @@ export function registerRuleRoutes(router, storage, opts) {
|
|
|
60
130
|
...input.definition,
|
|
61
131
|
name: input.name,
|
|
62
132
|
};
|
|
133
|
+
/*
|
|
134
|
+
* Same-name redeploy — the SAME helper deploy_rule uses, so the
|
|
135
|
+
* dashboard cannot accept a duplicate the tool refuses (two rules
|
|
136
|
+
* with one name both fire with indistinguishable rule_results).
|
|
137
|
+
* Throws before anything is written when the name is taken and
|
|
138
|
+
* `replace` is false; with `replace: true` the earlier rule(s) are
|
|
139
|
+
* deleted (audit rows kept, unregistered from the engine) beforehand.
|
|
140
|
+
*/
|
|
141
|
+
const replaced = retireSameNamedRules(opts.customRuleStore, opts.evalEngine, tenantId, input.name, input.replace, 'local');
|
|
63
142
|
const rule = opts.customRuleStore.deploy(tenantId, {
|
|
64
143
|
name: input.name,
|
|
65
144
|
description: input.description,
|
|
@@ -75,13 +154,69 @@ export function registerRuleRoutes(router, storage, opts) {
|
|
|
75
154
|
// v0.5 architectural item. Severity rides along: high/critical makes
|
|
76
155
|
// the rule hard-failing (same as the MCP deploy path and boot loading).
|
|
77
156
|
opts.evalEngine.registerRule(rule.evalType, createCustomRule(rule.definition, rule.severity), rule.id);
|
|
78
|
-
res.status(201).json({
|
|
157
|
+
res.status(201).json({
|
|
158
|
+
rule,
|
|
159
|
+
...(replaced.length > 0 ? { replaced, warning: replacedRulesWarning(input.name, replaced) } : {}),
|
|
160
|
+
});
|
|
79
161
|
}
|
|
80
162
|
catch (err) {
|
|
81
163
|
if (err instanceof z.ZodError) {
|
|
82
164
|
res.status(400).json({ error: 'Invalid rule definition', details: err.issues });
|
|
83
165
|
return;
|
|
84
166
|
}
|
|
167
|
+
if (err instanceof DuplicateRuleNameError) {
|
|
168
|
+
// 409, not 400: the body was valid — the name is taken.
|
|
169
|
+
res.status(409).json({
|
|
170
|
+
error: err.message,
|
|
171
|
+
existing: err.existing.map((r) => ({ id: r.id, evalType: r.evalType, severity: r.severity, enabled: r.enabled })),
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
/*
|
|
179
|
+
* Enable / disable without deleting. delete_rule's and list_rules' tool
|
|
180
|
+
* descriptions have pointed users at "the dashboard's toggle affordance"
|
|
181
|
+
* since v0.4; until now nothing — no route, no UI call, no MCP tool —
|
|
182
|
+
* invoked the store's setEnabled, so the advertised affordance did not
|
|
183
|
+
* exist. The engine is kept in lockstep the same way delete is: a
|
|
184
|
+
* disabled rule stops firing on the very next evaluate_output call, and a
|
|
185
|
+
* re-enabled one fires again, no restart in either direction.
|
|
186
|
+
*/
|
|
187
|
+
router.patch('/rules/custom/:id', (req, res) => {
|
|
188
|
+
try {
|
|
189
|
+
const tenantId = requireTenant(req);
|
|
190
|
+
const { enabled } = ToggleSchema.parse(req.body);
|
|
191
|
+
const existing = opts.customRuleStore.get(tenantId, req.params.id);
|
|
192
|
+
if (!existing) {
|
|
193
|
+
res.status(404).json({ error: 'Rule not found' });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (existing.enabled === enabled) {
|
|
197
|
+
// Idempotent: the engine already matches the store; touching it
|
|
198
|
+
// would double-register or no-op-unregister.
|
|
199
|
+
res.json({ rule: existing });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const rule = opts.customRuleStore.setEnabled(tenantId, req.params.id, enabled);
|
|
203
|
+
if (!rule) {
|
|
204
|
+
res.status(404).json({ error: 'Rule not found' });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (enabled) {
|
|
208
|
+
opts.evalEngine.registerRule(rule.evalType, createCustomRule(rule.definition, rule.severity), rule.id);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
opts.evalEngine.unregisterRule(rule.id);
|
|
212
|
+
}
|
|
213
|
+
res.json({ rule });
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
if (err instanceof z.ZodError) {
|
|
217
|
+
res.status(400).json({ error: 'Invalid toggle request', details: err.issues });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
85
220
|
throw err;
|
|
86
221
|
}
|
|
87
222
|
});
|
|
@@ -128,7 +263,9 @@ async function previewRule(tenantId, input, storage) {
|
|
|
128
263
|
sort_by: 'timestamp',
|
|
129
264
|
sort_order: 'desc',
|
|
130
265
|
});
|
|
131
|
-
|
|
266
|
+
// definition.name is optional on the wire (the deploy path overwrites it
|
|
267
|
+
// anyway); the evaluator wants a name for its result rows.
|
|
268
|
+
const rule = createCustomRule({ ...input.definition, name: input.definition.name ?? 'preview' });
|
|
132
269
|
// Sanity-probe the rule against an empty input. A broken DEFINITION —
|
|
133
270
|
// invalid regex, pattern too long, ReDoS rejection, missing required
|
|
134
271
|
// config — comes back marked configInvalid (custom.ts routes every
|
|
@@ -189,6 +326,22 @@ async function previewRule(tenantId, input, storage) {
|
|
|
189
326
|
}
|
|
190
327
|
}
|
|
191
328
|
}
|
|
329
|
+
/*
|
|
330
|
+
* The sample gets its own budget: it is the one output the author is
|
|
331
|
+
* actually asking about, and it must not inherit a breaker the replay
|
|
332
|
+
* may already have opened.
|
|
333
|
+
*/
|
|
334
|
+
let sample;
|
|
335
|
+
if (input.sampleOutput !== undefined) {
|
|
336
|
+
const r = rule.evaluate({ output: input.sampleOutput, regexBudget: { breaches: 0 } });
|
|
337
|
+
sample = {
|
|
338
|
+
passed: r.passed,
|
|
339
|
+
score: r.score,
|
|
340
|
+
message: r.message,
|
|
341
|
+
skipped: r.skipped === true,
|
|
342
|
+
...(r.skipReason !== undefined ? { skipReason: r.skipReason } : {}),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
192
345
|
return {
|
|
193
346
|
tracesEvaluated: traceResult.traces.length,
|
|
194
347
|
wouldFail,
|
|
@@ -196,5 +349,6 @@ async function previewRule(tenantId, input, storage) {
|
|
|
196
349
|
wouldSkip,
|
|
197
350
|
examples,
|
|
198
351
|
windowSinceIso: since,
|
|
352
|
+
...(sample ? { sample } : {}),
|
|
199
353
|
};
|
|
200
354
|
}
|
|
@@ -2,6 +2,7 @@ import { requireTenant } from '../../middleware/tenant.js';
|
|
|
2
2
|
import { generateTraceId, generateSpanId } from '../../utils/ids.js';
|
|
3
3
|
import { bestEffortExport } from '../../otel/lazy.js';
|
|
4
4
|
import { traceQuerySchema, ingestTraceSchema } from '../validation.js';
|
|
5
|
+
import { DEFAULT_EVAL_TYPE, DEFAULT_EVAL_TYPE_NOTE } from '../../eval/engine.js';
|
|
5
6
|
export function registerTraceRoutes(router, storage, options) {
|
|
6
7
|
/*
|
|
7
8
|
* Deterministic capture over HTTP. MCP tool calls are model-
|
|
@@ -23,7 +24,8 @@ export function registerTraceRoutes(router, storage, options) {
|
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
26
|
// Server-minted, exactly like log_trace — a client-supplied
|
|
26
|
-
// trace_id was already
|
|
27
|
+
// trace_id was already REJECTED by the strict schema above (400,
|
|
28
|
+
// with a message saying the server mints it).
|
|
27
29
|
const traceId = generateTraceId();
|
|
28
30
|
const timestamp = body.timestamp ?? new Date().toISOString();
|
|
29
31
|
const trace = {
|
|
@@ -57,12 +59,23 @@ export function registerTraceRoutes(router, storage, options) {
|
|
|
57
59
|
}
|
|
58
60
|
// Deterministic engine, same context evaluate_output builds. The
|
|
59
61
|
// superRefine on ingestTraceSchema guarantees output is present.
|
|
60
|
-
|
|
62
|
+
// eval_type="all" takes the same every-bundle path the MCP tool
|
|
63
|
+
// takes (evaluateAll): one pass, one regex budget, the critical veto
|
|
64
|
+
// spanning every bundle, and a per-category breakdown — stored under
|
|
65
|
+
// eval_type "all" so the dashboard reads it as the tool's rows.
|
|
66
|
+
const context = {
|
|
61
67
|
output: body.output,
|
|
62
68
|
input: body.input,
|
|
63
69
|
costUsd: body.cost_usd,
|
|
64
70
|
tokenUsage: body.token_usage,
|
|
65
|
-
}
|
|
71
|
+
};
|
|
72
|
+
// An omitted eval_type runs every bundle — the same default, from the
|
|
73
|
+
// same constant, as the MCP tool — and says so in the response.
|
|
74
|
+
const evalTypeOmitted = body.eval_type === undefined;
|
|
75
|
+
const evalType = body.eval_type ?? DEFAULT_EVAL_TYPE;
|
|
76
|
+
const evaluation = evalType === 'all'
|
|
77
|
+
? options.evalEngine.evaluateAll(context)
|
|
78
|
+
: options.evalEngine.evaluate(evalType, context);
|
|
66
79
|
evaluation.trace_id = traceId;
|
|
67
80
|
await storage.insertEvalResult(tenantId, evaluation);
|
|
68
81
|
res.status(201).json({
|
|
@@ -87,6 +100,15 @@ export function registerTraceRoutes(router, storage, options) {
|
|
|
87
100
|
...(evaluation.critical_failures?.length
|
|
88
101
|
? { critical_failures: evaluation.critical_failures }
|
|
89
102
|
: {}),
|
|
103
|
+
// The other half of the veto contract, same as evaluate_output: a
|
|
104
|
+
// critical rule that SKIPPED did not judge the output and cannot
|
|
105
|
+
// veto, so a fail-closed gate needs to see it named.
|
|
106
|
+
...(evaluation.critical_skipped?.length
|
|
107
|
+
? { critical_skipped: evaluation.critical_skipped }
|
|
108
|
+
: {}),
|
|
109
|
+
// Per-bundle breakdown — eval_type="all" only.
|
|
110
|
+
...(evaluation.categories ? { categories: evaluation.categories } : {}),
|
|
111
|
+
...(evalTypeOmitted ? { note: DEFAULT_EVAL_TYPE_NOTE } : {}),
|
|
90
112
|
},
|
|
91
113
|
});
|
|
92
114
|
}
|
|
@@ -108,6 +130,8 @@ export function registerTraceRoutes(router, storage, options) {
|
|
|
108
130
|
framework: query.framework,
|
|
109
131
|
since: query.since,
|
|
110
132
|
until: query.until,
|
|
133
|
+
min_score: query.min_score,
|
|
134
|
+
max_score: query.max_score,
|
|
111
135
|
},
|
|
112
136
|
limit: query.limit,
|
|
113
137
|
offset: query.offset,
|
|
@@ -398,6 +398,17 @@ function generateTimestamp(dayIndex) {
|
|
|
398
398
|
const minute = randomInt(0, 59);
|
|
399
399
|
const second = randomInt(0, 59);
|
|
400
400
|
dayStart.setHours(hour, minute, second, randomInt(0, 999));
|
|
401
|
+
/*
|
|
402
|
+
* The last day is TODAY, and the hour above is drawn from the whole day
|
|
403
|
+
* — so before this clamp a demo seeded at 09:00 carried traces stamped
|
|
404
|
+
* 22:00 tonight, which the timeline rendered as "just now" and the
|
|
405
|
+
* "new since you last looked" counter kept re-counting. A demo trace
|
|
406
|
+
* is never in the future: anything past the seed moment lands inside
|
|
407
|
+
* the hour before it (still seeded-random, still deterministic).
|
|
408
|
+
*/
|
|
409
|
+
if (dayStart.getTime() > now.getTime()) {
|
|
410
|
+
return new Date(now.getTime() - randomInt(1, 3_600_000)).toISOString();
|
|
411
|
+
}
|
|
401
412
|
return dayStart.toISOString();
|
|
402
413
|
}
|
|
403
414
|
function scoreRules(evalType, rules, weights) {
|
package/dist/dashboard/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import { irisHome } from '../utils/iris-home.js';
|
|
|
7
7
|
import { createAuthMiddleware } from '../middleware/auth.js';
|
|
8
8
|
import { createCorsMiddleware } from '../middleware/cors.js';
|
|
9
9
|
import { createErrorHandler } from '../middleware/error-handler.js';
|
|
10
|
-
import { createApiRateLimiter } from '../middleware/rate-limit.js';
|
|
10
|
+
import { createApiRateLimiter, createAuthGateRateLimiter } from '../middleware/rate-limit.js';
|
|
11
11
|
import { createTenantMiddleware } from '../middleware/tenant.js';
|
|
12
12
|
import { createRebindingGuard, isLoopbackHost } from '../middleware/rebinding-guard.js';
|
|
13
13
|
import { registerTraceRoutes } from './routes/traces.js';
|
|
@@ -21,6 +21,7 @@ import { registerFailureRoutes } from './routes/failures.js';
|
|
|
21
21
|
import { registerRuleRoutes } from './routes/rules.js';
|
|
22
22
|
import { registerPreferencesRoutes } from './routes/preferences.js';
|
|
23
23
|
import { registerAuditRoutes } from './routes/audit.js';
|
|
24
|
+
import { createSessionAuth } from './session-auth.js';
|
|
24
25
|
export function createDashboardServer(storage, config, logger, options) {
|
|
25
26
|
const app = express();
|
|
26
27
|
// Security headers
|
|
@@ -56,8 +57,17 @@ export function createDashboardServer(storage, config, logger, options) {
|
|
|
56
57
|
}));
|
|
57
58
|
// CORS
|
|
58
59
|
app.use(createCorsMiddleware(config.security.allowedOrigins));
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
/*
|
|
61
|
+
* Authentication. The Bearer middleware is the contract for MCP clients
|
|
62
|
+
* and capture SDKs; the session layer in front of it is what lets a
|
|
63
|
+
* BROWSER present the same key once (`?key=` or the sign-in form) and
|
|
64
|
+
* then ride an HttpOnly cookie — without it, `--api-key` made the
|
|
65
|
+
* dashboard UI 401 on every page load (#373 item 6). With no key
|
|
66
|
+
* configured both are pass-throughs. The limiter ahead of it is the
|
|
67
|
+
* ceiling every authorization decision sits behind.
|
|
68
|
+
*/
|
|
69
|
+
app.use(createAuthGateRateLimiter(config));
|
|
70
|
+
app.use(createSessionAuth({ apiKey: config.security.apiKey, bearerAuth: createAuthMiddleware(config) }));
|
|
61
71
|
// Tenant resolution — attaches req.tenantId to every request.
|
|
62
72
|
// OSS: always resolves to LOCAL_TENANT. Cloud: swaps for an auth-aware
|
|
63
73
|
// resolver that reads the authenticated session. See middleware/tenant.ts.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type RequestHandler } from 'express';
|
|
2
|
+
export declare const SESSION_COOKIE = "iris_session";
|
|
3
|
+
export interface SessionAuthOptions {
|
|
4
|
+
apiKey: string | undefined;
|
|
5
|
+
/** The Bearer middleware every non-session request still goes through. */
|
|
6
|
+
bearerAuth: RequestHandler;
|
|
7
|
+
}
|
|
8
|
+
export declare function createSessionAuth(opts: SessionAuthOptions): RequestHandler;
|