Last active
March 9, 2026 19:15
-
-
Save bojanrajkovic/b5088863417715c9bf262dc63af930e7 to your computer and use it in GitHub Desktop.
Loupe: Comment Re-Anchoring — Four Anchor States Prototype
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Loupe — Comment Re-Anchoring States</title> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #0d0d17; | |
| --panel-bg: #13131f; | |
| --surface: #1a1a2e; | |
| --surface-hover: #202038; | |
| --border: rgba(120, 115, 175, 0.18); | |
| --border-subtle: rgba(120, 115, 175, 0.10); | |
| --text: #e8e6f0; | |
| --text-muted: #7e7a96; | |
| --text-dim: #4e4a66; | |
| /* diff line backgrounds */ | |
| --diff-add: rgba(74, 222, 128, 0.08); | |
| --diff-add-num: rgba(74, 222, 128, 0.15); | |
| --diff-del: rgba(248, 113, 113, 0.08); | |
| --diff-del-num: rgba(248, 113, 113, 0.15); | |
| --diff-ctx: transparent; | |
| --diff-ctx-num: rgba(120, 115, 175, 0.06); | |
| --diff-hunk: rgba(99, 102, 241, 0.10); | |
| --diff-hunk-label: #818cf8; | |
| /* state accent colors — severity gradient: no color → blue → amber → red */ | |
| --shifted: #60a5fa; | |
| --shifted-dim: rgba(96, 165, 250, 0.12); | |
| --outdated: #f59e0b; | |
| --outdated-dim: rgba(245, 158, 11, 0.12); | |
| --orphaned: #dc2626; | |
| --orphaned-dim: rgba(220, 38, 38, 0.12); | |
| --radius-sm: 4px; | |
| --radius: 8px; | |
| --radius-lg: 12px; | |
| --mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', ui-monospace, 'Courier New', monospace; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| line-height: 1.5; | |
| } | |
| /* ── Layout ── */ | |
| .app { | |
| max-width: 960px; | |
| margin: 0 auto; | |
| padding: 32px 24px 64px; | |
| } | |
| /* ── Header ── */ | |
| .header { | |
| margin-bottom: 32px; | |
| } | |
| .header-eyebrow { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| margin-bottom: 10px; | |
| } | |
| .header h1 { | |
| font-size: 26px; | |
| font-weight: 700; | |
| color: var(--text); | |
| letter-spacing: -0.02em; | |
| margin-bottom: 6px; | |
| } | |
| .header p { | |
| font-size: 14px; | |
| color: var(--text-muted); | |
| max-width: 620px; | |
| line-height: 1.7; | |
| } | |
| /* ── Tab Navigation ── */ | |
| .tabs { | |
| display: flex; | |
| gap: 4px; | |
| margin-bottom: 28px; | |
| background: var(--panel-bg); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| padding: 5px; | |
| } | |
| .tab { | |
| flex: 1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 9px 16px; | |
| border-radius: var(--radius); | |
| border: none; | |
| background: none; | |
| cursor: pointer; | |
| font-size: 13px; | |
| font-weight: 500; | |
| color: var(--text-muted); | |
| transition: background 0.18s ease, color 0.18s ease; | |
| white-space: nowrap; | |
| } | |
| .tab:hover { | |
| background: var(--surface-hover); | |
| color: var(--text); | |
| } | |
| /* Anchored tab: default grey — no accent color */ | |
| .tab.active { | |
| background: var(--surface); | |
| color: var(--text); | |
| } | |
| .tab.active.tab-shifted { background: var(--shifted-dim); color: var(--shifted); } | |
| .tab.active.tab-outdated { background: var(--outdated-dim); color: var(--outdated); } | |
| .tab.active.tab-orphaned { background: var(--orphaned-dim); color: var(--orphaned); } | |
| /* ── State Panels ── */ | |
| .state-panel { display: none; } | |
| .state-panel.active { display: block; } | |
| /* ── State Info Bar — caption style ── */ | |
| .state-info { | |
| border-radius: var(--radius); | |
| padding: 6px 0; | |
| margin-bottom: 16px; | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| line-height: 1.5; | |
| } | |
| /* ── Diff File Block ── */ | |
| .diff-file { | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-lg); | |
| overflow: hidden; | |
| background: var(--panel-bg); | |
| } | |
| .diff-file-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 16px; | |
| background: var(--surface); | |
| border-bottom: 1px solid var(--border); | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: var(--text-muted); | |
| } | |
| .diff-file-header .file-icon { | |
| opacity: 0.5; | |
| flex-shrink: 0; | |
| } | |
| .diff-file-header .file-path { | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| color: var(--text); | |
| flex: 1; | |
| } | |
| .diff-file-header .diff-stats { | |
| display: flex; | |
| gap: 8px; | |
| font-size: 11px; | |
| } | |
| .stat-add { color: #4ade80; } | |
| .stat-del { color: #f87171; } | |
| /* ── Orphaned file-level header ── */ | |
| .orphaned-file-banner { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 10px 16px; | |
| background: rgba(220, 38, 38, 0.06); | |
| border-bottom: 1px solid rgba(220, 38, 38, 0.2); | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| } | |
| .orphaned-file-banner .orphaned-count { | |
| font-weight: 600; | |
| color: var(--orphaned); | |
| } | |
| /* ── Diff Table ── */ | |
| .diff-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-family: var(--mono); | |
| font-size: 12.5px; | |
| line-height: 1.6; | |
| } | |
| .diff-table tr { position: relative; } | |
| /* line number cells */ | |
| .diff-table td.ln { | |
| width: 44px; | |
| min-width: 44px; | |
| text-align: right; | |
| padding: 0 8px; | |
| user-select: none; | |
| color: var(--text-dim); | |
| font-size: 11.5px; | |
| vertical-align: top; | |
| } | |
| .diff-table tr.add td.ln { background: var(--diff-add-num); } | |
| .diff-table tr.del td.ln { background: var(--diff-del-num); } | |
| .diff-table tr.ctx td.ln { background: var(--diff-ctx-num); } | |
| .diff-table tr.hunk td { background: var(--diff-hunk); color: var(--text-muted); font-size: 11.5px; padding: 4px 12px; } | |
| .diff-table tr.hunk td .hunk-label { color: var(--diff-hunk-label); font-weight: 600; } | |
| /* sign column */ | |
| .diff-table td.sign { | |
| width: 18px; | |
| min-width: 18px; | |
| text-align: center; | |
| padding: 0 2px; | |
| user-select: none; | |
| vertical-align: top; | |
| } | |
| .diff-table tr.add td.sign { background: var(--diff-add); color: #4ade80; } | |
| .diff-table tr.del td.sign { background: var(--diff-del); color: #f87171; } | |
| .diff-table tr.ctx td.sign { background: var(--diff-ctx); color: transparent; } | |
| /* code cell */ | |
| .diff-table td.code { | |
| padding: 0 12px 0 4px; | |
| width: 100%; | |
| vertical-align: top; | |
| white-space: pre; | |
| } | |
| .diff-table tr.add td.code { background: var(--diff-add); } | |
| .diff-table tr.del td.code { background: var(--diff-del); } | |
| .diff-table tr.ctx td.code { background: var(--diff-ctx); } | |
| /* ── Inline Comment Thread ── */ | |
| .comment-thread { | |
| background: var(--surface); | |
| border-top: 1px solid var(--border-subtle); | |
| border-bottom: 1px solid var(--border-subtle); | |
| } | |
| .comment-thread td { | |
| padding: 0 !important; | |
| background: var(--surface) !important; | |
| } | |
| .comment-thread-inner { | |
| padding: 14px 18px 14px 58px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| /* ── Comment Bubble ── */ | |
| .comment-bubble { | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-start; | |
| } | |
| .avatar { | |
| width: 28px; height: 28px; | |
| border-radius: 50%; | |
| flex-shrink: 0; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 11px; | |
| font-weight: 700; | |
| color: #fff; | |
| background: #5b21b6; | |
| } | |
| .comment-content { | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| .comment-meta { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 4px; | |
| font-size: 12px; | |
| } | |
| .comment-author { font-weight: 600; color: var(--text); } | |
| .comment-time { color: var(--text-muted); } | |
| .comment-body { | |
| font-size: 13px; | |
| color: var(--text); | |
| line-height: 1.6; | |
| } | |
| .comment-body code { | |
| font-family: var(--mono); | |
| font-size: 11.5px; | |
| background: rgba(120, 115, 175, 0.15); | |
| padding: 1px 5px; | |
| border-radius: 3px; | |
| color: #c4b5fd; | |
| } | |
| /* ── Anchored microtext ── */ | |
| .anchored-check { | |
| font-size: 11px; | |
| color: #6b8f6b; | |
| font-weight: 500; | |
| } | |
| /* ── State Badges ── */ | |
| .badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 2px 8px; | |
| border-radius: 20px; | |
| font-size: 11px; | |
| font-weight: 600; | |
| line-height: 1.5; | |
| white-space: nowrap; | |
| } | |
| .badge-shifted { background: rgba(96, 165, 250, 0.15); color: var(--shifted); border: 1px solid rgba(96, 165, 250, 0.25); } | |
| .badge-outdated { background: rgba(245, 158, 11, 0.15); color: var(--outdated); border: 1px solid rgba(245, 158, 11, 0.25); } | |
| .badge-orphaned { background: rgba(220, 38, 38, 0.15); color: var(--orphaned); border: 1px solid rgba(220, 38, 38, 0.25); } | |
| /* ── Shifted ghost row ── */ | |
| .ghost-row td { | |
| opacity: 0.35; | |
| border-top: 1px dashed rgba(96, 165, 250, 0.3) !important; | |
| } | |
| .ghost-indicator { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| font-size: 11px; | |
| color: var(--shifted); | |
| opacity: 0.7; | |
| padding: 2px 0; | |
| } | |
| /* ── Outdated context block ── */ | |
| .outdated-context { | |
| margin-top: 10px; | |
| border: 1px solid rgba(245, 158, 11, 0.25); | |
| border-radius: var(--radius); | |
| overflow: hidden; | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| } | |
| .outdated-context-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 6px 10px; | |
| background: rgba(245, 158, 11, 0.08); | |
| border-bottom: 1px solid rgba(245, 158, 11, 0.15); | |
| font-size: 11px; | |
| font-weight: 600; | |
| color: var(--outdated); | |
| letter-spacing: 0.03em; | |
| text-transform: uppercase; | |
| } | |
| .outdated-context-code { | |
| padding: 8px 10px; | |
| background: rgba(245, 158, 11, 0.04); | |
| color: #d4c07a; | |
| line-height: 1.6; | |
| white-space: pre; | |
| } | |
| /* ── Orphaned section ── */ | |
| .orphaned-comment-section { | |
| border: 1px solid rgba(220, 38, 38, 0.2); | |
| border-radius: var(--radius-lg); | |
| overflow: hidden; | |
| margin-bottom: 20px; | |
| } | |
| .orphaned-section-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 10px 16px; | |
| background: rgba(220, 38, 38, 0.06); | |
| border-bottom: 1px solid rgba(220, 38, 38, 0.15); | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| .orphaned-section-header:hover { | |
| background: rgba(220, 38, 38, 0.10); | |
| } | |
| .orphaned-section-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: var(--orphaned); | |
| } | |
| .orphaned-section-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| } | |
| .chevron { | |
| transition: transform 0.2s ease; | |
| color: var(--text-muted); | |
| } | |
| .chevron.open { transform: rotate(180deg); } | |
| .orphaned-context-block { | |
| overflow: hidden; | |
| max-height: 0; | |
| transition: max-height 0.3s ease; | |
| } | |
| .orphaned-context-block.expanded { | |
| max-height: 300px; | |
| } | |
| .orphaned-context-inner { | |
| background: rgba(220, 38, 38, 0.03); | |
| border-bottom: 1px solid rgba(220, 38, 38, 0.12); | |
| } | |
| .orphaned-context-label { | |
| padding: 8px 16px 4px; | |
| font-size: 10px; | |
| font-weight: 700; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| color: var(--orphaned); | |
| opacity: 0.7; | |
| } | |
| .orphaned-context-code { | |
| padding: 6px 16px 12px; | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| color: #d4a0a8; | |
| line-height: 1.6; | |
| white-space: pre; | |
| } | |
| /* Orphaned comment body uses consistent left-padding matching inline threads */ | |
| .orphaned-comment-body { | |
| padding: 14px 18px 14px 58px; | |
| background: var(--surface); | |
| } | |
| /* ── Syntax colors (minimal) ── */ | |
| .kw { color: #c792ea; } /* keyword */ | |
| .fn { color: #82aaff; } /* function */ | |
| .str { color: #c3e88d; } /* string */ | |
| .num { color: #f78c6c; } /* number */ | |
| .cmt { color: #546e7a; font-style: italic; } /* comment */ | |
| .ty { color: #ffcb6b; } /* type */ | |
| .op { color: #89ddff; } /* operator */ | |
| .pn { color: #bfc7d5; } /* punctuation */ | |
| /* ── Keyboard hint ── */ | |
| .keyboard-hint { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 6px; | |
| margin-top: 28px; | |
| font-size: 11.5px; | |
| color: var(--text-dim); | |
| } | |
| kbd { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-width: 22px; | |
| height: 22px; | |
| padding: 0 6px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| box-shadow: 0 1px 0 var(--border); | |
| } | |
| /* ── Scrollbar ── */ | |
| ::-webkit-scrollbar { width: 6px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: rgba(120,115,175,0.2); border-radius: 3px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <!-- ── Header ── --> | |
| <div class="header"> | |
| <div class="header-eyebrow"> | |
| Loupe — Prototype | |
| </div> | |
| <h1>Comment Re-Anchoring States</h1> | |
| <p>As a branch evolves, comments anchored to specific lines are re-evaluated against the latest diff. Each comment lands in one of four states depending on whether its original content is still present, moved, changed, or gone.</p> | |
| </div> | |
| <!-- ── Tabs ── --> | |
| <div class="tabs" role="tablist" aria-label="Anchor states"> | |
| <button class="tab tab-anchored active" role="tab" aria-selected="true" data-state="anchored" onclick="switchTab('anchored')"> | |
| Anchored | |
| </button> | |
| <button class="tab tab-shifted" role="tab" aria-selected="false" data-state="shifted" onclick="switchTab('shifted')"> | |
| Shifted | |
| </button> | |
| <button class="tab tab-outdated" role="tab" aria-selected="false" data-state="outdated" onclick="switchTab('outdated')"> | |
| Outdated | |
| </button> | |
| <button class="tab tab-orphaned" role="tab" aria-selected="false" data-state="orphaned" onclick="switchTab('orphaned')"> | |
| Orphaned | |
| </button> | |
| </div> | |
| <!-- ════════════════════════════════════════════ | |
| STATE 1 — ANCHORED | |
| ════════════════════════════════════════════ --> | |
| <div class="state-panel active" id="panel-anchored"> | |
| <div class="state-info"> | |
| Comment is exactly where it was placed — no action needed. | |
| </div> | |
| <div class="diff-file"> | |
| <div class="diff-file-header"> | |
| <svg class="file-icon" width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M9.5 1H3a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.5L9.5 1z" stroke="currentColor" stroke-width="1.2"/><path d="M9 1v5h5" stroke="currentColor" stroke-width="1.2"/></svg> | |
| <span class="file-path">src/lib/server/retry.ts</span> | |
| <div class="diff-stats"> | |
| <span class="stat-add">+12</span> | |
| <span class="stat-del">-3</span> | |
| </div> | |
| </div> | |
| <table class="diff-table"> | |
| <tbody> | |
| <tr class="hunk"><td colspan="3"><span class="hunk-label">@@ -38,7 +38,16 @@</span> export function createRetryPolicy</td></tr> | |
| <tr class="ctx"><td class="ln">38</td><td class="sign"> </td><td class="code"><span class="kw">export function</span> <span class="fn">createRetryPolicy</span><span class="pn">(</span><span class="ty">config</span><span class="op">:</span> <span class="ty">RetryConfig</span><span class="pn">)</span><span class="op">:</span> <span class="ty">RetryPolicy</span> <span class="pn">{</span></td></tr> | |
| <tr class="ctx"><td class="ln">39</td><td class="sign"> </td><td class="code"> <span class="kw">const</span> resolved <span class="op">=</span> <span class="pn">{</span> <span class="op">...</span>DEFAULT_RETRY_CONFIG<span class="pn">,</span> <span class="op">...</span>config <span class="pn">};</span></td></tr> | |
| <tr class="ctx"><td class="ln">40</td><td class="sign"> </td><td class="code"> </td></tr> | |
| <tr class="add"><td class="ln">41</td><td class="sign">+</td><td class="code"> <span class="cmt">// Validate before constructing the policy object</span></td></tr> | |
| <tr class="add"><td class="ln">42</td><td class="sign">+</td><td class="code"> <span class="fn">validateRetryConfig</span><span class="pn">(</span>resolved<span class="pn">);</span></td></tr> | |
| <tr class="ctx"><td class="ln">43</td><td class="sign"> </td><td class="code"> </td></tr> | |
| <tr class="ctx"><td class="ln">44</td><td class="sign"> </td><td class="code"> <span class="kw">return new</span> <span class="ty">RetryPolicy</span><span class="pn">(</span>resolved<span class="pn">);</span></td></tr> | |
| <tr class="ctx"><td class="ln">45</td><td class="sign"> </td><td class="code"><span class="pn">}</span></td></tr> | |
| <!-- anchored comment thread on line 42 --> | |
| <tr class="comment-thread"> | |
| <td colspan="3"> | |
| <div class="comment-thread-inner"> | |
| <div class="comment-bubble"> | |
| <div class="avatar">MK</div> | |
| <div class="comment-content"> | |
| <div class="comment-meta"> | |
| <span class="comment-author">marta.k</span> | |
| <span class="comment-time">2 days ago</span> | |
| <span class="anchored-check">✓ Anchored</span> | |
| </div> | |
| <div class="comment-body"> | |
| Should <code>validateRetryConfig</code> throw on invalid input, or return a validation result? If it throws, callers can't distinguish config errors from runtime errors at the catch site. | |
| </div> | |
| </div> | |
| </div> | |
| <div class="comment-bubble"> | |
| <div class="avatar" style="background:#1d4ed8">BR</div> | |
| <div class="comment-content"> | |
| <div class="comment-meta"> | |
| <span class="comment-author">bojan</span> | |
| <span class="comment-time">1 day ago</span> | |
| </div> | |
| <div class="comment-body"> | |
| Good point. I'll introduce a <code>ConfigError</code> subclass so callers can <code>catch (e) { if (e instanceof ConfigError) ... }</code>. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr class="ctx"><td class="ln">46</td><td class="sign"> </td><td class="code"> </td></tr> | |
| <tr class="add"><td class="ln">47</td><td class="sign">+</td><td class="code"><span class="kw">function</span> <span class="fn">validateRetryConfig</span><span class="pn">(</span>cfg<span class="op">:</span> <span class="ty">ResolvedRetryConfig</span><span class="pn">)</span><span class="op">:</span> <span class="kw">void</span> <span class="pn">{</span></td></tr> | |
| <tr class="add"><td class="ln">48</td><td class="sign">+</td><td class="code"> <span class="kw">if</span> <span class="pn">(</span>cfg<span class="pn">.</span>maxAttempts <span class="op"><</span> <span class="num">1</span><span class="pn">)</span> <span class="kw">throw new</span> <span class="ty">ConfigError</span><span class="pn">(</span><span class="str">"maxAttempts must be ≥ 1"</span><span class="pn">);</span></td></tr> | |
| <tr class="add"><td class="ln">49</td><td class="sign">+</td><td class="code"> <span class="kw">if</span> <span class="pn">(</span>cfg<span class="pn">.</span>baseDelayMs <span class="op"><</span> <span class="num">0</span><span class="pn">)</span> <span class="kw">throw new</span> <span class="ty">ConfigError</span><span class="pn">(</span><span class="str">"baseDelayMs must be ≥ 0"</span><span class="pn">);</span></td></tr> | |
| <tr class="add"><td class="ln">50</td><td class="sign">+</td><td class="code"><span class="pn">}</span></td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- ════════════════════════════════════════════ | |
| STATE 2 — SHIFTED | |
| ════════════════════════════════════════════ --> | |
| <div class="state-panel" id="panel-shifted"> | |
| <div class="state-info"> | |
| Same content moved to a different line — comment follows automatically. | |
| </div> | |
| <div class="diff-file"> | |
| <div class="diff-file-header"> | |
| <svg class="file-icon" width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M9.5 1H3a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.5L9.5 1z" stroke="currentColor" stroke-width="1.2"/><path d="M9 1v5h5" stroke="currentColor" stroke-width="1.2"/></svg> | |
| <span class="file-path">src/lib/server/http.ts</span> | |
| <div class="diff-stats"> | |
| <span class="stat-add">+9</span> | |
| <span class="stat-del">-0</span> | |
| </div> | |
| </div> | |
| <table class="diff-table"> | |
| <tbody> | |
| <tr class="hunk"><td colspan="3"><span class="hunk-label">@@ -15,6 +15,15 @@</span> function handleRequest</td></tr> | |
| <tr class="ctx"><td class="ln">15</td><td class="sign"> </td><td class="code"><span class="kw">function</span> <span class="fn">handleRequest</span><span class="pn">(</span>req<span class="op">:</span> <span class="ty">IncomingMessage</span><span class="pn">)</span><span class="op">:</span> <span class="ty">ParsedRequest</span> <span class="pn">{</span></td></tr> | |
| <tr class="add"><td class="ln">16</td><td class="sign">+</td><td class="code"> <span class="kw">const</span> traceId <span class="op">=</span> <span class="fn">generateTraceId</span><span class="pn">();</span></td></tr> | |
| <tr class="add"><td class="ln">17</td><td class="sign">+</td><td class="code"> <span class="kw">const</span> logger <span class="op">=</span> <span class="fn">createRequestLogger</span><span class="pn">(</span>traceId<span class="pn">);</span></td></tr> | |
| <tr class="add"><td class="ln">18</td><td class="sign">+</td><td class="code"> logger<span class="pn">.</span><span class="fn">debug</span><span class="pn">(</span><span class="str">"request received"</span><span class="pn">,</span> <span class="pn">{</span> url<span class="op">:</span> req<span class="pn">.</span>url <span class="pn">});</span></td></tr> | |
| <tr class="add"><td class="ln">19</td><td class="sign">+</td><td class="code"> <span class="kw">const</span> span <span class="op">=</span> tracer<span class="pn">.</span><span class="fn">startSpan</span><span class="pn">(</span><span class="str">"http.handle"</span><span class="pn">,</span> <span class="pn">{</span> traceId <span class="pn">});</span></td></tr> | |
| <tr class="add"><td class="ln">20</td><td class="sign">+</td><td class="code"> </td></tr> | |
| <tr class="add"><td class="ln">21</td><td class="sign">+</td><td class="code"> <span class="kw">try</span> <span class="pn">{</span></td></tr> | |
| <tr class="add"><td class="ln">22</td><td class="sign">+</td><td class="code"> <span class="kw">const</span> method <span class="op">=</span> req<span class="pn">.</span>method<span class="op">?.</span><span class="fn">toUpperCase</span><span class="pn">()</span> <span class="op">??</span> <span class="str">"GET"</span><span class="pn">;</span></td></tr> | |
| <tr class="add"><td class="ln">23</td><td class="sign">+</td><td class="code"> <span class="kw">const</span> url <span class="op">=</span> <span class="kw">new</span> <span class="ty">URL</span><span class="pn">(</span>req<span class="pn">.</span>url <span class="op">??</span> <span class="str">"/"</span><span class="pn">,</span> <span class="str">"http://localhost"</span><span class="pn">);</span></td></tr> | |
| <tr class="add"><td class="ln">24</td><td class="sign">+</td><td class="code"> <span class="kw">const</span> path <span class="op">=</span> url<span class="pn">.</span>pathname<span class="pn">;</span></td></tr> | |
| <tr class="add"><td class="ln">25</td><td class="sign">+</td><td class="code"> <span class="kw">const</span> query <span class="op">=</span> <span class="ty">Object</span><span class="pn">.</span><span class="fn">fromEntries</span><span class="pn">(</span>url<span class="pn">.</span>searchParams<span class="pn">.</span><span class="fn">entries</span><span class="pn">());</span></td></tr> | |
| <tr class="add"><td class="ln">26</td><td class="sign">+</td><td class="code"> </td></tr> | |
| <!-- ghost row — original position of the comment (line 18 of old file → visually before line 27) --> | |
| <tr class="ghost-row"> | |
| <td class="ln" style="color:var(--shifted);opacity:0.5">~18</td> | |
| <td class="sign" style="background:rgba(96,165,250,0.06);color:var(--shifted)"> </td> | |
| <td class="code" style="background:rgba(96,165,250,0.04)"> | |
| <span class="ghost-indicator"> | |
| <svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8l5 5 5-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> | |
| Shifted to line 27 | |
| </span> | |
| </td> | |
| </tr> | |
| <tr class="add"><td class="ln">27</td><td class="sign">+</td><td class="code"> <span class="kw">const</span> headers <span class="op">=</span> <span class="fn">parseHeaders</span><span class="pn">(</span>req<span class="pn">.</span>rawHeaders<span class="pn">);</span></td></tr> | |
| <!-- shifted comment thread on line 27 --> | |
| <tr class="comment-thread"> | |
| <td colspan="3"> | |
| <div class="comment-thread-inner"> | |
| <div class="comment-bubble"> | |
| <div class="avatar">MK</div> | |
| <div class="comment-content"> | |
| <div class="comment-meta"> | |
| <span class="comment-author">marta.k</span> | |
| <span class="comment-time">3 days ago</span> | |
| <span class="badge badge-shifted">↕ Shifted from line 18</span> | |
| </div> | |
| <div class="comment-body"> | |
| <code>parseHeaders</code> allocates a new object for every request — worth memoizing if the same raw header array is reused across retries. At least add a note in the JSDoc. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr class="ctx"><td class="ln">28</td><td class="sign"> </td><td class="code"> <span class="kw">return</span> <span class="pn">{</span> method<span class="pn">,</span> path<span class="pn">,</span> query<span class="pn">,</span> headers <span class="pn">};</span></td></tr> | |
| <tr class="ctx"><td class="ln">29</td><td class="sign"> </td><td class="code"> <span class="pn">}</span> <span class="kw">finally</span> <span class="pn">{</span></td></tr> | |
| <tr class="ctx"><td class="ln">30</td><td class="sign"> </td><td class="code"> span<span class="pn">.</span><span class="fn">end</span><span class="pn">();</span></td></tr> | |
| <tr class="ctx"><td class="ln">31</td><td class="sign"> </td><td class="code"> <span class="pn">}</span></td></tr> | |
| <tr class="ctx"><td class="ln">32</td><td class="sign"> </td><td class="code"><span class="pn">}</span></td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- ════════════════════════════════════════════ | |
| STATE 3 — OUTDATED | |
| ════════════════════════════════════════════ --> | |
| <div class="state-panel" id="panel-outdated"> | |
| <div class="state-info"> | |
| The commented code was modified or deleted — original context preserved. | |
| </div> | |
| <div class="diff-file"> | |
| <div class="diff-file-header"> | |
| <svg class="file-icon" width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M9.5 1H3a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.5L9.5 1z" stroke="currentColor" stroke-width="1.2"/><path d="M9 1v5h5" stroke="currentColor" stroke-width="1.2"/></svg> | |
| <span class="file-path">src/lib/server/backoff.ts</span> | |
| <div class="diff-stats"> | |
| <span class="stat-add">+3</span> | |
| <span class="stat-del">-1</span> | |
| </div> | |
| </div> | |
| <table class="diff-table"> | |
| <tbody> | |
| <tr class="hunk"><td colspan="3"><span class="hunk-label">@@ -28,9 +28,11 @@</span> function computeDelay</td></tr> | |
| <tr class="ctx"><td class="ln">28</td><td class="sign"> </td><td class="code"><span class="kw">function</span> <span class="fn">computeDelay</span><span class="pn">(</span>attempt<span class="op">:</span> <span class="kw">number</span><span class="pn">,</span> cfg<span class="op">:</span> <span class="ty">BackoffConfig</span><span class="pn">)</span><span class="op">:</span> <span class="kw">number</span> <span class="pn">{</span></td></tr> | |
| <tr class="ctx"><td class="ln">29</td><td class="sign"> </td><td class="code"> <span class="kw">const</span> jitter <span class="op">=</span> cfg<span class="pn">.</span>jitter <span class="op">?</span> <span class="ty">Math</span><span class="pn">.</span><span class="fn">random</span><span class="pn">()</span> <span class="op">*</span> cfg<span class="pn">.</span>baseDelayMs <span class="op">:</span> <span class="num">0</span><span class="pn">;</span></td></tr> | |
| <tr class="del"><td class="ln">30</td><td class="sign">-</td><td class="code"> <span class="kw">const</span> delay <span class="op">=</span> cfg<span class="pn">.</span>baseDelayMs <span class="op">*</span> attempt <span class="op">+</span> jitter<span class="pn">;</span> <span class="cmt">// linear</span></td></tr> | |
| <tr class="add"><td class="ln">30</td><td class="sign">+</td><td class="code"> <span class="kw">const</span> exp <span class="op">=</span> <span class="ty">Math</span><span class="pn">.</span><span class="fn">pow</span><span class="pn">(</span><span class="num">2</span><span class="pn">,</span> attempt<span class="pn">);</span></td></tr> | |
| <tr class="add"><td class="ln">31</td><td class="sign">+</td><td class="code"> <span class="kw">const</span> delay <span class="op">=</span> cfg<span class="pn">.</span>baseDelayMs <span class="op">*</span> exp <span class="op">+</span> jitter<span class="pn">;</span> <span class="cmt">// exponential</span></td></tr> | |
| <!-- outdated comment thread — shown at closest surviving position (line 31) --> | |
| <tr class="comment-thread"> | |
| <td colspan="3"> | |
| <div class="comment-thread-inner"> | |
| <div class="comment-bubble"> | |
| <div class="avatar">MK</div> | |
| <div class="comment-content"> | |
| <div class="comment-meta"> | |
| <span class="comment-author">marta.k</span> | |
| <span class="comment-time">4 days ago</span> | |
| <span class="badge badge-outdated">⚠ Outdated</span> | |
| </div> | |
| <div class="comment-body"> | |
| Linear backoff will hammer the service during thundering herd events. Should this be exponential with jitter? See <em>Exponential Backoff And Jitter</em> (AWS blog) for reference. | |
| </div> | |
| <div class="outdated-context"> | |
| <div class="outdated-context-header"> | |
| <svg width="11" height="11" viewBox="0 0 16 16" fill="none"><path d="M8 1v7l4 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/></svg> | |
| Original context | |
| </div> | |
| <div class="outdated-context-code"> const delay = cfg.baseDelayMs * attempt + jitter; // linear</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| <tr class="add"><td class="ln">32</td><td class="sign">+</td><td class="code"> <span class="kw">const</span> capped <span class="op">=</span> <span class="ty">Math</span><span class="pn">.</span><span class="fn">min</span><span class="pn">(</span>delay<span class="pn">,</span> cfg<span class="pn">.</span>maxDelayMs<span class="pn">);</span></td></tr> | |
| <tr class="ctx"><td class="ln">33</td><td class="sign"> </td><td class="code"> <span class="kw">return</span> capped<span class="pn">;</span></td></tr> | |
| <tr class="ctx"><td class="ln">34</td><td class="sign"> </td><td class="code"><span class="pn">}</span></td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- ════════════════════════════════════════════ | |
| STATE 4 — ORPHANED | |
| ════════════════════════════════════════════ --> | |
| <div class="state-panel" id="panel-orphaned"> | |
| <div class="state-info"> | |
| Anchor text is completely gone from the file — comment surfaced at file level. | |
| </div> | |
| <!-- Orphaned comment surfaced at file level (before the diff table) --> | |
| <div class="orphaned-comment-section"> | |
| <div class="orphaned-section-header" onclick="toggleOrphanedContext(this)" aria-expanded="false"> | |
| <div class="orphaned-section-left"> | |
| <span class="badge badge-orphaned">✗ Orphaned</span> | |
| <span>comment on deleted content</span> | |
| </div> | |
| <div class="orphaned-section-right"> | |
| <span>show original context</span> | |
| <svg class="chevron" width="14" height="14" viewBox="0 0 16 16" fill="none"> | |
| <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |
| </svg> | |
| </div> | |
| </div> | |
| <div class="orphaned-context-block" id="orphaned-ctx"> | |
| <div class="orphaned-context-inner"> | |
| <div class="orphaned-context-label">Original context (deleted)</div> | |
| <div class="orphaned-context-code">function legacyNormalizeUrl(raw: string): string { | |
| // Strip trailing slashes, lowercase hostname, drop default port | |
| const u = new URL(raw.trim()); | |
| u.hostname = u.hostname.toLowerCase(); | |
| if ((u.protocol === "http:" && u.port === "80") || | |
| (u.protocol === "https:" && u.port === "443")) { | |
| u.port = ""; | |
| } | |
| return u.toString().replace(/\/$/, ""); | |
| }</div> | |
| </div> | |
| </div> | |
| <div class="orphaned-comment-body"> | |
| <div class="comment-bubble"> | |
| <div class="avatar">MK</div> | |
| <div class="comment-content"> | |
| <div class="comment-meta"> | |
| <span class="comment-author">marta.k</span> | |
| <span class="comment-time">5 days ago</span> | |
| </div> | |
| <div class="comment-body"> | |
| This doesn’t handle IPv6 addresses correctly — <code>u.hostname</code> includes brackets for IPv6 but <code>toLowerCase()</code> is still safe there. The real issue is <code>URL</code> constructor will throw on <code>raw</code> values that aren’t absolute URLs. Should add a try/catch or require callers to pre-validate. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- The actual diff of the file (the function was deleted) --> | |
| <div class="diff-file"> | |
| <div class="diff-file-header"> | |
| <svg class="file-icon" width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M9.5 1H3a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.5L9.5 1z" stroke="currentColor" stroke-width="1.2"/><path d="M9 1v5h5" stroke="currentColor" stroke-width="1.2"/></svg> | |
| <span class="file-path">src/lib/server/url.ts</span> | |
| <div class="diff-stats"> | |
| <span class="stat-add">+1</span> | |
| <span class="stat-del">-14</span> | |
| </div> | |
| </div> | |
| <div class="orphaned-file-banner"> | |
| <svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M8 5v4M8 11v.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/></svg> | |
| <span class="orphaned-count">1 orphaned comment</span> | |
| <span>— the anchor content was removed from this file</span> | |
| </div> | |
| <table class="diff-table"> | |
| <tbody> | |
| <tr class="hunk"><td colspan="3"><span class="hunk-label">@@ -1,18 +1,5 @@</span> url utilities</td></tr> | |
| <tr class="ctx"><td class="ln">1</td><td class="sign"> </td><td class="code"><span class="kw">import</span> <span class="pn">{</span> normalizeUrl <span class="pn">}</span> <span class="kw">from</span> <span class="str">"./url-utils"</span><span class="pn">;</span></td></tr> | |
| <tr class="ctx"><td class="ln">2</td><td class="sign"> </td><td class="code"> </td></tr> | |
| <tr class="del"><td class="ln">3</td><td class="sign">-</td><td class="code"><span class="kw">function</span> <span class="fn">legacyNormalizeUrl</span><span class="pn">(</span>raw<span class="op">:</span> <span class="kw">string</span><span class="pn">)</span><span class="op">:</span> <span class="kw">string</span> <span class="pn">{</span></td></tr> | |
| <tr class="del"><td class="ln">4</td><td class="sign">-</td><td class="code"> <span class="cmt">// Strip trailing slashes, lowercase hostname, drop default port</span></td></tr> | |
| <tr class="del"><td class="ln">5</td><td class="sign">-</td><td class="code"> <span class="kw">const</span> u <span class="op">=</span> <span class="kw">new</span> <span class="ty">URL</span><span class="pn">(</span>raw<span class="pn">.</span><span class="fn">trim</span><span class="pn">());</span></td></tr> | |
| <tr class="del"><td class="ln">6</td><td class="sign">-</td><td class="code"> u<span class="pn">.</span>hostname <span class="op">=</span> u<span class="pn">.</span>hostname<span class="pn">.</span><span class="fn">toLowerCase</span><span class="pn">();</span></td></tr> | |
| <tr class="del"><td class="ln">7</td><td class="sign">-</td><td class="code"> <span class="kw">if</span> <span class="pn">((</span>u<span class="pn">.</span>protocol <span class="op">===</span> <span class="str">"http:"</span> <span class="op">&&</span> u<span class="pn">.</span>port <span class="op">===</span> <span class="str">"80"</span><span class="pn">)</span> <span class="op">||</span></td></tr> | |
| <tr class="del"><td class="ln">8</td><td class="sign">-</td><td class="code"> <span class="pn">(</span>u<span class="pn">.</span>protocol <span class="op">===</span> <span class="str">"https:"</span> <span class="op">&&</span> u<span class="pn">.</span>port <span class="op">===</span> <span class="str">"443"</span><span class="pn">)</span><span class="pn">)</span> <span class="pn">{</span></td></tr> | |
| <tr class="del"><td class="ln">9</td><td class="sign">-</td><td class="code"> u<span class="pn">.</span>port <span class="op">=</span> <span class="str">""</span><span class="pn">;</span></td></tr> | |
| <tr class="del"><td class="ln">10</td><td class="sign">-</td><td class="code"> <span class="pn">}</span></td></tr> | |
| <tr class="del"><td class="ln">11</td><td class="sign">-</td><td class="code"> <span class="kw">return</span> u<span class="pn">.</span><span class="fn">toString</span><span class="pn">().</span><span class="fn">replace</span><span class="pn">(</span><span class="str">/\/$/<br></span><span class="pn">,</span> <span class="str">""</span><span class="pn">);</span></td></tr> | |
| <tr class="del"><td class="ln">12</td><td class="sign">-</td><td class="code"><span class="pn">}</span></td></tr> | |
| <tr class="del"><td class="ln">13</td><td class="sign">-</td><td class="code"> </td></tr> | |
| <tr class="del"><td class="ln">14</td><td class="sign">-</td><td class="code"><span class="kw">export function</span> <span class="fn">buildApiUrl</span><span class="pn">(</span>path<span class="op">:</span> <span class="kw">string</span><span class="pn">)</span><span class="op">:</span> <span class="kw">string</span> <span class="pn">{</span></td></tr> | |
| <tr class="add"><td class="ln">3</td><td class="sign">+</td><td class="code"><span class="kw">export function</span> <span class="fn">buildApiUrl</span><span class="pn">(</span>path<span class="op">:</span> <span class="kw">string</span><span class="pn">)</span><span class="op">:</span> <span class="kw">string</span> <span class="pn">{</span></td></tr> | |
| <tr class="del"><td class="ln">15</td><td class="sign">-</td><td class="code"> <span class="kw">return</span> <span class="fn">legacyNormalizeUrl</span><span class="pn">(</span><span class="str">`${BASE_URL}/${path}`</span><span class="pn">);</span></td></tr> | |
| <tr class="add"><td class="ln">4</td><td class="sign">+</td><td class="code"> <span class="kw">return</span> <span class="fn">normalizeUrl</span><span class="pn">(</span><span class="str">`${BASE_URL}/${path}`</span><span class="pn">);</span></td></tr> | |
| <tr class="ctx"><td class="ln">5</td><td class="sign"> </td><td class="code"><span class="pn">}</span></td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Keyboard hint --> | |
| <div class="keyboard-hint"> | |
| <kbd>1</kbd><kbd>2</kbd><kbd>3</kbd><kbd>4</kbd> to switch states · | |
| <kbd>←</kbd><kbd>→</kbd> to navigate | |
| </div> | |
| </div><!-- /app --> | |
| <script> | |
| const STATES = ['anchored', 'shifted', 'outdated', 'orphaned']; | |
| let current = 0; | |
| function switchTab(state) { | |
| const idx = STATES.indexOf(state); | |
| if (idx === -1) return; | |
| current = idx; | |
| // Update tabs | |
| document.querySelectorAll('.tab').forEach(t => { | |
| t.classList.toggle('active', t.dataset.state === state); | |
| t.setAttribute('aria-selected', t.dataset.state === state ? 'true' : 'false'); | |
| }); | |
| // Update panels | |
| document.querySelectorAll('.state-panel').forEach(p => { | |
| p.classList.toggle('active', p.id === 'panel-' + state); | |
| }); | |
| } | |
| function toggleOrphanedContext(headerEl) { | |
| const ctx = document.getElementById('orphaned-ctx'); | |
| const chevron = headerEl.querySelector('.chevron'); | |
| const isOpen = ctx.classList.contains('expanded'); | |
| ctx.classList.toggle('expanded', !isOpen); | |
| chevron.classList.toggle('open', !isOpen); | |
| headerEl.setAttribute('aria-expanded', String(!isOpen)); | |
| const label = headerEl.querySelector('.orphaned-section-right span'); | |
| label.textContent = isOpen ? 'show original context' : 'hide original context'; | |
| } | |
| // Keyboard navigation | |
| document.addEventListener('keydown', e => { | |
| // Number keys 1–4 | |
| if (e.key >= '1' && e.key <= '4' && !e.ctrlKey && !e.metaKey && !e.altKey) { | |
| switchTab(STATES[parseInt(e.key, 10) - 1]); | |
| return; | |
| } | |
| // Arrow keys | |
| if ((e.key === 'ArrowRight' || e.key === 'ArrowDown') && !e.ctrlKey && !e.metaKey) { | |
| e.preventDefault(); | |
| switchTab(STATES[(current + 1) % STATES.length]); | |
| } else if ((e.key === 'ArrowLeft' || e.key === 'ArrowUp') && !e.ctrlKey && !e.metaKey) { | |
| e.preventDefault(); | |
| switchTab(STATES[(current + STATES.length - 1) % STATES.length]); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment