freshcontext-mcp 0.3.16 → 0.3.17

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/demo/index.html DELETED
@@ -1,513 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Most RAG pipelines fail because of time, not embeddings — FreshContext</title>
7
- <meta name="description" content="A 5-document demonstration of why semantic-only retrieval gives 2026 systems 2022 answers — and what it costs to fix it.">
8
- <style>
9
- :root {
10
- --bg: #fafaf7;
11
- --surface: #ffffff;
12
- --ink: #1a1a1a;
13
- --ink-soft: #555;
14
- --ink-faint: #888;
15
- --rule: #d4d0c4;
16
- --code-bg: #f0ebe1;
17
- --stale: #b34a3c;
18
- --stale-soft: #f4e4e0;
19
- --fresh: #2d6a4f;
20
- --fresh-soft: #e0ede5;
21
- --accent: #2a4d8f;
22
- }
23
- * { box-sizing: border-box; }
24
- html, body { margin: 0; padding: 0; }
25
- body {
26
- background: var(--bg);
27
- color: var(--ink);
28
- font-family: Charter, "Iowan Old Style", "Source Serif Pro", Georgia, serif;
29
- font-size: 18px;
30
- line-height: 1.6;
31
- -webkit-font-smoothing: antialiased;
32
- }
33
- .wrap {
34
- max-width: 780px;
35
- margin: 0 auto;
36
- padding: 64px 28px 96px;
37
- }
38
- h1 {
39
- font-size: 38px;
40
- line-height: 1.15;
41
- margin: 0 0 12px;
42
- letter-spacing: -0.02em;
43
- }
44
- h2 {
45
- font-size: 24px;
46
- margin: 56px 0 16px;
47
- letter-spacing: -0.01em;
48
- border-top: 1px solid var(--rule);
49
- padding-top: 32px;
50
- }
51
- h3 {
52
- font-size: 16px;
53
- text-transform: uppercase;
54
- letter-spacing: 0.08em;
55
- color: var(--ink-soft);
56
- margin: 32px 0 8px;
57
- }
58
- p { margin: 0 0 16px; }
59
- .lede {
60
- font-size: 20px;
61
- color: var(--ink-soft);
62
- margin: 0 0 8px;
63
- }
64
- .byline {
65
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
66
- font-size: 13px;
67
- color: var(--ink-faint);
68
- text-transform: uppercase;
69
- letter-spacing: 0.1em;
70
- margin-bottom: 32px;
71
- }
72
- code, .mono {
73
- font-family: "JetBrains Mono", Menlo, Consolas, monospace;
74
- font-size: 0.92em;
75
- }
76
- .inline-code {
77
- background: var(--code-bg);
78
- padding: 1px 6px;
79
- border-radius: 3px;
80
- }
81
- blockquote {
82
- margin: 24px 0;
83
- padding: 16px 24px;
84
- border-left: 3px solid var(--accent);
85
- background: var(--surface);
86
- font-style: italic;
87
- color: var(--ink-soft);
88
- }
89
- .query-box {
90
- background: var(--surface);
91
- border: 1px solid var(--rule);
92
- padding: 20px 24px;
93
- margin: 16px 0 32px;
94
- border-radius: 4px;
95
- }
96
- .query-box .label {
97
- font-family: -apple-system, BlinkMacSystemFont, sans-serif;
98
- font-size: 12px;
99
- text-transform: uppercase;
100
- letter-spacing: 0.1em;
101
- color: var(--ink-faint);
102
- margin-bottom: 6px;
103
- }
104
- .query-box .q {
105
- font-size: 20px;
106
- line-height: 1.4;
107
- }
108
- .doc {
109
- background: var(--surface);
110
- border: 1px solid var(--rule);
111
- border-radius: 4px;
112
- padding: 18px 22px;
113
- margin-bottom: 14px;
114
- }
115
- .doc-header {
116
- display: flex;
117
- justify-content: space-between;
118
- align-items: baseline;
119
- gap: 16px;
120
- margin-bottom: 8px;
121
- font-family: -apple-system, BlinkMacSystemFont, sans-serif;
122
- }
123
- .doc-source {
124
- font-size: 13px;
125
- color: var(--ink-faint);
126
- text-transform: lowercase;
127
- }
128
- .doc-date {
129
- font-size: 13px;
130
- color: var(--ink-faint);
131
- white-space: nowrap;
132
- }
133
- .doc-title {
134
- font-weight: 600;
135
- margin: 0 0 8px;
136
- font-size: 18px;
137
- }
138
- .doc-content {
139
- font-size: 16px;
140
- color: var(--ink-soft);
141
- margin: 0;
142
- }
143
- .scores {
144
- margin-top: 12px;
145
- padding-top: 10px;
146
- border-top: 1px dashed var(--rule);
147
- display: flex;
148
- gap: 24px;
149
- font-family: "JetBrains Mono", Menlo, monospace;
150
- font-size: 13px;
151
- }
152
- .score-pill {
153
- display: inline-flex;
154
- gap: 6px;
155
- align-items: baseline;
156
- }
157
- .score-pill .label {
158
- color: var(--ink-faint);
159
- }
160
- .score-pill .val {
161
- font-weight: 600;
162
- }
163
- .age {
164
- color: var(--ink-faint);
165
- font-size: 13px;
166
- }
167
- .math-block {
168
- background: var(--surface);
169
- border: 1px solid var(--rule);
170
- border-radius: 4px;
171
- padding: 24px 28px;
172
- margin: 16px 0;
173
- }
174
- .math-eq {
175
- text-align: center;
176
- font-size: 28px;
177
- margin: 16px 0 24px;
178
- font-family: "Latin Modern Math", "STIX Two Math", Cambria, Georgia, serif;
179
- }
180
- .math-eq i { color: var(--accent); }
181
- .math-where {
182
- font-family: "JetBrains Mono", Menlo, monospace;
183
- font-size: 14px;
184
- color: var(--ink-soft);
185
- margin: 0;
186
- }
187
- .math-where li { margin-bottom: 4px; list-style: none; padding-left: 0; }
188
- .math-where ul { padding-left: 0; margin: 0; }
189
- .compare {
190
- display: grid;
191
- grid-template-columns: 1fr 1fr;
192
- gap: 20px;
193
- margin: 24px 0;
194
- }
195
- @media (max-width: 700px) {
196
- .compare { grid-template-columns: 1fr; }
197
- }
198
- .col {
199
- background: var(--surface);
200
- border-radius: 6px;
201
- padding: 24px;
202
- border: 1px solid var(--rule);
203
- }
204
- .col.stale { border-top: 4px solid var(--stale); }
205
- .col.fresh { border-top: 4px solid var(--fresh); }
206
- .col-header {
207
- font-family: -apple-system, BlinkMacSystemFont, sans-serif;
208
- font-size: 13px;
209
- text-transform: uppercase;
210
- letter-spacing: 0.1em;
211
- color: var(--ink-faint);
212
- margin-bottom: 4px;
213
- }
214
- .col-title {
215
- font-weight: 700;
216
- font-size: 18px;
217
- margin-bottom: 16px;
218
- }
219
- .col.stale .col-title { color: var(--stale); }
220
- .col.fresh .col-title { color: var(--fresh); }
221
- .ranking {
222
- list-style: none;
223
- padding: 0;
224
- margin: 0;
225
- }
226
- .ranking li {
227
- padding: 10px 0;
228
- border-bottom: 1px solid var(--rule);
229
- font-size: 15px;
230
- line-height: 1.4;
231
- }
232
- .ranking li:last-child { border-bottom: none; }
233
- .ranking .rank-num {
234
- display: inline-block;
235
- width: 24px;
236
- color: var(--ink-faint);
237
- font-family: "JetBrains Mono", monospace;
238
- font-size: 13px;
239
- }
240
- .ranking .rank-doc {
241
- font-weight: 500;
242
- }
243
- .ranking .rank-score {
244
- float: right;
245
- font-family: "JetBrains Mono", monospace;
246
- font-size: 13px;
247
- color: var(--ink-faint);
248
- }
249
- .answer {
250
- font-size: 15px;
251
- line-height: 1.55;
252
- }
253
- .answer strong { color: var(--ink); }
254
- .answer ul { margin: 8px 0; padding-left: 22px; }
255
- .answer li { margin-bottom: 6px; }
256
- .verdict {
257
- margin-top: 16px;
258
- padding: 12px 16px;
259
- border-radius: 4px;
260
- font-family: -apple-system, BlinkMacSystemFont, sans-serif;
261
- font-size: 14px;
262
- font-weight: 600;
263
- }
264
- .verdict.bad { background: var(--stale-soft); color: var(--stale); }
265
- .verdict.good { background: var(--fresh-soft); color: var(--fresh); }
266
- .regen-note {
267
- background: var(--surface);
268
- border: 1px dashed var(--rule);
269
- border-radius: 4px;
270
- padding: 12px 16px;
271
- margin: 16px 0;
272
- font-family: -apple-system, BlinkMacSystemFont, sans-serif;
273
- font-size: 13px;
274
- color: var(--ink-soft);
275
- }
276
- .regen-note strong { color: var(--ink); }
277
- .regen-note code {
278
- background: var(--code-bg);
279
- padding: 1px 5px;
280
- border-radius: 3px;
281
- }
282
- .footer {
283
- margin-top: 80px;
284
- padding-top: 32px;
285
- border-top: 1px solid var(--rule);
286
- font-family: -apple-system, BlinkMacSystemFont, sans-serif;
287
- font-size: 14px;
288
- color: var(--ink-soft);
289
- }
290
- .footer a { color: var(--accent); text-decoration: none; }
291
- .footer a:hover { text-decoration: underline; }
292
- .footer code { background: var(--code-bg); padding: 1px 6px; border-radius: 3px; font-size: 13px; }
293
- </style>
294
- </head>
295
- <body>
296
-
297
- <div class="wrap">
298
-
299
- <div class="byline">FreshContext · A live demonstration · v1.0</div>
300
-
301
- <h1>Most RAG pipelines fail because of time, not embeddings.</h1>
302
- <p class="lede">Same model. Same retrieval set. Same query. Two completely different answers — because one of them remembered when its sources were written.</p>
303
-
304
- <h2>The query</h2>
305
- <p>A developer in 2026 asks an AI agent for current best practices. Five documents are retrieved by semantic similarity:</p>
306
-
307
- <div class="query-box">
308
- <div class="label">Query</div>
309
- <div class="q" id="query"></div>
310
- </div>
311
-
312
- <h2>The retrieval set</h2>
313
- <p>Each document scored on pure semantic similarity to the query. Top result is a 2022 blog post — confidently written, dense keyword match, authoritative source.</p>
314
-
315
- <div id="docs"></div>
316
-
317
- <h2>The math</h2>
318
- <p>FreshContext applies a single correction: an exponential decay weight based on document age. Nothing else changes — same embeddings, same vectors, same retrieval set.</p>
319
-
320
- <div class="math-block">
321
- <div class="math-eq">R<sub>t</sub> = R<sub>0</sub> · e<sup>−<i>λ</i>·t</sup></div>
322
- <div class="math-where">
323
- <ul>
324
- <li>R<sub>0</sub> = base semantic relevancy [0–100]</li>
325
- <li>λ &nbsp;= source-specific decay constant (per hour)</li>
326
- <li>t &nbsp;= hours elapsed since the document was published</li>
327
- <li>R<sub>t</sub> = decay-adjusted relevancy at query time</li>
328
- </ul>
329
- </div>
330
- <p style="margin-top: 16px; font-size: 14px; color: var(--ink-soft);">For this demo, λ = 0.0001 (half-life ≈ 9.5 months). The live FreshContext engine uses source-specific λ values: HN front page ≈ 14h half-life, blog posts ≈ 29 days, academic papers ≈ 1.6 years.</p>
331
- </div>
332
-
333
- <h2>The ranking shift</h2>
334
- <p>The 2022 blog falls from rank 1 to rank 5. The 2026 X post rises from rank 4 to rank 1. Same documents, decay applied:</p>
335
-
336
- <div class="compare">
337
- <div class="col stale">
338
- <div class="col-header">Without FreshContext</div>
339
- <div class="col-title">Semantic top-k ranking</div>
340
- <ol class="ranking" id="rank-baseline"></ol>
341
- </div>
342
- <div class="col fresh">
343
- <div class="col-header">With FreshContext</div>
344
- <div class="col-title">Decay-adjusted ranking</div>
345
- <ol class="ranking" id="rank-fresh"></ol>
346
- </div>
347
- </div>
348
-
349
- <h2>The answer that changes</h2>
350
- <p>Both answers come from the same model with the same query. The only difference is which three documents the model saw at the top of the context window:</p>
351
-
352
- <div id="regen-note-anchor"></div>
353
-
354
- <div class="compare">
355
-
356
- <div class="col stale">
357
- <div class="col-header">Without FreshContext</div>
358
- <div class="col-title" id="stale-context-label">Top context: …</div>
359
- <div class="answer" id="stale-answer"></div>
360
- <div class="verdict" id="stale-verdict"></div>
361
- </div>
362
-
363
- <div class="col fresh">
364
- <div class="col-header">With FreshContext</div>
365
- <div class="col-title" id="fresh-context-label">Top context: …</div>
366
- <div class="answer" id="fresh-answer"></div>
367
- <div class="verdict" id="fresh-verdict"></div>
368
- </div>
369
-
370
- </div>
371
-
372
- <h2>What this is, and what it isn't</h2>
373
-
374
- <p><strong>This isn't a model problem.</strong> The same model produced both answers. Claude wasn't wrong in the first version — it faithfully summarized what was put in front of it.</p>
375
-
376
- <p><strong>This isn't an embedding problem.</strong> The cosine similarity scores were correct. The 2022 blog really is semantically dense and well-written.</p>
377
-
378
- <p><strong>This is a context-engineering problem.</strong> Retrieval ranks correctly along one axis (semantic similarity) and ignores the other axis that matters in production (temporal validity). FreshContext adds the missing axis.</p>
379
-
380
- <blockquote>"Most RAG pipelines rank context correctly semantically but incorrectly temporally."</blockquote>
381
-
382
- <h2>Reproduce this</h2>
383
- <p>The retrieval set, the math, and the prompts are all open. Read the data file, change the query, swap λ, run it against your own RAG pipeline:</p>
384
-
385
- <div class="footer">
386
- <p>
387
- <strong>Code:</strong> <a href="https://github.com/PrinceGabriel-lgtm/freshcontext-mcp">github.com/PrinceGabriel-lgtm/freshcontext-mcp</a><br>
388
- <strong>Spec:</strong> <a href="https://freshcontext-site.pages.dev">freshcontext-site.pages.dev</a><br>
389
- <strong>Install:</strong> <code>npm install freshcontext-mcp</code><br>
390
- <strong>Live API:</strong> <code>https://freshcontext-mcp.gimmanuel73.workers.dev/v1/intel/feed/default</code>
391
- </p>
392
- <p style="margin-top: 24px; color: var(--ink-faint);">Built by Immanuel Gabriel (Prince Gabriel) · Grootfontein, Namibia · MIT licensed.</p>
393
- </div>
394
-
395
- </div>
396
-
397
- <script>
398
- // ─── Compute R_t live on page load ──────────────────────────────────────
399
- // The math runs in the browser. The answers are loaded from data.json —
400
- // no API key required to view this page. To regenerate the answers from a
401
- // real Claude call, run `node generate.mjs` with your own ANTHROPIC_API_KEY.
402
- // FreshContext incorporates LLMs as optional output, doesn't require them.
403
-
404
- (async function() {
405
- const res = await fetch('./data.json');
406
- const data = await res.json();
407
-
408
- const now = new Date(data.now).getTime();
409
- const lambda = data.decay.lambda_per_hour;
410
-
411
- // Compute t (age in hours) and R_t for each doc
412
- const docs = data.documents.map(d => {
413
- const published = new Date(d.published_at).getTime();
414
- const age_hours = (now - published) / (1000 * 60 * 60);
415
- const r_t = d.base_score * Math.exp(-lambda * age_hours);
416
- const age_days = age_hours / 24;
417
- const age_years = age_days / 365.25;
418
- let age_human;
419
- if (age_years >= 1) age_human = age_years.toFixed(1) + ' years';
420
- else if (age_days >= 30) age_human = (age_days / 30).toFixed(1) + ' months';
421
- else age_human = age_days.toFixed(0) + ' days';
422
- return { ...d, age_hours, r_t, age_human };
423
- });
424
-
425
- // ─── Render query ─────────────────────────────────────────────────────
426
- document.getElementById('query').textContent = data.query;
427
-
428
- // ─── Render docs ──────────────────────────────────────────────────────
429
- const docsEl = document.getElementById('docs');
430
- docs.forEach((d) => {
431
- const card = document.createElement('div');
432
- card.className = 'doc';
433
- card.innerHTML = `
434
- <div class="doc-header">
435
- <span class="doc-source">${d.source}</span>
436
- <span class="doc-date">${d.published_at.slice(0, 10)} · <span class="age">${d.age_human} ago</span></span>
437
- </div>
438
- <div class="doc-title">${d.title}</div>
439
- <p class="doc-content">${d.content}</p>
440
- <div class="scores">
441
- <span class="score-pill"><span class="label">R₀ (semantic):</span> <span class="val">${d.base_score}</span></span>
442
- <span class="score-pill"><span class="label">Rₜ (decay-adjusted):</span> <span class="val">${d.r_t.toFixed(1)}</span></span>
443
- </div>
444
- `;
445
- docsEl.appendChild(card);
446
- });
447
-
448
- // ─── Build rankings ───────────────────────────────────────────────────
449
- const baseline = [...docs].sort((a, b) => b.base_score - a.base_score);
450
- const fresh = [...docs].sort((a, b) => b.r_t - a.r_t);
451
-
452
- function renderRanking(elId, list, scoreKey) {
453
- const el = document.getElementById(elId);
454
- list.forEach((d, i) => {
455
- const li = document.createElement('li');
456
- const score = scoreKey === 'base_score' ? d.base_score : d.r_t.toFixed(1);
457
- li.innerHTML = `
458
- <span class="rank-num">${i + 1}.</span>
459
- <span class="rank-doc">${d.title}</span>
460
- <span class="rank-score">${score}</span>
461
- `;
462
- el.appendChild(li);
463
- });
464
- }
465
- renderRanking('rank-baseline', baseline, 'base_score');
466
- renderRanking('rank-fresh', fresh, 'r_t');
467
-
468
- // ─── Render answers from data.json ────────────────────────────────────
469
- // baked_answers is the default. innerHTML is safe here because we own
470
- // the source data; a future BYOK flow would need DOMPurify or a proper
471
- // markdown→HTML pipeline before rendering user-supplied content.
472
- const ans = data.baked_answers;
473
-
474
- function renderAnswer(side) {
475
- const a = ans[side];
476
- document.getElementById(`${side}-context-label`).textContent = `Top context: ${a.context_label}`;
477
- const html = [
478
- `<p>${a.intro}</p>`,
479
- '<ul>' + a.bullets.map(b => `<li>${b}</li>`).join('') + '</ul>',
480
- `<p>${a.outro}</p>`,
481
- ].join('');
482
- document.getElementById(`${side}-answer`).innerHTML = html;
483
- const v = document.getElementById(`${side}-verdict`);
484
- v.classList.add(a.verdict_class);
485
- v.textContent = a.verdict_text;
486
- }
487
- renderAnswer('stale');
488
- renderAnswer('fresh');
489
-
490
- // ─── Regen-source note ────────────────────────────────────────────────
491
- const note = document.getElementById('regen-note-anchor');
492
- if (ans._last_regenerated) {
493
- const when = new Date(ans._last_regenerated).toISOString().slice(0, 10);
494
- note.innerHTML = `
495
- <div class="regen-note">
496
- <strong>Live regeneration:</strong> these two answers were produced by
497
- <code>${ans._last_model || 'Claude'}</code> on <code>${when}</code> from the
498
- retrieval set above. Same model, same query, different top-3 docs.
499
- </div>`;
500
- } else {
501
- note.innerHTML = `
502
- <div class="regen-note">
503
- <strong>Illustrative outputs:</strong> the two answers below are baked
504
- into <code>data.json</code> — no API key needed to view this page.
505
- Run <code>node generate.mjs</code> with your own
506
- <code>ANTHROPIC_API_KEY</code> to replace them with a fresh Claude call
507
- against the same retrieval set.
508
- </div>`;
509
- }
510
- })();
511
- </script>
512
- </body>
513
- </html>
@@ -1,61 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>FreshContext logo — JPEG exporter</title>
6
- <style>
7
- body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 40px; background: #fafaf7; color: #1a1a1a; }
8
- h1 { font-size: 20px; margin: 0 0 16px; }
9
- p { margin: 0 0 12px; color: #555; font-size: 14px; }
10
- .preview { display: inline-block; padding: 12px; background: white; border: 1px solid #d4d0c4; border-radius: 4px; margin: 16px 0; }
11
- canvas { display: block; }
12
- button { background: #2a4d8f; color: white; border: 0; padding: 10px 20px; border-radius: 4px; font-size: 14px; cursor: pointer; }
13
- button:hover { background: #1f3a6e; }
14
- </style>
15
- </head>
16
- <body>
17
-
18
- <h1>FreshContext logo — JPEG export</h1>
19
- <p>150×150 JPEG, exactly what AgenticMarket requires. Click the button to download.</p>
20
-
21
- <div class="preview">
22
- <canvas id="canvas" width="150" height="150"></canvas>
23
- </div>
24
-
25
- <p><button id="download">Download as JPEG</button></p>
26
-
27
- <script>
28
- // SVG inlined directly to avoid browser fetch() restrictions on file:// URLs.
29
- const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" viewBox="0 0 150 150">
30
- <rect width="150" height="150" fill="#fafaf7"/>
31
- <line x1="20" y1="115" x2="130" y2="115" stroke="#2a4d8f" stroke-width="2" stroke-linecap="round" opacity="0.25" stroke-dasharray="3,4"/>
32
- <path d="M 25 28 C 55 28, 70 105, 125 116" stroke="#2a4d8f" stroke-width="8" fill="none" stroke-linecap="round"/>
33
- <circle cx="25" cy="28" r="5" fill="#2a4d8f"/>
34
- </svg>`;
35
-
36
- const blob = new Blob([svgString], { type: 'image/svg+xml' });
37
- const url = URL.createObjectURL(blob);
38
-
39
- const img = new Image();
40
- img.onload = () => {
41
- const canvas = document.getElementById('canvas');
42
- const ctx = canvas.getContext('2d');
43
- ctx.fillStyle = '#fafaf7';
44
- ctx.fillRect(0, 0, 150, 150);
45
- ctx.drawImage(img, 0, 0, 150, 150);
46
- };
47
- img.src = url;
48
-
49
- document.getElementById('download').onclick = () => {
50
- const canvas = document.getElementById('canvas');
51
- canvas.toBlob((b) => {
52
- const a = document.createElement('a');
53
- a.href = URL.createObjectURL(b);
54
- a.download = 'freshcontext-logo.jpg';
55
- a.click();
56
- }, 'image/jpeg', 0.95);
57
- };
58
- </script>
59
-
60
- </body>
61
- </html>
package/demo/logo.svg DELETED
@@ -1,23 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg width="150" height="150" viewBox="0 0 150 150" xmlns="http://www.w3.org/2000/svg">
3
- <!-- Background -->
4
- <rect width="150" height="150" fill="#fafaf7"/>
5
-
6
- <!-- Floor line (subtle): represents R_t hard floor / threshold -->
7
- <line x1="20" y1="115" x2="130" y2="115"
8
- stroke="#2a4d8f"
9
- stroke-width="2"
10
- stroke-linecap="round"
11
- opacity="0.25"
12
- stroke-dasharray="3,4"/>
13
-
14
- <!-- Decay curve (main): R_t = R_0 · e^(-λt) -->
15
- <path d="M 25 28 C 55 28, 70 105, 125 116"
16
- stroke="#2a4d8f"
17
- stroke-width="8"
18
- fill="none"
19
- stroke-linecap="round"/>
20
-
21
- <!-- R_0 anchor dot (top-left, where the data starts fresh) -->
22
- <circle cx="25" cy="28" r="5" fill="#2a4d8f"/>
23
- </svg>