@orangepro/orangepro-mcp 0.1.0 → 0.2.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.
@@ -1,651 +1,562 @@
1
1
  const TEMPLATE = `<!DOCTYPE html>
2
- <html lang="en" data-theme="dark">
2
+ <html lang="en">
3
3
  <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>OrangePro · Behavior Coverage Report</title>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
6
+ <title>OrangePro · Behavior Coverage</title>
7
7
  <style>
8
- /* ============================================================
9
- OrangePro behavior-coverage report — single-file vision demo.
10
- Identity: GitHub-dark product UI. Honest tier layer added on
11
- top of the v5 mockup: every flow hop is tagged hard-reachable
12
- vs framework-derived, and dynamic-proof is first-class.
13
- All view data lives in window.DATA (the backend graph contract).
14
- ============================================================ */
15
- :root {
16
- --bg: #0d1117;
17
- --surface: #161b22;
18
- --surface-2: #1b2129;
19
- --surface-3: #21262d;
20
- --border: #2b313a;
21
- --border-2: #383f49;
22
- --ink: #e6edf3;
23
- --ink-soft: #c9d3df;
24
- --muted: #9aa4b1;
25
- --faint: #6e7681;
26
-
27
- --green: #3fb950; --green-bg: rgba(63,185,80,.12); --green-bd: rgba(63,185,80,.35);
28
- --amber: #e3b341; --amber-bg: rgba(227,179,65,.12); --amber-bd: rgba(227,179,65,.35);
29
- --red: #f85149; --red-bg: rgba(248,81,73,.11); --red-bd: rgba(248,81,73,.32);
30
- --blue: #58a6ff; --blue-bg: rgba(88,166,255,.12); --blue-bd: rgba(88,166,255,.35);
31
- --purple: #bc8cff; --purple-bg: rgba(188,140,255,.12);--purple-bd:rgba(188,140,255,.35);
32
- --accent: #f0883e; --accent-bg: rgba(240,136,62,.12); --accent-bd:rgba(240,136,62,.40);
33
-
34
- --radius: 12px;
35
- --radius-sm: 8px;
36
- --mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas, monospace;
37
- --sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
38
-
39
- --z-sticky: 100; --z-tooltip: 600;
40
- --ease: cubic-bezier(.22,1,.36,1);
41
- }
42
-
43
- * { box-sizing: border-box; }
44
- html { -webkit-text-size-adjust: 100%; }
45
- body {
46
- margin: 0;
47
- background: var(--bg);
48
- color: var(--ink);
49
- font-family: var(--sans);
50
- font-size: 14px;
51
- line-height: 1.55;
52
- -webkit-font-smoothing: antialiased;
53
- text-rendering: optimizeLegibility;
54
- }
55
- ::selection { background: var(--accent-bg); }
56
- a { color: var(--blue); text-decoration: none; }
57
-
58
- /* scrollbar — keep native behavior, only tint */
59
- ::-webkit-scrollbar { width: 10px; height: 10px; }
60
- ::-webkit-scrollbar-thumb { background: #30363d; border-radius: 6px; border: 2px solid var(--bg); }
61
- ::-webkit-scrollbar-thumb:hover { background: #3d444d; }
62
-
63
- .wrap { max-width: 1120px; margin: 0 auto; padding: 30px 28px 80px; }
64
-
65
- /* ---------- header ---------- */
66
- header.top { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; padding-bottom: 22px; border-bottom: 1px solid var(--border); }
67
- .brand { display: flex; align-items: center; gap: 13px; }
68
- .logo { width: 38px; height: 38px; border-radius: 9px; background: linear-gradient(150deg, #f0883e, #db5a28); display: grid; place-items: center; color: #1a0f06; font-weight: 800; font-size: 17px; box-shadow: 0 2px 10px rgba(219,90,40,.35); flex-shrink: 0; }
69
- .brand h1 { margin: 0; font-size: 17px; font-weight: 650; letter-spacing: -.01em; }
70
- .brand p { margin: 2px 0 0; font-size: 12.5px; color: var(--muted); }
71
- .meta { text-align: right; font-size: 12px; color: var(--muted); line-height: 1.7; white-space: nowrap; }
72
- .meta .repo-state { color: var(--ink-soft); }
73
-
74
- /* ---------- KPI cards ---------- */
75
- .kpis { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; margin: 22px 0 8px; }
76
- .kpi { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px 18px 17px; position: relative; overflow: hidden; transition: border-color .18s var(--ease), transform .18s var(--ease); }
77
- .kpi:hover { border-color: var(--border-2); transform: translateY(-1px); }
78
- /* a quiet per-tier corner wash ties the status dot to the number's tone */
79
- .kpi::before { content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none; opacity: .6; background: radial-gradient(150px 105px at 100% 0%, var(--kpi-wash, transparent), transparent 70%); }
80
- .kpi .label, .kpi .num, .kpi .sub { position: relative; z-index: 1; }
81
- .kpi .label { font-size: 11px; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); font-weight: 600; }
82
- .kpi .num { font-size: 32px; font-weight: 750; letter-spacing: -.02em; margin: 9px 0 3px; line-height: 1; font-variant-numeric: tabular-nums; }
83
- .kpi .sub { font-size: 12px; color: var(--muted); }
84
- .kpi[data-tone="proven"] { --kpi-wash: var(--green-bg); }
85
- .kpi[data-tone="assoc"] { --kpi-wash: var(--amber-bg); }
86
- .kpi[data-tone="none"] { --kpi-wash: var(--red-bg); }
87
- .kpi[data-tone="proven"] .num { color: var(--green); }
88
- .kpi[data-tone="assoc"] .num { color: var(--amber); }
89
- .kpi[data-tone="none"] .num { color: var(--red); }
90
- .kpi .spark { position: absolute; right: 14px; top: 14px; width: 7px; height: 7px; border-radius: 50%; }
91
- .kpi[data-tone="proven"] .spark { background: var(--green); box-shadow: 0 0 0 4px var(--green-bg); }
92
- .kpi[data-tone="assoc"] .spark { background: var(--amber); box-shadow: 0 0 0 4px var(--amber-bg); }
93
- .kpi[data-tone="none"] .spark { background: var(--red); box-shadow: 0 0 0 4px var(--red-bg); }
94
-
95
- /* ---------- pipeline strip (which PR populated what) ---------- */
96
- .pipeline { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 16px 0 4px; padding: 11px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 12px; }
97
- .pipeline .plabel { color: var(--muted); font-weight: 600; margin-right: 2px; }
98
- .pstage { display: inline-flex; align-items: center; gap: 6px; padding: 4px 9px; border-radius: 20px; border: 1px solid var(--border-2); color: var(--ink-soft); background: var(--surface-2); font-variant-numeric: tabular-nums; }
99
- .pstage .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--faint); }
100
- .pstage[data-on="1"] { color: var(--green); border-color: var(--green-bd); background: var(--green-bg); }
101
- .pstage[data-on="1"] .dot { background: var(--green); }
102
- .pstage[data-on="partial"] { color: var(--amber); border-color: var(--amber-bd); background: var(--amber-bg); }
103
- .pstage[data-on="partial"] .dot { background: var(--amber); }
104
- .pstage .pr { opacity: .7; font-size: 11px; }
105
- .pstage .arrow { color: var(--faint); }
106
-
107
- .proof-callout { display: grid; grid-template-columns: auto 1fr; gap: 10px 12px; margin: 12px 0 4px; padding: 13px 15px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); }
108
- .proof-callout .mark { width: 9px; height: 9px; border-radius: 50%; margin-top: 5px; background: var(--blue); box-shadow: 0 0 0 4px var(--blue-bg); }
109
- .proof-callout h2 { margin: 0 0 3px; font-size: 13.5px; font-weight: 650; }
110
- .proof-callout p { margin: 0; color: var(--muted); font-size: 12.5px; max-width: 92ch; text-wrap: pretty; }
111
- .proof-callout .action { margin-top: 6px; color: var(--ink-soft); }
112
- .proof-callout code { font-family: var(--mono); color: var(--blue); background: var(--blue-bg); border: 1px solid var(--blue-bd); border-radius: 5px; padding: 1px 5px; }
113
- .proof-callout[data-state="proven"] .mark { background: var(--green); box-shadow: 0 0 0 4px var(--green-bg); }
114
- .proof-callout[data-state="attempted"] .mark { background: var(--amber); box-shadow: 0 0 0 4px var(--amber-bg); }
115
-
116
- /* ---------- zero-dynamic-proof explainer (rendered only when Dynamically Proven == 0) ---------- */
117
- .zero-proof { margin: 12px 0 4px; padding: 15px 18px; background: var(--surface); border: 1px solid var(--amber-bd); border-radius: var(--radius-sm); }
118
- .zero-proof[hidden] { display: none; }
119
- .zero-proof h2 { margin: 0 0 8px; font-size: 13.5px; font-weight: 650; color: var(--amber); }
120
- .zero-proof p { margin: 0 0 8px; color: var(--muted); font-size: 12.5px; max-width: 92ch; text-wrap: pretty; }
121
- .zero-proof p:last-child { margin-bottom: 0; }
122
-
123
- /* ---------- tier definitions legend (four honest display tiers) ---------- */
124
- .tier-legend { margin: 8px 0 4px; }
125
- .tier-legend .li { align-items: baseline; }
126
-
127
- /* ---------- tabs ---------- */
128
- nav.tabs { display: flex; gap: 4px; margin: 22px 0 0; border-bottom: 1px solid var(--border); }
129
- .tab { appearance: none; background: none; border: 0; color: var(--muted); font: inherit; font-weight: 550; padding: 11px 14px 13px; cursor: pointer; position: relative; display: inline-flex; align-items: center; gap: 8px; border-radius: 7px 7px 0 0; transition: color .15s; }
130
- .tab:hover { color: var(--ink-soft); }
131
- .tab[aria-selected="true"] { color: var(--ink); }
132
- .tab[aria-selected="true"]::after { content: ""; position: absolute; left: 6px; right: 6px; bottom: -1px; height: 2px; background: var(--accent); border-radius: 2px 2px 0 0; }
133
- .tab .count { font-size: 11px; font-weight: 600; color: var(--muted); background: var(--surface-3); padding: 1px 7px; border-radius: 20px; font-variant-numeric: tabular-nums; }
134
- .tab[aria-selected="true"] .count { color: var(--ink-soft); }
135
- .tab:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
136
-
137
- /* ---------- panels ---------- */
138
- .panel { display: none; padding-top: 22px; }
139
- .panel.active { display: block; animation: fade .26s var(--ease); }
140
- @keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
141
-
142
- /* note box — NO side stripe (full border + inline marker) */
143
- .note { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 15px 18px; margin-bottom: 20px; }
144
- .note h3 { margin: 0 0 5px; font-size: 13.5px; font-weight: 650; display: flex; align-items: center; gap: 8px; }
145
- .note h3 .ic { color: var(--accent); display: inline-flex; }
146
- .note p { margin: 0; font-size: 13px; color: var(--muted); max-width: 74ch; text-wrap: pretty; }
147
- .note b { color: var(--ink-soft); font-weight: 600; }
148
-
149
- /* ---------- two-column scan layout ---------- */
150
- .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
151
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; }
152
- .card .ttl { font-size: 11px; letter-spacing: .07em; text-transform: uppercase; color: var(--accent); font-weight: 650; margin: 0 0 12px; }
153
- .pill { display: inline-block; background: var(--accent-bg); border: 1px solid var(--accent-bd); color: #f6b079; border-radius: 7px; padding: 6px 11px; font-size: 12.5px; font-family: var(--mono); }
154
- .bignum { font-size: 30px; font-weight: 750; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
155
- .split2 { display: flex; gap: 30px; margin-top: 4px; }
156
- .split2 .v { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
157
- .split2 .v.b { color: var(--blue); } .split2 .v.p { color: var(--purple); }
158
- .split2 .k { font-size: 12px; color: var(--muted); }
159
- .svc-list { margin-top: 4px; max-height: 268px; overflow-y: auto; }
160
- .svc { display: flex; justify-content: space-between; align-items: center; padding: 8px 2px; border-bottom: 1px solid var(--border); font-size: 13px; }
161
- .svc:last-child { border-bottom: 0; }
162
- .svc .nm { font-family: var(--mono); color: var(--ink-soft); }
163
- .svc .ct { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
164
- .excl { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 13px 15px; font-size: 12.5px; color: var(--muted); margin-top: 10px; }
165
- .excl b { color: var(--ink-soft); }
166
-
167
- /* ---------- behaviors ---------- */
168
- .beh-layout { display: grid; grid-template-columns: 220px 1fr; gap: 16px; }
169
- .beh-side { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px; height: max-content; position: sticky; top: 16px; }
170
- .grp { width: 100%; text-align: left; appearance: none; border: 0; background: none; font: inherit; color: var(--muted); padding: 9px 12px; border-radius: var(--radius-sm); cursor: pointer; display: flex; justify-content: space-between; align-items: center; transition: background .14s, color .14s; }
171
- .grp:hover { background: var(--surface-2); color: var(--ink-soft); }
172
- .grp:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
173
- .grp[aria-pressed="true"] { background: var(--accent-bg); color: var(--accent); font-weight: 600; }
174
- .grp .gc { font-size: 11.5px; color: inherit; opacity: .8; font-variant-numeric: tabular-nums; }
175
- .beh-main { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
176
- .beh-row { padding: 13px 18px; border-bottom: 1px solid var(--border); transition: background .14s; cursor: pointer; }
177
- .beh-row:last-child { border-bottom: 0; }
178
- .beh-row:hover { background: var(--surface-2); }
179
- .beh-row:focus-visible { outline: 2px solid var(--blue); outline-offset: -2px; background: var(--surface-2); }
180
- .beh-row .sig { font-family: var(--mono); font-size: 13px; color: var(--ink); }
181
- .beh-row .desc { font-size: 12.5px; color: var(--muted); margin-top: 4px; max-width: 82ch; text-wrap: pretty; }
182
- .beh-row .filemeta { font-family: var(--mono); font-size: 11.5px; color: var(--faint); }
183
-
184
- /* badges — shared semantic vocabulary */
185
- .badge { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 20px; border: 1px solid transparent; vertical-align: middle; letter-spacing: .01em; }
186
- .badge .d { width: 6px; height: 6px; border-radius: 50%; }
187
- .b-proven { color: var(--green); background: var(--green-bg); border-color: var(--green-bd); } .b-proven .d { background: var(--green); }
188
- .b-assoc { color: var(--amber); background: var(--amber-bg); border-color: var(--amber-bd); } .b-assoc .d { background: var(--amber); }
189
- .b-none { color: var(--red); background: var(--red-bg); border-color: var(--red-bd); } .b-none .d { background: var(--red); }
190
- .b-hard { color: var(--blue); background: var(--blue-bg); border-color: var(--blue-bd); } .b-hard .d { background: var(--blue); }
191
- .b-fw { color: var(--amber); background: var(--amber-bg); border-color: var(--amber-bd); } .b-fw .d { background: var(--amber); }
192
- .b-entry { color: var(--purple);background: var(--purple-bg);border-color: var(--purple-bd); }
193
- .b-info { color: var(--muted); background: var(--surface-3); border-color: var(--border-2); }
194
- .b-risk { color: var(--red); background: var(--red-bg); border-color: var(--red-bd); }
195
-
196
- .beh-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
197
-
198
- /* ---------- behavior detail modal ---------- */
199
- .drill-overlay { display: none; position: fixed; inset: 0; z-index: var(--z-tooltip); padding: 24px; background: rgba(1,4,9,.66); justify-content: center; align-items: center; }
200
- .drill-overlay.open { display: flex; animation: fade .18s var(--ease); }
201
- .drill-card { position: relative; background: var(--surface); border: 1px solid var(--border-2); border-radius: var(--radius); padding: 22px 24px 24px; max-width: 620px; width: 100%; max-height: 82vh; overflow-y: auto; box-shadow: 0 16px 48px rgba(0,0,0,.5); }
202
- .drill-close { position: absolute; top: 12px; right: 14px; appearance: none; background: none; border: 0; color: var(--muted); font-size: 22px; line-height: 1; cursor: pointer; padding: 4px 8px; border-radius: 6px; }
203
- .drill-close:hover { color: var(--ink); background: var(--surface-2); }
204
- .drill-close:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
205
- .drill-title { font-family: var(--mono); font-size: 15px; font-weight: 650; color: var(--ink); margin: 0 34px 18px 0; text-wrap: balance; }
206
- .drill-section { margin-bottom: 15px; }
207
- .drill-section:last-child { margin-bottom: 0; }
208
- .drill-section h4 { font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--accent); font-weight: 650; margin: 0 0 6px; }
209
- .drill-section p { margin: 0; font-size: 13px; color: var(--muted); text-wrap: pretty; }
210
- .drill-section p.mono { font-family: var(--mono); font-size: 12px; color: var(--blue); word-break: break-all; }
211
- .drill-section .why-box { font-size: 12.5px; color: var(--ink-soft); background: var(--blue-bg); border: 1px solid var(--blue-bd); border-radius: var(--radius-sm); padding: 10px 12px; text-wrap: pretty; }
212
- .drill-section .action-box { font-size: 12.5px; color: var(--ink-soft); background: var(--green-bg); border: 1px solid var(--green-bd); border-radius: var(--radius-sm); padding: 10px 12px; text-wrap: pretty; }
213
-
214
- /* ---------- flows ---------- */
215
- .legend { display: flex; flex-wrap: wrap; gap: 18px; align-items: center; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 11px 16px; margin-bottom: 18px; font-size: 12px; color: var(--muted); }
216
- .legend .li { display: inline-flex; align-items: center; gap: 7px; }
217
- .legend .rail-demo { width: 22px; height: 0; border-top: 2px solid var(--blue); }
218
- .legend .rail-demo.fw { border-top: 2px dashed var(--amber); }
219
- .legend b { color: var(--ink-soft); font-weight: 600; }
220
-
221
- .flow { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 22px; margin-bottom: 16px; transition: border-color .16s var(--ease); }
222
- .flow:hover { border-color: var(--border-2); }
223
- .flow-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
224
- .flow-head h3 { margin: 0; font-size: 15.5px; font-weight: 650; letter-spacing: -.01em; text-wrap: balance; }
225
- .flow-tags { display: flex; gap: 7px; flex-wrap: wrap; }
226
- .trigger { font-family: var(--mono); font-size: 12.5px; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 9px 13px; margin: 14px 0 6px; display: flex; align-items: center; gap: 10px; }
227
- .trigger .tk { color: var(--faint); font-size: 11px; letter-spacing: .05em; text-transform: uppercase; }
228
- .trigger .verb { color: var(--green); font-weight: 700; }
229
- .trigger .path { color: var(--blue); }
230
-
231
- .steps { margin: 14px 0 0; padding: 0; list-style: none; position: relative; }
232
- .step { position: relative; padding: 0 0 18px 30px; }
233
- .step:last-child { padding-bottom: 0; }
234
- /* the connecting rail between dots, colored by the *incoming* hop tier */
235
- .step::before { content: ""; position: absolute; left: 6px; top: 14px; bottom: -4px; width: 0; border-left: 2px solid var(--blue); }
236
- .step:last-child::before { display: none; }
237
- .step[data-edge="framework-derived"]::before { border-left-style: dashed; border-left-color: var(--amber); }
238
- .step .node { position: absolute; left: 0; top: 3px; width: 13px; height: 13px; border-radius: 50%; background: var(--bg); border: 2px solid var(--blue); }
239
- .step[data-tier="framework-derived"] .node { border-color: var(--amber); border-style: dashed; }
240
- .step .sig { font-family: var(--mono); font-size: 13px; color: var(--ink); display: inline-flex; align-items: center; gap: 9px; flex-wrap: wrap; }
241
- .step .idx { color: var(--faint); font-size: 11px; margin-right: 2px; font-variant-numeric: tabular-nums; }
242
- .step .desc { font-size: 12.5px; color: var(--muted); margin-top: 3px; }
243
- .hop-tag { font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 20px; letter-spacing: .02em; }
244
- .hop-tag.hard { color: var(--blue); background: var(--blue-bg); border: 1px solid var(--blue-bd); }
245
- .hop-tag.fw { color: var(--amber); background: var(--amber-bg); border: 1px solid var(--amber-bd); }
246
-
247
- .flow-foot { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 12.5px; }
248
- .flow-foot .why { color: var(--muted); }
249
- .flow-foot .why b { color: var(--ink-soft); font-weight: 600; }
250
-
251
- /* ---------- AI-suggested candidate flows (worklist — never evidence) ----------
252
- Deliberately NEUTRAL: no tier colors (blue/amber) and no proof colors
253
- (green/red). Dashed borders + gray rails keep candidates visually apart. */
254
- .ai-flows { margin-top: 28px; padding-top: 20px; border-top: 1px dashed var(--border-2); }
255
- .badge.b-ai { color: var(--muted); background: var(--surface-3); border-color: var(--border-2); }
256
- .cflow { background: var(--surface); border: 1px dashed var(--border-2); border-radius: var(--radius); padding: 20px 22px; margin-bottom: 16px; }
257
- .cflow .flow-head h3 { color: var(--ink-soft); }
258
- .step.cstep::before { border-left: 2px dotted var(--border-2); }
259
- .step.cstep .node { border-color: var(--faint); border-style: dotted; }
260
- .hop-tag.match { color: var(--ink-soft); background: var(--surface-3); border: 1px solid var(--border-2); }
261
- .hop-tag.unv { color: var(--muted); background: var(--surface-2); border: 1px dashed var(--border-2); }
262
-
263
- /* ---------- priority risks ---------- */
264
- .risk { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px 20px; margin-bottom: 14px; transition: border-color .16s; }
265
- .risk:hover { border-color: var(--border-2); }
266
- .risk .rk { font-size: 12px; color: var(--accent); font-weight: 700; font-variant-numeric: tabular-nums; }
267
- .risk .ep { font-family: var(--mono); font-size: 14px; margin: 5px 0 0; }
268
- .risk .ep .verb { color: var(--green); font-weight: 700; }
269
- .risk .ep .path { color: var(--ink); }
270
- .risk .rdesc { font-size: 12.5px; color: var(--muted); margin: 9px 0 11px; max-width: 80ch; text-wrap: pretty; }
271
- .risk .rtags { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 12px; }
272
- .todo { background: var(--green-bg); border: 1px solid var(--green-bd); border-radius: var(--radius-sm); padding: 11px 14px; }
273
- .todo .th { font-size: 11px; letter-spacing: .05em; text-transform: uppercase; color: var(--green); font-weight: 700; margin-bottom: 3px; }
274
- .todo p { margin: 0; font-size: 12.5px; color: var(--ink-soft); }
275
-
276
- .empty { text-align: center; padding: 46px 20px; color: var(--muted); }
277
- .empty .ic { font-size: 26px; margin-bottom: 8px; opacity: .6; }
278
- .empty b { color: var(--ink-soft); }
279
-
280
- @media (max-width: 880px) {
281
- .kpis { grid-template-columns: repeat(2, 1fr); }
282
- .cols, .beh-layout { grid-template-columns: 1fr; }
283
- .beh-side { position: static; }
284
- .meta { display: none; }
285
- }
286
- @media (prefers-reduced-motion: reduce) {
287
- *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
288
- }
8
+ :root {
9
+ --bg:#0d1117; --s1:#161b22; --s2:#1c2128; --s3:#21262d;
10
+ --bd:#2b313a; --bd2:#383f49;
11
+ --ink:#e6edf3; --ink2:#c9d3df; --muted:#9aa4b1; --faint:#6e7681;
12
+ --green:#3fb950; --gbg:rgba(63,185,80,.13); --gbd:rgba(63,185,80,.38);
13
+ --amber:#e3b341; --abg:rgba(227,179,65,.13); --abd:rgba(227,179,65,.38);
14
+ --red:#f85149; --rbg:rgba(248,81,73,.12); --rbd:rgba(248,81,73,.35);
15
+ --blue:#58a6ff; --bbg:rgba(88,166,255,.12); --bbd:rgba(88,166,255,.35);
16
+ --orange:#f0883e;--obg:rgba(240,136,62,.12); --obd:rgba(240,136,62,.40);
17
+ --mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
18
+ --sans:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
19
+ --ease:cubic-bezier(.22,1,.36,1);
20
+ }
21
+ *{box-sizing:border-box;margin:0;padding:0}
22
+ body{background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:14px;line-height:1.55;-webkit-font-smoothing:antialiased}
23
+ a{color:var(--blue);text-decoration:none}
24
+ ::-webkit-scrollbar{width:7px;height:7px}
25
+ ::-webkit-scrollbar-thumb{background:#30363d;border-radius:4px;border:2px solid var(--bg)}
26
+
27
+ .wrap{max-width:1100px;margin:0 auto;padding:24px 24px 80px}
28
+
29
+ /* HEADER */
30
+ .hdr{display:flex;align-items:center;justify-content:space-between;gap:16px;padding-bottom:14px;border-bottom:1px solid var(--bd)}
31
+ .hdr-left{display:flex;align-items:center;gap:10px}
32
+ .logo{width:28px;height:28px;border-radius:6px;background:linear-gradient(145deg,#f0883e,#c95c1a);display:grid;place-items:center;font-weight:800;font-size:13px;color:#1a0a02;flex-shrink:0}
33
+ .hdr-name{font-size:15px;font-weight:650;letter-spacing:-.01em}
34
+ .hdr-right{font-size:11.5px;color:var(--muted);display:flex;gap:10px;align-items:center}
35
+ .hdr-tag{background:var(--s2);border:1px solid var(--bd);border-radius:4px;padding:2px 7px;font-family:var(--mono);font-size:10.5px;color:var(--ink2)}
36
+
37
+ /* KPI STRIP */
38
+ .kpis{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin:16px 0 0}
39
+ .kpi{background:var(--s1);border:1px solid var(--bd);border-radius:9px;padding:13px 14px;position:relative;overflow:hidden}
40
+ .kpi::before{content:"";position:absolute;inset:0;pointer-events:none;opacity:.4;background:radial-gradient(90px 70px at 100% 0%,var(--kw,transparent),transparent 70%)}
41
+ .kpi-lbl{font-size:10px;color:var(--muted);font-weight:600;letter-spacing:.05em;text-transform:uppercase;position:relative}
42
+ .kpi-num{font-size:24px;font-weight:750;letter-spacing:-.02em;margin:5px 0 1px;line-height:1;font-variant-numeric:tabular-nums;position:relative}
43
+ .kpi-sub{font-size:10.5px;color:var(--faint);position:relative}
44
+ .kpi[data-t="total"]{--kw:rgba(88,166,255,.08)} .kpi[data-t="total"] .kpi-num{color:var(--ink)}
45
+ .kpi[data-t="proven"]{--kw:var(--gbg)} .kpi[data-t="proven"] .kpi-num{color:var(--green)}
46
+ .kpi[data-t="signal"]{--kw:var(--abg)} .kpi[data-t="signal"] .kpi-num{color:var(--amber)}
47
+ .kpi[data-t="reach"]{--kw:var(--bbg)} .kpi[data-t="reach"] .kpi-num{color:var(--blue)}
48
+ .kpi[data-t="nosig"]{--kw:var(--rbg)} .kpi[data-t="nosig"] .kpi-num{color:var(--red)}
49
+
50
+ /* TABS */
51
+ nav.tabs{display:flex;gap:2px;margin:18px 0 0;border-bottom:1px solid var(--bd)}
52
+ .tab{appearance:none;background:none;border:0;color:var(--muted);font:inherit;font-size:13px;font-weight:550;padding:9px 13px 10px;cursor:pointer;position:relative;display:inline-flex;align-items:center;gap:5px;border-radius:5px 5px 0 0;transition:color .12s}
53
+ .tab:hover{color:var(--ink2)}
54
+ .tab[aria-selected="true"]{color:var(--ink)}
55
+ .tab[aria-selected="true"]::after{content:"";position:absolute;left:5px;right:5px;bottom:-1px;height:2px;background:var(--orange);border-radius:2px 2px 0 0}
56
+ .tab .tc{font-size:10px;font-weight:600;color:var(--muted);background:var(--s3);padding:1px 5px;border-radius:20px;font-variant-numeric:tabular-nums}
57
+
58
+ /* PANELS */
59
+ .panel{display:none;padding-top:16px}
60
+ .panel.active{display:block;animation:fi .18s var(--ease)}
61
+ @keyframes fi{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:none}}
62
+
63
+ /* SECTION BRIDGE — one-liner that connects developer vocab to OrangePro concepts */
64
+ .bridge{font-size:12.5px;color:var(--muted);margin-bottom:14px;padding:0;line-height:1.6}
65
+ .bridge b{color:var(--ink2);font-weight:600}
66
+ .bridge code{font-family:var(--mono);font-size:11px;background:var(--s2);border:1px solid var(--bd);border-radius:3px;padding:1px 4px;color:var(--ink2)}
67
+
68
+ /* CODEBASE */
69
+ .cols2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
70
+ .card{background:var(--s1);border:1px solid var(--bd);border-radius:9px;padding:16px}
71
+ .card-lbl{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--orange);font-weight:650;margin:0 0 8px}
72
+ .fw-pill{display:inline-block;background:var(--obg);border:1px solid var(--obd);color:#f6b079;border-radius:5px;padding:3px 9px;font-size:12px;font-family:var(--mono)}
73
+ .bignum{font-size:22px;font-weight:750;font-variant-numeric:tabular-nums}
74
+ .split2{display:flex;gap:16px;margin-top:4px}
75
+ .split2 .v{font-size:17px;font-weight:700;font-variant-numeric:tabular-nums}
76
+ .split2 .vb{color:var(--blue)} .split2 .vp{color:#bc8cff}
77
+ .split2 .k{font-size:10.5px;color:var(--muted)}
78
+ .svc-list{margin-top:6px;max-height:200px;overflow-y:auto}
79
+ .svc{display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--bd);font-size:12px}
80
+ .svc:last-child{border-bottom:0}
81
+ .svc .nm{font-family:var(--mono);color:var(--ink2)}
82
+ .svc .ct{color:var(--muted);font-variant-numeric:tabular-nums}
83
+
84
+ /* BEHAVIORS card grid with tier glow */
85
+ .beh-layout{display:grid;grid-template-columns:160px 1fr;gap:12px}
86
+ .beh-side{background:var(--s1);border:1px solid var(--bd);border-radius:9px;padding:5px;height:max-content;position:sticky;top:12px}
87
+ .beh-side-lbl{font-size:9.5px;color:var(--faint);font-weight:600;letter-spacing:.05em;text-transform:uppercase;padding:5px 8px 3px}
88
+ .beh-search{width:100%;margin:2px 0 8px;padding:7px 9px;font:inherit;font-size:12px;color:var(--ink);background:var(--s);border:1px solid var(--bd);border-radius:6px;outline:none}
89
+ .beh-search:focus{border-color:var(--amber)}
90
+ .grp{width:100%;text-align:left;appearance:none;border:0;background:none;font:inherit;font-size:12px;color:var(--muted);padding:6px 8px;border-radius:5px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;transition:background .1s,color .1s}
91
+ .grp:hover{background:var(--s2);color:var(--ink2)}
92
+ .grp[aria-pressed="true"]{background:var(--obg);color:var(--orange);font-weight:600}
93
+ .grp .gc{font-size:10px;opacity:.8}
94
+ .beh-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:9px}
95
+ .beh-card{background:var(--s1);border:1px solid var(--bd);border-radius:9px;padding:12px 14px;cursor:pointer;transition:border-color .12s,transform .12s;position:relative;overflow:hidden}
96
+ .beh-card::before{content:"";position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .15s;background:radial-gradient(120px 90px at 0% 100%,var(--gw,transparent),transparent 70%)}
97
+ .beh-card:hover{transform:translateY(-1px);border-color:var(--bd2)}
98
+ .beh-card:hover::before{opacity:1}
99
+ .beh-card[data-t="proven"]{--gw:var(--gbg);border-color:rgba(63,185,80,.22)}
100
+ .beh-card[data-t="proven"]:hover{border-color:var(--gbd)}
101
+ .beh-card[data-t="assoc"]{--gw:var(--abg)}
102
+ .beh-card[data-t="assoc"]:hover{border-color:var(--abd)}
103
+ .beh-card[data-t="reach"]{--gw:var(--bbg)}
104
+ .beh-card[data-t="reach"]:hover{border-color:var(--bbd)}
105
+ .beh-card[data-t="nosig"]{--gw:var(--rbg)}
106
+ .beh-card[data-t="nosig"]:hover{border-color:var(--rbd)}
107
+ .bc-top{display:flex;align-items:flex-start;justify-content:space-between;gap:6px}
108
+ .bc-sig{font-family:var(--mono);font-size:12px;font-weight:600;color:var(--ink);word-break:break-all;flex:1}
109
+ .bc-badge{flex-shrink:0}
110
+ .bc-file{font-family:var(--mono);font-size:10px;color:var(--faint);margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
111
+ .bc-bar{height:3px;border-radius:0 0 9px 9px;position:absolute;bottom:0;left:0;right:0}
112
+ .beh-card[data-t="proven"] .bc-bar{background:var(--green)}
113
+ .beh-card[data-t="assoc"] .bc-bar{background:var(--amber)}
114
+ .beh-card[data-t="reach"] .bc-bar{background:var(--blue)}
115
+ .beh-card[data-t="nosig"] .bc-bar{background:var(--red)}
116
+
117
+ /* BADGES */
118
+ .badge{display:inline-flex;align-items:center;gap:3px;font-size:10px;font-weight:600;padding:2px 6px;border-radius:20px;border:1px solid transparent}
119
+ .badge .d{width:4px;height:4px;border-radius:50%}
120
+ .b-proven{color:var(--green);background:var(--gbg);border-color:var(--gbd)} .b-proven .d{background:var(--green)}
121
+ .b-signal{color:var(--amber);background:var(--abg);border-color:var(--abd)} .b-signal .d{background:var(--amber)}
122
+ .b-reach{color:var(--blue);background:var(--bbg);border-color:var(--bbd)} .b-reach .d{background:var(--blue)}
123
+ .b-nosig{color:var(--red);background:var(--rbg);border-color:var(--rbd)} .b-nosig .d{background:var(--red)}
124
+ .b-risk{color:var(--red);background:var(--rbg);border-color:var(--rbd)} .b-risk .d{background:var(--red)}
125
+ .b-info{color:var(--muted);background:var(--s3);border-color:var(--bd2)}
126
+
127
+ /* FLOWS */
128
+ .flow-card{background:var(--s1);border:1px solid var(--bd);border-radius:9px;margin-bottom:12px;overflow:hidden;transition:border-color .12s}
129
+ .flow-card:hover{border-color:var(--bd2)}
130
+ .flow-head{padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;border-bottom:1px solid var(--bd)}
131
+ .flow-head h3{font-size:13.5px;font-weight:650;letter-spacing:-.01em;margin:0}
132
+ .flow-tags{display:flex;gap:5px;flex-wrap:wrap}
133
+ /* horizontal chain */
134
+ .flow-chain{padding:14px 16px;display:flex;align-items:center;gap:0;overflow-x:auto;min-height:60px}
135
+ .flow-chain::-webkit-scrollbar{height:3px}
136
+ .fn{display:flex;flex-direction:column;align-items:center;flex-shrink:0;min-width:80px;max-width:150px}
137
+ .fn-box{background:var(--s2);border:1px solid var(--bd2);border-radius:7px;padding:6px 10px;font-family:var(--mono);font-size:11px;color:var(--ink2);text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:145px;width:100%;transition:border-color .15s}
138
+ .fn-box.fnb-entry{background:var(--bbg);border-color:var(--bbd);color:var(--blue)}
139
+ .fn-box.fnb-proven{background:var(--gbg);border-color:var(--gbd);color:var(--green);animation:pulse 2.4s ease infinite}
140
+ .fn-box.fnb-assoc{background:var(--abg);border-color:var(--abd);color:var(--amber)}
141
+ @keyframes pulse{0%,100%{box-shadow:0 0 0 0 var(--gbg)}50%{box-shadow:0 0 0 4px transparent}}
142
+ .fe{flex:1;min-width:20px;max-width:40px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
143
+ .fe-line{width:100%;height:2px}
144
+ .fe-line.fel-hard{background:linear-gradient(90deg,var(--blue),rgba(88,166,255,.4))}
145
+ .fe-line.fel-fw{background:repeating-linear-gradient(90deg,var(--amber) 0,var(--amber) 3px,transparent 3px,transparent 6px)}
146
+ /* vertical chain for 5+ nodes */
147
+ .flow-chain.fc-vert{flex-direction:column;align-items:flex-start;gap:0;overflow-x:visible;padding:16px 20px}
148
+ .fc-vert .fn{flex-direction:row;align-items:center;min-width:0;max-width:none;gap:10px}
149
+ .fc-vert .fn-box{text-align:left;white-space:normal;max-width:300px;width:auto}
150
+ .fc-vert .fe{min-width:0;max-width:none;width:auto;margin-left:18px;padding:3px 0}
151
+ .fc-vert .fe-line{width:2px;height:20px;min-width:2px}
152
+ .fc-vert .fe-line.fel-hard{background:linear-gradient(180deg,var(--blue),rgba(88,166,255,.4))}
153
+ .fc-vert .fe-line.fel-fw{background:repeating-linear-gradient(180deg,var(--amber) 0,var(--amber) 3px,transparent 3px,transparent 6px)}
154
+ /* AI flows */
155
+ .ai-sec{margin-top:20px;padding-top:14px;border-top:1px dashed var(--bd2)}
156
+ .ai-lbl{font-size:11px;color:var(--faint);font-weight:600;margin-bottom:10px}
157
+ .ai-card{background:var(--s1);border:1px dashed var(--bd2);border-radius:9px;margin-bottom:10px;overflow:hidden}
158
+ .ai-card .fn-box{border-style:dashed}
159
+ .ai-card .fe-line{opacity:.45}
160
+
161
+ /* RISKS */
162
+ .risk-card{background:var(--s1);border:1px solid var(--bd);border-radius:9px;padding:14px 16px;margin-bottom:10px}
163
+ .risk-tools{display:flex;align-items:center;gap:8px;margin:0 0 12px;flex-wrap:wrap}
164
+ .risk-filter{appearance:none;border:1px solid var(--bd);background:var(--s1);color:var(--muted);border-radius:20px;padding:5px 10px;font:inherit;font-size:12px;font-weight:600;cursor:pointer}
165
+ .risk-filter:hover{border-color:var(--bd2);color:var(--ink2)}
166
+ .risk-filter[aria-pressed="true"]{border-color:var(--orange);background:var(--obg);color:var(--orange)}
167
+ .risk-rank{font-size:10px;color:var(--orange);font-weight:700;margin-bottom:3px}
168
+ .risk-ep{font-family:var(--mono);font-size:13px;margin:0 0 6px}
169
+ .risk-ep .v{color:var(--green);font-weight:700}
170
+ .risk-desc{font-size:12px;color:var(--muted);margin:0 0 8px;max-width:72ch}
171
+ .risk-tags{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:8px}
172
+ .todo{background:var(--gbg);border:1px solid var(--gbd);border-radius:6px;padding:8px 11px;font-size:11.5px;color:var(--ink2)}
173
+
174
+ /* DRILL MODAL */
175
+ .drill-overlay{display:none;position:fixed;inset:0;z-index:600;padding:24px;background:rgba(1,4,9,.7);justify-content:center;align-items:center}
176
+ .drill-overlay.open{display:flex;animation:fi .14s var(--ease)}
177
+ .drill-card{position:relative;background:var(--s1);border:1px solid var(--bd2);border-radius:11px;padding:18px 20px;max-width:480px;width:100%;max-height:75vh;overflow-y:auto;box-shadow:0 16px 50px rgba(0,0,0,.5)}
178
+ .drill-close{position:absolute;top:8px;right:10px;appearance:none;background:none;border:0;color:var(--muted);font-size:18px;cursor:pointer;padding:4px 7px;border-radius:4px}
179
+ .drill-close:hover{color:var(--ink);background:var(--s2)}
180
+ .drill-title{font-family:var(--mono);font-size:13px;font-weight:650;margin:0 28px 12px 0;word-break:break-all}
181
+ .drill-row{margin-bottom:10px}
182
+ .drill-row:last-child{margin-bottom:0}
183
+ .drill-row h4{font-size:9.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--orange);font-weight:650;margin:0 0 4px}
184
+ .drill-row p{margin:0;font-size:12px;color:var(--muted)}
185
+ .drill-row .mono{font-family:var(--mono);font-size:11px;color:var(--blue)}
186
+ .abox{font-size:12px;color:var(--ink2);border-radius:6px;padding:8px 10px}
187
+ .abox code{font-family:var(--mono);font-size:10.5px;background:rgba(0,0,0,.3);border-radius:3px;padding:1px 4px}
188
+ .abox.ax-proven{background:var(--gbg);border:1px solid var(--gbd)}
189
+ .abox.ax-signal{background:var(--abg);border:1px solid var(--abd)}
190
+ .abox.ax-reach{background:var(--bbg);border:1px solid var(--bbd)}
191
+ .abox.ax-nosig{background:var(--rbg);border:1px solid var(--rbd)}
192
+
193
+ /* GENERATED TEST SAMPLES */
194
+ .gen-tests{margin-top:10px;border-top:1px solid var(--bd);padding-top:10px}
195
+ .gen-tests-lbl{font-size:10px;color:var(--orange);font-weight:650;letter-spacing:.04em;text-transform:uppercase;margin-bottom:6px;display:flex;align-items:center;gap:5px}
196
+ .gen-tests-lbl::before{content:"";width:12px;height:12px;background:var(--obg);border:1px solid var(--obd);border-radius:3px;display:inline-block}
197
+ .gen-test{background:var(--s2);border:1px solid var(--bd);border-radius:7px;margin-bottom:6px;overflow:hidden}
198
+ .gen-test-head{display:flex;align-items:center;justify-content:space-between;padding:8px 11px;cursor:pointer;gap:8px}
199
+ .gen-test-head:hover{background:var(--s3)}
200
+ .gen-test-name{font-family:var(--mono);font-size:11.5px;color:var(--ink2);font-weight:600}
201
+ .gen-test-assert{font-size:10.5px;color:var(--muted);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
202
+ .gen-test-toggle{color:var(--faint);font-size:10px;flex-shrink:0;transition:transform .15s}
203
+ .gen-test.open .gen-test-toggle{transform:rotate(180deg)}
204
+ .gen-test-body{display:none;padding:0 11px 10px;font-family:var(--mono);font-size:11px;line-height:1.6;color:var(--ink2);white-space:pre-wrap;border-top:1px solid var(--bd)}
205
+ .gen-test.open .gen-test-body{display:block}
206
+ /* TESTING CATEGORIES STRIP */
207
+ .cat-strip{margin-top:10px;padding-top:9px;border-top:1px solid var(--bd)}
208
+ .cat-strip-lbl{font-size:9.5px;color:var(--muted);font-weight:600;letter-spacing:.04em;text-transform:uppercase;margin-bottom:6px}
209
+ .cat-pills{display:flex;flex-wrap:wrap;gap:5px}
210
+ .cat-pill{font-size:10px;padding:3px 8px;border-radius:4px;font-weight:600;letter-spacing:.01em;display:inline-flex;align-items:center;gap:4px}
211
+ .cat-pill.cp-shown{background:var(--gbg);border:1px solid var(--gbd);color:var(--green)}
212
+ .cat-pill.cp-locked{background:transparent;border:1px dashed var(--bd2);color:var(--faint)}
213
+ .cat-pill.cp-locked::after{content:"\\1F512";font-size:8px}
214
+ /* PAYWALL CTA */
215
+ .paywall{background:linear-gradient(135deg,rgba(240,136,62,.08),rgba(240,136,62,.02));border:1px dashed var(--obd);border-radius:9px;padding:14px 16px;margin-top:14px;text-align:center}
216
+ .paywall-num{font-size:18px;font-weight:750;color:var(--orange);font-variant-numeric:tabular-nums}
217
+ .paywall-txt{font-size:12px;color:var(--muted);margin:4px 0 10px}
218
+ .paywall-btn{display:inline-flex;align-items:center;gap:6px;background:var(--orange);color:#1a0a02;font-size:12px;font-weight:700;padding:7px 14px;border-radius:6px;text-decoration:none;transition:opacity .12s}
219
+ .paywall-btn:hover{opacity:.85}
220
+
221
+ @media(max-width:800px){
222
+ .kpis{grid-template-columns:repeat(3,1fr)}
223
+ .cols2,.beh-layout{grid-template-columns:1fr}
224
+ .beh-side{position:static}
225
+ }
226
+ @media(max-width:500px){.kpis{grid-template-columns:repeat(2,1fr)}}
227
+ @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;transition-duration:.001ms!important}}
289
228
  </style>
290
229
  </head>
291
230
  <body>
292
231
  <div class="wrap">
293
232
 
294
- <header class="top">
295
- <div class="brand">
296
- <div class="logo">O</div>
297
- <div>
298
- <h1 id="repo-name">—</h1>
299
- <p>Behavior Coverage Report</p>
300
- </div>
301
- </div>
302
- <div class="meta">
303
- <div>Scanned: <span id="scan-date">—</span></div>
304
- <div class="repo-state" id="analysis-kind">Static analysis</div>
305
- </div>
306
- </header>
307
-
308
- <section class="kpis" id="kpis" aria-label="Coverage summary"></section>
309
-
310
- <div class="legend tier-legend" aria-label="Tier definitions">
311
- <span class="li" title="Mutation killed — a test broke when OrangePro changed this behavior. Highest confidence."><span class="badge b-proven"><span class="d"></span>Dynamically Proven</span> Mutation killed — a test broke when OrangePro changed this behavior. Highest confidence.</span>
312
- <span class="li" title="A test file appears to import or call this behavior. Not dynamically verified."><span class="badge b-assoc"><span class="d"></span>Statically Linked</span> A test file appears to import or call this behavior. Not dynamically verified.</span>
313
- <span class="li" title="Appears in a static flow from an entry point, but no test signal was found."><span class="badge b-none"><span class="d"></span>Reachable Untested</span> Appears in a static flow from an entry point, but no test signal was found.</span>
314
- <span class="li" title="No test or static-flow signal found yet."><span class="badge b-none"><span class="d"></span>No Signal</span> No test or static-flow signal found yet.</span>
233
+ <header class="hdr">
234
+ <div class="hdr-left">
235
+ <div class="logo">O</div>
236
+ <span class="hdr-name" id="repo-name">—</span>
315
237
  </div>
316
-
317
- <div class="pipeline" id="pipeline" aria-label="Evidence pipeline status"></div>
318
- <section class="proof-callout" id="proof-guidance" aria-label="Dynamic proof guidance"></section>
319
- <section class="zero-proof" id="zero-proof" aria-label="Why dynamic proof is zero" hidden></section>
320
-
321
- <nav class="tabs" role="tablist" aria-label="Report sections">
322
- <button class="tab" role="tab" aria-selected="true" data-tab="codebase">Your Codebase</button>
323
- <button class="tab" role="tab" aria-selected="false" data-tab="behaviors">Behaviors <span class="count" id="t-beh">0</span></button>
324
- <button class="tab" role="tab" aria-selected="false" data-tab="flows">Flows <span class="count" id="t-flow">0</span></button>
325
- <button class="tab" role="tab" aria-selected="false" data-tab="risks">Priority Risks <span class="count" id="t-risk">0</span></button>
326
- </nav>
327
-
328
- <!-- YOUR CODEBASE -->
329
- <section class="panel active" id="panel-codebase" role="tabpanel">
330
- <div class="note">
331
- <h3><span class="ic" aria-hidden="true">◆</span> What we scanned</h3>
332
- <p>We read your repository's source, identified service classes, route handlers, and test files, then classified each public method as a <b>testable behavior</b> or infrastructure plumbing. No code is executed in this pass.</p>
333
- </div>
334
- <div class="cols">
335
- <div class="card">
336
- <p class="ttl">Framework</p>
337
- <span class="pill" id="framework">—</span>
338
- <p class="ttl" style="margin-top:22px">Services discovered <span id="svc-count" style="color:var(--ink);font-family:var(--mono)"></span></p>
339
- <div class="svc-list" id="svc-list"></div>
340
- </div>
341
- <div class="card">
342
- <p class="ttl">Test files</p>
343
- <div class="bignum" id="test-total">—</div>
344
- <div class="split2">
345
- <div><div class="v b" id="test-int">—</div><div class="k">Integration</div></div>
346
- <div><div class="v p" id="test-unit">—</div><div class="k">Unit / snapshot</div></div>
347
- </div>
348
- <p class="ttl" style="margin-top:24px">What we excluded</p>
349
- <div class="excl" id="excluded"></div>
350
- </div>
351
- </div>
352
- </section>
353
-
354
- <!-- BEHAVIORS -->
355
- <section class="panel" id="panel-behaviors" role="tabpanel">
356
- <div class="note">
357
- <h3><span class="ic" aria-hidden="true">◆</span> How we define a behavior</h3>
358
- <p>A <b>behavior</b> is a discrete unit of functionality a user or system can trigger and observe an outcome from: the smallest thing you'd write an acceptance criterion for. A service method that creates, updates, deletes, or retrieves data with observable side effects.</p>
359
- </div>
360
- <div class="beh-layout">
361
- <aside class="beh-side" id="beh-groups" aria-label="Filter by package"></aside>
362
- <div class="beh-main" id="beh-list"></div>
363
- </div>
364
- </section>
365
-
366
- <!-- FLOWS -->
367
- <section class="panel" id="panel-flows" role="tabpanel">
368
- <div class="note">
369
- <h3><span class="ic" aria-hidden="true">◆</span> How behaviors compose into flows</h3>
370
- <p>A <b>flow</b> is the implementation chain we can statically compose from a single entry point, crossing service boundaries. Each hop is a real call in your source, labelled by how exactly we resolved it. A flow is only as trustworthy as its weakest hop, and <b>reachable ≠ proven</b>: reachability is static, while proof requires a passing test that exercises the path without mocking its target.</p>
371
- </div>
372
- <div class="legend">
373
- <span class="li"><span class="rail-demo"></span> <b>Hard reachable</b>: exact call, both ends authored</span>
374
- <span class="li"><span class="rail-demo fw"></span> <b>Framework-derived</b>: generated/SDK target, deterministic but not authored</span>
375
- <span class="li"><span class="badge b-proven"><span class="d"></span>Dynamic proof</span> real test, target not mocked</span>
238
+ <div class="hdr-right">
239
+ <span class="hdr-tag" id="framework">—</span>
240
+ <span id="scan-date">—</span>
241
+ </div>
242
+ </header>
243
+
244
+ <section class="kpis" id="kpis"></section>
245
+
246
+ <nav class="tabs" role="tablist">
247
+ <button class="tab" role="tab" aria-selected="true" data-tab="codebase">Your Code</button>
248
+ <button class="tab" role="tab" aria-selected="false" data-tab="behaviors">Behaviors <span class="tc" id="t-beh">—</span></button>
249
+ <button class="tab" role="tab" aria-selected="false" data-tab="flows">Flows <span class="tc" id="t-flow">—</span></button>
250
+ <button class="tab" role="tab" aria-selected="false" data-tab="risks">Risks <span class="tc" id="t-risk">—</span></button>
251
+ </nav>
252
+
253
+ <!-- TAB 1: YOUR CODE — familiar territory -->
254
+ <section class="panel active" id="panel-codebase" role="tabpanel">
255
+ <p class="bridge">We scanned your repo and found <b id="br-methods">—</b> public methods across <b id="br-services">—</b> services, with <b id="br-tests">—</b> test files. Here's what we're working with.</p>
256
+ <div class="cols2">
257
+ <div class="card">
258
+ <p class="card-lbl">Framework</p>
259
+ <span class="fw-pill" id="fw-pill">—</span>
260
+ <p class="card-lbl" style="margin-top:14px">Services (by exported method count)</p>
261
+ <div class="svc-list" id="svc-list"></div>
376
262
  </div>
377
- <div id="flow-list"></div>
378
-
379
- <!-- AI-SUGGESTED CANDIDATE FLOWS — worklist only, never evidence, never counted -->
380
- <div class="ai-flows" id="ai-flows" hidden>
381
- <div class="note">
382
- <h3><span class="ic" aria-hidden="true" style="color:var(--muted)">◇</span> AI-suggested flows — verify these</h3>
383
- <p><span id="ai-flow-headline"></span> Proposed by a model over existing graph ids only. <b>Not evidence — candidate chains to verify.</b> They are never counted in flow totals, tiers, or coverage; a hop marker only says whether the proposed hop matches an already-known call edge.</p>
263
+ <div class="card">
264
+ <p class="card-lbl">Test files found</p>
265
+ <div class="bignum" id="test-total">—</div>
266
+ <div class="split2">
267
+ <div><div class="v vb" id="test-int">—</div><div class="k">Integration</div></div>
268
+ <div><div class="v vp" id="test-unit">—</div><div class="k">Unit</div></div>
384
269
  </div>
385
- <div id="ai-flow-list"></div>
386
270
  </div>
387
- </section>
271
+ </div>
272
+ </section>
273
+
274
+ <!-- TAB 2: BEHAVIORS — bridge from "methods" to "behaviors" -->
275
+ <section class="panel" id="panel-behaviors" role="tabpanel">
276
+ <p class="bridge">Each card below is a <b>public method</b> in your code that has an observable outcome — we call it a <b>behavior</b>. The color tells you how well it's tested: <span style="color:var(--green)">green</span> = a test proves it breaks if you change it, <span style="color:var(--amber)">amber</span> = a test touches it but doesn't prove breakage, <span style="color:var(--red)">red</span> = nothing tests it.</p>
277
+ <div class="beh-layout">
278
+ <aside class="beh-side" id="beh-groups">
279
+ <div class="beh-side-lbl">Search</div>
280
+ <input id="beh-search" class="beh-search" type="search" placeholder="name or file…" aria-label="Search behaviors by name or file">
281
+ <div class="beh-side-lbl">Evidence</div>
282
+ <div id="tier-filter"></div>
283
+ <div class="beh-side-lbl">Package</div>
284
+ </aside>
285
+ <div class="beh-grid" id="beh-grid"></div>
286
+ </div>
287
+ </section>
288
+
289
+ <!-- TAB 3: FLOWS — bridge from "methods" to "user journeys" -->
290
+ <section class="panel" id="panel-flows" role="tabpanel">
291
+ <p class="bridge">A <b>flow</b> is the sequence of methods that execute when a real request hits your system. Each box is a method you already saw in the Behaviors tab — but here they're connected as a call chain. Solid lines = hard-coded calls. Dashed = framework-derived. <span style="color:var(--green)">Green boxes</span> pulse because a test proves they break if mutated.</p>
292
+ <div id="flow-list"></div>
293
+ <div class="ai-sec" id="ai-sec" hidden>
294
+ <div class="ai-lbl">AI-suggested flows — plausible paths, not proven. Dashed borders = unverified.</div>
295
+ <div id="ai-list"></div>
296
+ </div>
297
+ </section>
388
298
 
389
- <!-- PRIORITY RISKS -->
390
- <section class="panel" id="panel-risks" role="tabpanel">
391
- <div class="note">
392
- <h3><span class="ic" aria-hidden="true">◆</span> What to test first</h3>
393
- <p>Ranked by the OrangePro risk score: blast radius (how many parts depend on this), code churn, and entry-point proximity. Fix these first to cut production-incident probability. This list is ranked, not exhaustive.</p>
394
- </div>
395
- <div id="risk-list"></div>
396
- </section>
299
+ <!-- TAB 4: RISKS — actionable -->
300
+ <section class="panel" id="panel-risks" role="tabpanel">
301
+ <p class="bridge">These are the flows with the highest blast radius and the weakest test coverage. Each one tells you exactly what to do next.</p>
302
+ <div class="risk-tools" id="risk-tools"></div>
303
+ <div id="risk-list"></div>
304
+ </section>
397
305
 
398
306
  </div>
399
307
 
400
- <!-- BEHAVIOR DETAIL MODAL — display-only drill into one behavior row -->
401
308
  <div class="drill-overlay" id="drill">
402
- <div class="drill-card" role="dialog" aria-modal="true" aria-labelledby="drill-title">
403
- <button class="drill-close" id="drill-close" type="button" aria-label="Close behavior details">&times;</button>
309
+ <div class="drill-card" role="dialog" aria-modal="true">
310
+ <button class="drill-close" id="drill-close" type="button">&times;</button>
404
311
  <div id="drill-content"></div>
405
312
  </div>
406
313
  </div>
407
314
 
408
315
  <script>
409
- /* =====================================================================
410
- window.DATA — the backend graph contract. Each block is populated by
411
- a specific slice/PR; \`pipeline[].on\` drives the status strip so the
412
- report visibly fills in as backend work lands.
413
- Numbers below are honest Medusa-representative values from review runs
414
- (1,173 behavior denominator · 1,744 hard CALLS · 421 injected · 44
415
- framework-derived) plus one typed-NestJS flow showing the hard+proven
416
- target state.
417
- ===================================================================== */
418
316
  window.DATA = __ORANGEPRO_DATA__;
419
317
 
420
- /* ===================== render ===================== */
421
- (function () {
422
- const D = window.DATA, $ = (s, r=document) => r.querySelector(s), el = (t, c, h) => { const e = document.createElement(t); if (c) e.className = c; if (h != null) e.innerHTML = h; return e; };
423
- const esc = s => String(s).replace(/[&<>]/g, m => ({ "&":"&amp;","<":"&lt;",">":"&gt;" }[m]));
424
- const tap = (v, fn) => { fn(v); return v; };
425
-
426
- // header
427
- $("#repo-name").textContent = D.repo;
428
- $("#scan-date").textContent = new Date(D.scanned + "T00:00:00").toLocaleDateString();
429
- $("#framework").textContent = D.framework;
430
- $("#analysis-kind").textContent = D.analysisKind === "static+dynamic" ? "Static + dynamic proof" : "Static analysis";
431
-
432
- // KPIs four honest tiers (Dynamically Proven / Statically Linked / Reachable Untested / No Signal) + the total.
433
- const kpis = [
434
- ["Behaviors mapped", D.summary.total, "Testable units identified (static)", "total", ""],
435
- ["Dynamically Proven", D.summary.proven, "Real test kills a targeted mutant; the only Proven", "proven", "Mutation killed — a test broke when OrangePro changed this behavior. Highest confidence."],
436
- ["Statically Linked", D.summary.associated, "Static test link, not verified", "assoc", "A test file appears to import or call this behavior. Not dynamically verified."],
437
- ["Reachable Untested", D.summary.reachableUntested, "In a static flow, no test signal", "none", "Appears in a static flow from an entry point, but no test signal was found."],
438
- ["No Signal", D.summary.noSignal, "No test or static-flow signal yet", "none", "No test or static-flow signal found yet."]
439
- ];
440
- $("#kpis").append(...kpis.map(([l, n, s, tone, tip]) => tap(el("div", "kpi", \`<span class="spark"></span><div class="label">\${l}</div><div class="num">\${Number(n || 0).toLocaleString()}</div><div class="sub">\${s}</div>\`), e => { e.dataset.tone = tone; if (tip) e.title = tip; })));
441
-
442
- // pipeline strip
443
- const ps = $("#pipeline");
444
- ps.append(el("span", "plabel", "Evidence pipeline"));
445
- D.pipeline.forEach((s, i) => {
446
- if (i) ps.append(tap(el("span", "arrow", "→"), e => e.style.color = "var(--faint)"));
447
- ps.append(tap(el("span", "pstage", \`<span class="dot"></span>\${s.label} <span class="pr">\${s.pr}</span>\`), e => e.dataset.on = s.on));
448
- });
318
+ (function(){
319
+ const D=window.DATA,$=(s)=>document.querySelector(s);
320
+ const el=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
321
+ const esc=s=>String(s).replace(/[&<>]/g,m=>({"&":"&amp;","<":"&lt;",">":"&gt;"}[m]));
322
+ const S=D.summary;
323
+
324
+ // header
325
+ $("#repo-name").textContent=D.repo;
326
+ $("#scan-date").textContent=new Date(D.scanned+"T00:00:00").toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"});
327
+ $("#framework").textContent=D.framework;
328
+ $("#fw-pill").textContent=D.framework;
329
+
330
+ // bridge text in codebase tab
331
+ $("#br-methods").textContent=S.total;
332
+ $("#br-services").textContent=D.scan.serviceTotal;
333
+ $("#br-tests").textContent=D.scan.tests.total;
334
+
335
+ // tab counts
336
+ $("#t-beh").textContent=D.behaviors.length;
337
+ $("#t-flow").textContent=D.flows.length;
338
+ $("#t-risk").textContent=D.risks.length;
339
+
340
+ // KPIs
341
+ [
342
+ {lbl:"Methods found",num:S.total,sub:"public, with observable outcome",t:"total"},
343
+ {lbl:"Dynamically Proven",num:S.proven,sub:"test breaks if you change it",t:"proven"},
344
+ {lbl:"Test signal",num:S.associated,sub:"test touches it, no proof",t:"signal"},
345
+ {lbl:"Reachable",num:S.reachableUntested,sub:"called but untested",t:"reach"},
346
+ {lbl:"No signal",num:S.noSignal,sub:"nothing touches it",t:"nosig"},
347
+ ].forEach(k=>{
348
+ const d=el("div","kpi",\`<div class="kpi-lbl">\${k.lbl}</div><div class="kpi-num">\${k.num}</div><div class="kpi-sub">\${k.sub}</div>\`);
349
+ d.dataset.t=k.t;
350
+ $("#kpis").append(d);
351
+ });
352
+
353
+ // codebase
354
+ D.scan.services.forEach(([nm,ct])=>$("#svc-list").append(el("div","svc",\`<span class="nm">\${esc(nm)}</span><span class="ct">\${ct}</span>\`)));
355
+ $("#test-total").textContent=D.scan.tests.total;
356
+ $("#test-int").textContent=D.scan.tests.integration;
357
+ $("#test-unit").textContent=D.scan.tests.unit;
358
+
359
+ // behaviors
360
+ function tierOf(b){
361
+ if(b.tier==="proven")return{cls:"proven",badge:"b-proven",label:"Dynamically Proven",ax:"ax-proven"};
362
+ if(b.tier==="assoc")return{cls:"assoc",badge:"b-signal",label:"Test signal",ax:"ax-signal"};
363
+ if(b.tier==="none"&&b.reachable)return{cls:"reach",badge:"b-reach",label:"Reachable",ax:"ax-reach"};
364
+ return{cls:"nosig",badge:"b-nosig",label:"No signal",ax:"ax-nosig"};
365
+ }
366
+ function ctaOf(b){
367
+ if(b.tier==="proven")return"This method is Dynamically Proven. A test breaks if you mutate it. Keep it green.";
368
+ if(b.tier==="assoc")return"A test calls this method, but doesn't prove breakage. Run <code>npx -y @orangepro/orangepro-mcp opro start</code> to attempt dynamic proof.";
369
+ if(b.tier==="none"&&b.reachable)return"This method is reachable from a tested path but has no direct test. Write one.";
370
+ return"Nothing in your test suite touches this method. Write a test that calls it and asserts the output.";
371
+ }
449
372
 
450
- const pg = D.proofGuidance || {
451
- state: D.summary.proven > 0 ? "proven" : "not_started",
452
- title: D.summary.proven > 0 ? "Dynamically Proven is active" : "0 Dynamically Proven means no dynamic proof has run yet",
453
- body: D.summary.proven > 0 ? "At least one behavior has a dynamic targeted-proof certificate." : "This report is an analysis pass. Statically Linked signals are useful signal, but Dynamically Proven starts only after a targeted mutant is killed by a real test.",
454
- action: "Run OrangePro from a coding agent with a model key and follow the returned proof handoff."
373
+ let activeGrp=null,activeTier=null,searchQ="";
374
+ const behSide=$("#beh-groups"),behGrid=$("#beh-grid");
375
+ function renderBeh(){
376
+ behGrid.innerHTML="";
377
+ const rows=D.behaviors.filter(b=>(!activeGrp||b.group===activeGrp)&&(!activeTier||tierOf(b).cls===activeTier)&&(!searchQ||(b.sig+" "+b.file).toLowerCase().includes(searchQ)));
378
+ rows.forEach(b=>{
379
+ const t=tierOf(b);
380
+ const c=el("div","beh-card",
381
+ \`<div class="bc-top"><span class="bc-sig">\${esc(b.sig)}</span><span class="badge \${t.badge} bc-badge"><span class="d"></span>\${t.label}</span></div><div class="bc-file">\${esc(b.file)}</div><div class="bc-bar"></div>\`);
382
+ c.dataset.t=t.cls;
383
+ c.tabIndex=0;
384
+ c.onclick=()=>showDrill(D.behaviors.indexOf(b));
385
+ behGrid.append(c);
386
+ });
387
+ }
388
+ // text search across behavior signature + file
389
+ const searchEl=$("#beh-search");
390
+ searchEl.addEventListener("input",()=>{searchQ=searchEl.value.trim().toLowerCase();renderBeh();});
391
+ // evidence-tier filter (Dynamically Proven first) so users can jump straight to a tier
392
+ const tierWrap=$("#tier-filter");
393
+ [["proven","Dynamically Proven"],["assoc","Test signal"],["reach","Reachable"],["nosig","No signal"]].forEach(([cls,label])=>{
394
+ const n=D.behaviors.filter(b=>tierOf(b).cls===cls).length;
395
+ const btn=el("button","grp",\`<span>\${label}</span><span class="gc">\${n}</span>\`);
396
+ btn.setAttribute("aria-pressed","false");
397
+ btn.onclick=()=>{
398
+ activeTier=activeTier===cls?null:cls;
399
+ [...tierWrap.querySelectorAll(".grp")].forEach(x=>x.setAttribute("aria-pressed",String(x===btn&&!!activeTier)));
400
+ renderBeh();
455
401
  };
456
- const proof = $("#proof-guidance");
457
- proof.dataset.state = pg.state;
458
- proof.innerHTML = \`<span class="mark" aria-hidden="true"></span><div><h2>\${esc(pg.title)}</h2><p>\${esc(pg.body)}</p><p class="action">\${esc(pg.action).replace(/orangepro_prove_loop|opro prove-loop/g, m => \`<code>\${m}</code>\`)}</p></div>\`;
459
-
460
- // Zero-dynamic-proof explainer — data-gated: D.zeroProofExplainer is present ONLY when proven === 0.
461
- const zp = D.zeroProofExplainer;
462
- if (zp) {
463
- const zpEl = $("#zero-proof");
464
- zpEl.hidden = false;
465
- zpEl.innerHTML = \`<h2>\${esc(zp.title)}</h2>\${(zp.body || []).map(p => \`<p>\${esc(p)}</p>\`).join("")}\`;
466
- }
467
-
468
- // tab counts
469
- $("#t-beh").textContent = D.summary.total.toLocaleString();
470
- $("#t-flow").textContent = D.flows.length;
471
- $("#t-risk").textContent = D.risks.length;
472
-
473
- // ---- codebase ----
474
- $("#svc-count").textContent = "(" + D.scan.serviceTotal + ")";
475
- $("#svc-list").append(...D.scan.services.map(([n, c]) => el("div", "svc", \`<span class="nm">\${esc(n)}</span><span class="ct">\${c} behaviors</span>\`)));
476
- $("#test-total").textContent = D.scan.tests.total.toLocaleString();
477
- $("#test-int").textContent = D.scan.tests.integration;
478
- $("#test-unit").textContent = D.scan.tests.unit;
479
- $("#excluded").innerHTML = \`<b>\${D.scan.excluded.count}</b> \${esc(D.scan.excluded.text)}\`;
480
-
481
- // ---- behaviors ----
482
- // Four display tiers over three classifications: the untested \`none\` tier splits into
483
- // "Reachable Untested" (symbol appears in a static flow) vs "No Signal" via b.reachable.
484
- const tierBadge = { proven: ["b-proven","Dynamically Proven"], assoc: ["b-assoc","Statically Linked"], none: ["b-none","No Signal"] };
485
- const tierTip = {
486
- proven: "Mutation killed — a test broke when OrangePro changed this behavior. Highest confidence.",
487
- assoc: "A test file appears to import or call this behavior. Not dynamically verified.",
488
- none: "No test or static-flow signal found yet.",
489
- reachable: "Appears in a static flow from an entry point, but no test signal was found."
402
+ tierWrap.append(btn);
403
+ });
404
+ D.behaviorGroups.forEach(g=>{
405
+ const btn=el("button","grp",\`<span>\${esc(g.key)}</span><span class="gc">\${g.count}</span>\`);
406
+ btn.setAttribute("aria-pressed","false");
407
+ btn.onclick=()=>{
408
+ activeGrp=activeGrp===g.key?null:g.key;
409
+ [...behSide.querySelectorAll(".grp")].forEach(x=>x.setAttribute("aria-pressed",String(x===btn&&!!activeGrp)));
410
+ renderBeh();
490
411
  };
491
- function tierFor(b) {
492
- const reach = b.tier === "none" && b.reachable;
493
- const [bc, blBase] = tierBadge[b.tier] || tierBadge.none;
494
- return { bc, bl: reach ? "Reachable Untested" : blBase, tip: reach ? tierTip.reachable : tierTip[b.tier] };
495
- }
496
- const behMain = $("#beh-list"), behSide = $("#beh-groups");
497
- let activeGroup = null;
498
- function renderBeh() {
499
- behMain.innerHTML = "";
500
- const rows = D.behaviors.filter(b => !activeGroup || b.group === activeGroup);
501
- if (!rows.length) { behMain.append(el("div","empty",\`<div class="ic">◇</div><b>No sampled behaviors in this package</b><div>The full list is graph-driven from the live analysis.</div>\`)); return; }
502
- rows.forEach(b => {
503
- const { bc, bl, tip } = tierFor(b);
504
- const row = el("div","beh-row",
505
- \`<div class="beh-head"><span class="sig">\${esc(b.sig)}</span><span class="badge \${bc}" title="\${esc(tip)}"><span class="d"></span>\${bl}</span></div>
506
- <div class="filemeta">\${esc(b.file)}</div>
507
- <div class="desc">\${esc(b.desc)}</div>\`);
508
- row.setAttribute("role", "button");
509
- row.tabIndex = 0;
510
- row.setAttribute("aria-haspopup", "dialog");
511
- const idx = D.behaviors.indexOf(b);
512
- row.addEventListener("click", () => showBehaviorDrill(idx, row));
513
- row.addEventListener("keydown", e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); showBehaviorDrill(idx, row); } });
514
- behMain.append(row);
515
- });
412
+ behSide.append(btn);
413
+ });
414
+ renderBeh();
415
+
416
+ // drill
417
+ const drill=$("#drill"),dc=$("#drill-content");
418
+ function showDrill(i){
419
+ const b=D.behaviors[i];if(!b)return;
420
+ const t=tierOf(b);
421
+ dc.innerHTML=\`<h2 class="drill-title">\${esc(b.sig)}</h2>
422
+ <div class="drill-row"><h4>Evidence</h4><span class="badge \${t.badge}"><span class="d"></span>\${t.label}</span></div>
423
+ <div class="drill-row"><h4>File</h4><p class="mono">\${esc(b.file)}</p></div>
424
+ <div class="drill-row"><h4>What to do</h4><div class="abox \${t.ax}">\${ctaOf(b)}</div></div>\`;
425
+ drill.classList.add("open");document.body.style.overflow="hidden";
426
+ }
427
+ $("#drill-close").onclick=()=>{drill.classList.remove("open");document.body.style.overflow=""};
428
+ drill.onclick=e=>{if(e.target===drill){drill.classList.remove("open");document.body.style.overflow=""}};
429
+ document.onkeydown=e=>{if(e.key==="Escape"){drill.classList.remove("open");document.body.style.overflow=""}};
430
+
431
+ // flows
432
+ const fl=$("#flow-list");
433
+ D.flows.forEach(f=>{
434
+ const rBadge=f.risk==="critical"?\`<span class="badge b-risk"><span class="d"></span>critical</span>\`:\`<span class="badge b-signal"><span class="d"></span>high</span>\`;
435
+ const pBadge=f.proof==="proven"?\`<span class="badge b-proven"><span class="d"></span>dynamically proven</span>\`:f.proof==="assoc"?\`<span class="badge b-signal"><span class="d"></span>signal</span>\`:\`<span class="badge b-nosig"><span class="d"></span>no proof</span>\`;
436
+
437
+ const totalNodes=(f.trigger?1:0)+f.steps.length;
438
+ const vert=totalNodes>4;
439
+ let chain=vert?\`<div class="flow-chain fc-vert">\`:\`<div class="flow-chain">\`;
440
+
441
+ if(f.trigger){
442
+ chain+=\`<div class="fn"><div class="fn-box fnb-entry"><span style="color:var(--green);font-weight:700">\${esc(f.trigger.verb)}</span> \${esc(f.trigger.path)}</div></div>\`;
516
443
  }
517
- D.behaviorGroups.forEach(g => {
518
- behSide.append(tap(el("button","grp",\`<span>\${esc(g.key)}</span><span class="gc">\${g.count}</span>\`), b => {
519
- b.setAttribute("aria-pressed", "false");
520
- b.onclick = () => {
521
- activeGroup = activeGroup === g.key ? null : g.key;
522
- [...behSide.children].forEach(c => c.setAttribute("aria-pressed", String(c === b && activeGroup)));
523
- renderBeh();
524
- };
525
- }));
444
+ f.steps.forEach(s=>{
445
+ const eCls=s.edge==="framework-derived"?"fel-fw":"fel-hard";
446
+ chain+=\`<div class="fe"><div class="fe-line \${eCls}"></div></div>\`;
447
+ const nCls=s.proof==="proven"?"fnb-proven":s.proof==="assoc"?"fnb-assoc":"";
448
+ chain+=\`<div class="fn"><div class="fn-box \${nCls}">\${esc(s.sig)}</div></div>\`;
526
449
  });
527
- renderBeh();
528
-
529
- // ---- behavior detail modal (display-only; every field comes straight from window.DATA) ----
530
- const drill = $("#drill"), drillContent = $("#drill-content");
531
- let drillReturnFocus = null;
532
- function showBehaviorDrill(i, trigger) {
533
- const b = D.behaviors[i];
534
- if (!b) return;
535
- const { bc, bl } = tierFor(b);
536
- const why = b.sig.includes(".")
537
- ? \`<b>\${esc(b.sig.split(".")[0])}</b> exposes a public method a user or system can trigger, with an observable outcome and side effects: the smallest unit you'd write an acceptance criterion for.\`
538
- : "A discrete operation with an observable outcome and side effects: the smallest unit you'd write an acceptance criterion for.";
539
- const action = b.tier === "proven"
540
- ? "Dynamically proven: a real test kills a targeted mutant with the target running unmocked. Keep the test green as the code evolves."
541
- : b.tier === "assoc"
542
- ? "Statically Linked: a test file imports or calls this behavior, but it is not dynamically verified. Add a targeted test that runs it unmocked and asserts its contract to move it to Dynamically Proven."
543
- : (b.tier === "none" && b.reachable)
544
- ? "Reachable Untested: this behavior appears in a static flow from an entry point, but no test signal was found. Write an integration or behavior test that calls it and asserts the observable outcome."
545
- : "No Signal: no test or static-flow signal found yet. Write an integration or behavior test that calls this behavior and asserts the observable outcome.";
546
- drillContent.innerHTML =
547
- \`<h2 class="drill-title" id="drill-title">\${esc(b.sig)}</h2>
548
- <div class="drill-section"><h4>Evidence tier</h4><span class="badge \${bc}"><span class="d"></span>\${bl}</span></div>
549
- \${b.desc ? \`<div class="drill-section"><h4>What this behavior does</h4><p>\${esc(b.desc)}</p></div>\` : ""}
550
- \${b.file ? \`<div class="drill-section"><h4>Source file</h4><p class="mono">\${esc(b.file)}</p></div>\` : ""}
551
- <div class="drill-section"><h4>Why this is a behavior</h4><div class="why-box">\${why}</div></div>
552
- <div class="drill-section"><h4>What to do</h4><div class="action-box">\${action}</div></div>\`;
553
- drillReturnFocus = trigger || document.activeElement;
554
- drill.classList.add("open");
555
- document.body.style.overflow = "hidden";
556
- $("#drill-close").focus();
557
- }
558
- function closeDrill() {
559
- if (!drill.classList.contains("open")) return;
560
- drill.classList.remove("open");
561
- document.body.style.overflow = "";
562
- if (drillReturnFocus && drillReturnFocus.focus) drillReturnFocus.focus();
563
- drillReturnFocus = null;
564
- }
565
- $("#drill-close").addEventListener("click", closeDrill);
566
- drill.addEventListener("click", e => { if (e.target === drill) closeDrill(); }); // backdrop click
567
- document.addEventListener("keydown", e => { if (e.key === "Escape") closeDrill(); }); // Escape
568
-
569
- // ---- flows ----
570
- const fl = $("#flow-list");
571
- D.flows.forEach(f => {
572
- const riskBadge = f.risk === "critical" ? \`<span class="badge b-risk"><span class="d"></span>critical risk</span>\`
573
- : f.risk === "high" ? \`<span class="badge b-assoc"><span class="d"></span>high risk</span>\`
574
- : f.risk === "medium" ? \`<span class="badge b-info">medium risk</span>\` : "";
575
- const proofBadge = f.proof === "proven" ? \`<span class="badge b-proven"><span class="d"></span>dynamically proven</span>\`
576
- : \`<span class="badge b-info">no dynamic proof</span>\`;
577
- const flowTier = f.steps.some(s => s.tier === "framework-derived")
578
- ? \`<span class="badge b-fw"><span class="d"></span>framework-derived reachable</span>\`
579
- : \`<span class="badge b-hard"><span class="d"></span>hard reachable</span>\`;
580
- const steps = f.steps.map((s, i) => {
581
- const hop = s.edge === "hard" ? \`<span class="hop-tag hard">hard</span>\`
582
- : s.edge === "framework-derived" ? \`<span class="hop-tag fw">framework-derived</span>\` : "";
583
- return \`<li class="step" data-tier="\${s.tier}" data-edge="\${s.edge||''}">
584
- <span class="node"></span>
585
- <span class="sig"><span class="idx">\${i+1}</span>\${esc(s.sig)} \${hop}</span>
586
- <div class="desc">\${esc(s.desc)}</div></li>\`;
587
- }).join("");
588
- const trig = f.trigger
589
- ? \`<div class="trigger"><span class="tk">Trigger</span><span><span class="verb">\${esc(f.trigger.verb)}</span> <span class="path">\${esc(f.trigger.path)}</span></span></div>\`
590
- : \`<div class="trigger"><span class="tk">Entry</span><span class="path">\${esc(f.steps[0] ? f.steps[0].sig : f.title)}</span></div>\`;
591
- fl.append(el("div","flow",
592
- \`<div class="flow-head"><h3>\${esc(f.title)}</h3>
593
- <div class="flow-tags">\${riskBadge}\${flowTier}\${proofBadge}<span class="badge b-info">\${f.services} services</span></div></div>
594
- \${trig}
595
- <ul class="steps">\${steps}</ul>
596
- <div class="flow-foot"><span class="why">\${f.why}</span></div>\`));
450
+ chain+=\`</div>\`;
451
+
452
+ fl.append(el("div","flow-card",
453
+ \`<div class="flow-head"><h3>\${esc(f.title)}</h3><div class="flow-tags">\${rBadge}\${pBadge}</div></div>\${chain}\`));
454
+ });
455
+
456
+ // AI flows
457
+ const cf=D.candidateFlows;
458
+ if(cf&&cf.flows.length){
459
+ $("#ai-sec").hidden=false;
460
+ cf.flows.forEach(f=>{
461
+ const vert=f.steps.length>4;
462
+ let chain=vert?\`<div class="flow-chain fc-vert">\`:\`<div class="flow-chain">\`;
463
+ f.steps.forEach((s,i)=>{
464
+ if(i)chain+=\`<div class="fe"><div class="fe-line fel-hard"></div></div>\`;
465
+ chain+=\`<div class="fn"><div class="fn-box">\${esc(s.sig)}</div></div>\`;
466
+ });
467
+ chain+=\`</div>\`;
468
+ $("#ai-list").append(el("div","flow-card ai-card",
469
+ \`<div class="flow-head"><h3>\${esc(f.title)}</h3><div class="flow-tags"><span class="badge b-info">\${esc(cf.model)}</span></div></div>\${chain}\`));
597
470
  });
471
+ }
598
472
 
599
- // ---- AI-suggested candidate flows (worklist only — excluded from every count above) ----
600
- const cf = D.candidateFlows;
601
- if (cf) {
602
- $("#ai-flows").hidden = false;
603
- // Artifact-derived values are untrusted: Number-coerce every numeric and
604
- // esc() every string before it reaches innerHTML.
605
- const num = v => (Number.isFinite(Number(v)) ? Number(v) : 0);
606
- $("#ai-flow-headline").innerHTML = \`Model proposed <b>\${num(cf.proposed)}</b> → accepted <b>\${num(cf.accepted)}</b> after closed-set validation.\`;
607
- const cl = $("#ai-flow-list");
608
- cf.flows.forEach(f => {
609
- const steps = f.steps.map((s, i) => {
610
- const hop = s.hop === "matches_known_edge" ? \`<span class="hop-tag match">matches known edge</span>\`
611
- : s.hop === "unverified" ? \`<span class="hop-tag unv">unverified</span>\` : "";
612
- return \`<li class="step cstep"><span class="node"></span>
613
- <span class="sig"><span class="idx">\${i+1}</span>\${esc(s.sig)} \${hop}</span>
614
- <div class="desc">\${esc(s.desc)}</div></li>\`;
615
- }).join("");
616
- cl.append(el("div","cflow",
617
- \`<div class="flow-head"><h3>\${esc(f.title)}</h3>
618
- <div class="flow-tags"><span class="badge b-ai">AI-suggested · \${esc(cf.model)} · \${esc(cf.prompt_version)}</span><span class="badge b-ai">confidence \${Math.round(num(f.confidence)*100)}%</span></div></div>
619
- <ul class="steps">\${steps}</ul>
620
- <div class="flow-foot"><span class="why">\${esc(f.rationale || "Candidate chain proposed by AI.")} <b>Not evidence</b> until a real test proves it.</span></div>\`));
473
+ // risks + generated test samples
474
+ let activeRiskFilter="all";
475
+ const riskList=$("#risk-list"),riskTools=$("#risk-tools");
476
+ function riskMatchesFilter(r){
477
+ const hasGenerated=Boolean(r.generatedTests&&r.generatedTests.length);
478
+ if(activeRiskFilter==="generated")return hasGenerated;
479
+ if(activeRiskFilter==="missing")return !hasGenerated;
480
+ return true;
481
+ }
482
+ function renderRiskFilters(){
483
+ const options=[
484
+ ["all","All",D.risks.length],
485
+ ["generated","Generated tests",D.risks.filter(r=>r.generatedTests&&r.generatedTests.length).length],
486
+ ["missing","No generated tests",D.risks.filter(r=>!(r.generatedTests&&r.generatedTests.length)).length]
487
+ ];
488
+ riskTools.innerHTML=options.map(([key,label,count])=>\`<button class="risk-filter" type="button" data-risk-filter="\${key}" aria-pressed="\${key===activeRiskFilter}">\${label} <span class="gc">\${count}</span></button>\`).join("");
489
+ }
490
+ function riskCardHtml(r){
491
+ const tags=r.tags.map(([t,k])=>\`<span class="badge b-\${k}"><span class="d"></span>\${esc(t)}</span>\`).join("");
492
+ // category strip: show which concern categories apply to this flow
493
+ let catHtml='';
494
+ if(r.applicableCategories&&r.applicableCategories.length){
495
+ const shownConcerns=new Set((r.generatedTests||[]).map(t=>t.concern).filter(Boolean));
496
+ const pills=r.applicableCategories.map(c=>{
497
+ const shown=shownConcerns.has(c);
498
+ const label=c.replace(/_/g,' ');
499
+ return \`<span class="cat-pill \${shown?'cp-shown':'cp-locked'}">\${esc(label)}</span>\`;
500
+ }).join('');
501
+ const shownN=r.applicableCategories.filter(c=>shownConcerns.has(c)).length;
502
+ const totalN=r.applicableCategories.length;
503
+ catHtml=\`<div class="cat-strip"><div class="cat-strip-lbl">Testing categories for this flow (\${shownN} of \${totalN} shown)</div><div class="cat-pills">\${pills}</div></div>\`;
504
+ }
505
+ let testsHtml='';
506
+ if(r.generatedTests&&r.generatedTests.length){
507
+ testsHtml=\`<div class="gen-tests"><div class="gen-tests-lbl">Generated tests (integration)</div>\`;
508
+ r.generatedTests.forEach(t=>{
509
+ const cBadge=t.concern?\`<span class="badge b-info" style="margin-left:6px;font-size:9px">\${esc(t.concern.replace('_',' '))}</span>\`:'';
510
+ testsHtml+=\`<div class="gen-test"><div class="gen-test-head"><span class="gen-test-name">\${esc(t.name)}\${cBadge}</span><span class="gen-test-assert">\${esc(t.assertion)}</span><span class="gen-test-toggle">&#9660;</span></div><div class="gen-test-body">\${esc(t.code)}</div></div>\`;
621
511
  });
512
+ testsHtml+=\`</div>\`;
622
513
  }
623
-
624
- // ---- risks ----
625
- const rl = $("#risk-list");
626
- D.risks.forEach(r => {
627
- const tags = r.tags.map(([t, k]) => \`<span class="badge b-\${k}">\${k==="entry"?"":'<span class="d"></span>'}\${esc(t)}</span>\`).join("");
628
- rl.append(el("div","risk",
629
- \`<div class="rk">#\${r.rank}</div>
630
- <div class="ep"><span class="verb">\${r.verb}</span> <span class="path">\${esc(r.path)}</span></div>
631
- <div class="rdesc">\${esc(r.desc)}</div>
632
- <div class="rtags">\${tags}</div>
633
- <div class="todo"><div class="th">What to do</div><p>\${esc(r.todo)}</p></div>\`));
634
- });
635
-
636
- // ---- tabs ----
637
- const tabs = [...document.querySelectorAll(".tab")];
638
- tabs.forEach(t => t.addEventListener("click", () => {
639
- tabs.forEach(x => x.setAttribute("aria-selected", String(x === t)));
640
- document.querySelectorAll(".panel").forEach(p => p.classList.toggle("active", p.id === "panel-" + t.dataset.tab));
641
- }));
642
- // keyboard arrows on tablist
643
- document.querySelector(".tabs").addEventListener("keydown", e => {
644
- if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
645
- const i = tabs.indexOf(document.activeElement); if (i < 0) return;
646
- const n = (i + (e.key === "ArrowRight" ? 1 : tabs.length - 1)) % tabs.length;
647
- tabs[n].focus(); tabs[n].click();
648
- });
514
+ return \`<div class="risk-rank">#\${r.rank}</div>
515
+ <div class="risk-ep"><span class="v">\${esc(r.verb)}</span> \${esc(r.path)}</div>
516
+ <div class="risk-desc">\${esc(r.desc)}</div>
517
+ <div class="risk-tags">\${tags}</div>
518
+ <div class="todo">\${esc(r.todo)}</div>\${testsHtml}\${catHtml}\`;
519
+ }
520
+ function renderRisks(){
521
+ riskList.innerHTML="";
522
+ D.risks.filter(riskMatchesFilter).forEach(r=>riskList.append(el("div","risk-card",riskCardHtml(r))));
523
+ if(activeRiskFilter==="all"&&D.generatedTotal){
524
+ const hiddenGenerated=Math.max(0,D.generatedTotal-D.shownCount);
525
+ const remainingRiskFlows=Math.max(0,D.risks.length-D.generatedTotal);
526
+ riskList.append(el("div","paywall",
527
+ hiddenGenerated
528
+ ? \`<div class="paywall-num">\${hiddenGenerated} more tests generated</div>
529
+ <div class="paywall-txt">OrangePro generated tests for \${D.generatedTotal} behaviors across your highest-risk flows. You're seeing \${D.shownCount}.</div>
530
+ <a class="paywall-btn" href="https://app.orangepro.ai" target="_blank">View all on OrangePro Platform &rarr;</a>\`
531
+ : remainingRiskFlows
532
+ ? \`<div class="paywall-num">\${remainingRiskFlows} high-risk flows left</div>
533
+ <div class="paywall-txt">The local MCP accepted \${D.generatedTotal} runnable generated test\${D.generatedTotal===1?"":"s"} for this report. Generate the remaining high-risk flow tests on OrangePro Platform.</div>
534
+ <a class="paywall-btn" href="https://app.orangepro.ai" target="_blank">Generate remaining tests on Platform &rarr;</a>\`
535
+ : \`<div class="paywall-num">All generated tests are shown</div>
536
+ <div class="paywall-txt">OrangePro generated tests for every high-risk flow in this report, and every generated test is visible here.</div>\`));
537
+ }
538
+ }
539
+ riskTools.addEventListener("click",e=>{
540
+ const btn=e.target.closest("[data-risk-filter]");
541
+ if(!btn)return;
542
+ activeRiskFilter=btn.dataset.riskFilter;
543
+ renderRiskFilters();
544
+ renderRisks();
545
+ });
546
+ renderRiskFilters();
547
+ renderRisks();
548
+ // toggle test expand
549
+ document.addEventListener('click',e=>{
550
+ const h=e.target.closest('.gen-test-head');
551
+ if(h)h.parentElement.classList.toggle('open');
552
+ });
553
+
554
+ // tabs
555
+ const tabs=[...document.querySelectorAll(".tab")];
556
+ tabs.forEach(t=>t.onclick=()=>{
557
+ tabs.forEach(x=>x.setAttribute("aria-selected",String(x===t)));
558
+ document.querySelectorAll(".panel").forEach(p=>p.classList.toggle("active",p.id==="panel-"+t.dataset.tab));
559
+ });
649
560
  })();
650
561
  </script>
651
562
  </body>