@drunkcoding/agents-and-skills 0.0.44 โ†’ 0.0.46

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.
Files changed (107) hide show
  1. package/.claude-plugin/marketplace.json +2 -47
  2. package/README.md +3 -6
  3. package/package.json +4 -1
  4. package/plugins/multica-tool/.claude-plugin/plugin.json +1 -1
  5. package/plugins/multica-tool/scripts/lib.mjs +53 -4
  6. package/plugins/multica-tool/scripts/multica-export.mjs +170 -50
  7. package/plugins/multica-tool/scripts/multica-import.mjs +209 -27
  8. package/plugins/multica-tool/skills/export/SKILL.md +91 -22
  9. package/plugins/multica-tool/skills/import/SKILL.md +24 -9
  10. package/plugins/multica-tool/skills/sync/SKILL.md +1 -1
  11. package/plugins/plugin-validator/.claude-plugin/plugin.json +1 -1
  12. package/plugins/html-effectiveness/.claude-plugin/plugin.json +0 -17
  13. package/plugins/html-effectiveness/README.md +0 -28
  14. package/plugins/html-effectiveness/agents/report-builder.md +0 -66
  15. package/plugins/html-effectiveness/assets/base.css +0 -51
  16. package/plugins/html-effectiveness/assets/base.js +0 -11
  17. package/plugins/html-effectiveness/assets/charts.js +0 -1
  18. package/plugins/html-effectiveness/assets/components.css +0 -10
  19. package/plugins/html-effectiveness/commands/html-report.md +0 -13
  20. package/plugins/html-effectiveness/docs/template-gallery.md +0 -28
  21. package/plugins/html-effectiveness/package.json +0 -6
  22. package/plugins/html-effectiveness/scripts/_tmp.js +0 -37
  23. package/plugins/html-effectiveness/scripts/catalog.js +0 -47
  24. package/plugins/html-effectiveness/scripts/extract.js +0 -104
  25. package/plugins/html-effectiveness/scripts/mustache.js +0 -90
  26. package/plugins/html-effectiveness/scripts/render.js +0 -176
  27. package/plugins/html-effectiveness/skills/html-effectiveness/SKILL.md +0 -94
  28. package/plugins/html-effectiveness/templates/01-exploration-code-approaches.html.tmpl +0 -400
  29. package/plugins/html-effectiveness/templates/02-exploration-visual-designs.html.tmpl +0 -456
  30. package/plugins/html-effectiveness/templates/03-code-review-pr.html.tmpl +0 -581
  31. package/plugins/html-effectiveness/templates/04-code-understanding.html.tmpl +0 -443
  32. package/plugins/html-effectiveness/templates/05-design-system.html.tmpl +0 -578
  33. package/plugins/html-effectiveness/templates/06-component-variants.html.tmpl +0 -557
  34. package/plugins/html-effectiveness/templates/07-prototype-animation.html.tmpl +0 -417
  35. package/plugins/html-effectiveness/templates/08-prototype-interaction.html.tmpl +0 -363
  36. package/plugins/html-effectiveness/templates/09-slide-deck.html.tmpl +0 -535
  37. package/plugins/html-effectiveness/templates/10-svg-illustrations.html.tmpl +0 -444
  38. package/plugins/html-effectiveness/templates/11-status-report.html.tmpl +0 -152
  39. package/plugins/html-effectiveness/templates/12-incident-report.html.tmpl +0 -547
  40. package/plugins/html-effectiveness/templates/13-flowchart-diagram.html.tmpl +0 -361
  41. package/plugins/html-effectiveness/templates/14-research-feature-explainer.html.tmpl +0 -348
  42. package/plugins/html-effectiveness/templates/15-research-concept-explainer.html.tmpl +0 -334
  43. package/plugins/html-effectiveness/templates/16-implementation-plan.html.tmpl +0 -657
  44. package/plugins/html-effectiveness/templates/17-pr-writeup.html.tmpl +0 -553
  45. package/plugins/html-effectiveness/templates/18-editor-triage-board.html.tmpl +0 -541
  46. package/plugins/html-effectiveness/templates/19-editor-feature-flags.html.tmpl +0 -627
  47. package/plugins/html-effectiveness/templates/20-editor-prompt-tuner.html.tmpl +0 -687
  48. package/plugins/html-effectiveness/templates/manifest.json +0 -441
  49. package/plugins/team-share/.claude-plugin/plugin.json +0 -18
  50. package/plugins/team-share/README.md +0 -46
  51. package/plugins/team-share/agents/team-share.md +0 -53
  52. package/plugins/team-share/commands/team-share.md +0 -4
  53. package/plugins/team-share/skills/claude-config/SKILL.md +0 -143
  54. package/plugins/team-share/skills/codegraph-setup/SKILL.md +0 -81
  55. package/plugins/team-share/skills/understand-setup/SKILL.md +0 -115
  56. package/plugins/tech-graph/.claude-plugin/plugin.json +0 -17
  57. package/plugins/tech-graph/README.md +0 -66
  58. package/plugins/tech-graph/agents/tech-graph.md +0 -97
  59. package/plugins/tech-graph/commands/tech-graph.md +0 -13
  60. package/plugins/tech-graph/scripts/check-deps.sh +0 -55
  61. package/plugins/tech-graph/skills/tech-graph/LICENSE +0 -21
  62. package/plugins/tech-graph/skills/tech-graph/README.md +0 -555
  63. package/plugins/tech-graph/skills/tech-graph/README.zh.md +0 -514
  64. package/plugins/tech-graph/skills/tech-graph/SKILL.md +0 -263
  65. package/plugins/tech-graph/skills/tech-graph/agents/openai.yaml +0 -4
  66. package/plugins/tech-graph/skills/tech-graph/assets/samples/sample-style1-flat.png +0 -0
  67. package/plugins/tech-graph/skills/tech-graph/assets/samples/sample-style2-dark.png +0 -0
  68. package/plugins/tech-graph/skills/tech-graph/assets/samples/sample-style3-blueprint.png +0 -0
  69. package/plugins/tech-graph/skills/tech-graph/assets/samples/sample-style4-notion.png +0 -0
  70. package/plugins/tech-graph/skills/tech-graph/assets/samples/sample-style5-glass.png +0 -0
  71. package/plugins/tech-graph/skills/tech-graph/assets/samples/sample-style6-claude.png +0 -0
  72. package/plugins/tech-graph/skills/tech-graph/assets/samples/sample-style7-openai.png +0 -0
  73. package/plugins/tech-graph/skills/tech-graph/fixtures/agent-memory-types-style4.json +0 -181
  74. package/plugins/tech-graph/skills/tech-graph/fixtures/api-flow-style7.json +0 -40
  75. package/plugins/tech-graph/skills/tech-graph/fixtures/mem0-style1.json +0 -297
  76. package/plugins/tech-graph/skills/tech-graph/fixtures/microservices-style3.json +0 -64
  77. package/plugins/tech-graph/skills/tech-graph/fixtures/multi-agent-style5.json +0 -45
  78. package/plugins/tech-graph/skills/tech-graph/fixtures/system-architecture-style6.json +0 -48
  79. package/plugins/tech-graph/skills/tech-graph/fixtures/tool-call-style2.json +0 -182
  80. package/plugins/tech-graph/skills/tech-graph/package.json +0 -42
  81. package/plugins/tech-graph/skills/tech-graph/references/diagram-types.md +0 -171
  82. package/plugins/tech-graph/skills/tech-graph/references/icons.md +0 -406
  83. package/plugins/tech-graph/skills/tech-graph/references/style-1-flat-icon.md +0 -108
  84. package/plugins/tech-graph/skills/tech-graph/references/style-2-dark-terminal.md +0 -107
  85. package/plugins/tech-graph/skills/tech-graph/references/style-3-blueprint.md +0 -113
  86. package/plugins/tech-graph/skills/tech-graph/references/style-4-notion-clean.md +0 -95
  87. package/plugins/tech-graph/skills/tech-graph/references/style-5-glassmorphism.md +0 -125
  88. package/plugins/tech-graph/skills/tech-graph/references/style-6-claude-official.md +0 -209
  89. package/plugins/tech-graph/skills/tech-graph/references/style-7-openai.md +0 -215
  90. package/plugins/tech-graph/skills/tech-graph/references/style-diagram-matrix.md +0 -135
  91. package/plugins/tech-graph/skills/tech-graph/references/svg-layout-best-practices.md +0 -100
  92. package/plugins/tech-graph/skills/tech-graph/references/svg-to-png.md +0 -115
  93. package/plugins/tech-graph/skills/tech-graph/scripts/README.md +0 -269
  94. package/plugins/tech-graph/skills/tech-graph/scripts/generate-diagram.sh +0 -181
  95. package/plugins/tech-graph/skills/tech-graph/scripts/generate-from-template.py +0 -1587
  96. package/plugins/tech-graph/skills/tech-graph/scripts/test-all-styles.sh +0 -143
  97. package/plugins/tech-graph/skills/tech-graph/scripts/validate-svg.sh +0 -306
  98. package/plugins/tech-graph/skills/tech-graph/templates/agent-architecture.svg +0 -28
  99. package/plugins/tech-graph/skills/tech-graph/templates/architecture.svg +0 -23
  100. package/plugins/tech-graph/skills/tech-graph/templates/comparison-matrix.svg +0 -14
  101. package/plugins/tech-graph/skills/tech-graph/templates/data-flow.svg +0 -28
  102. package/plugins/tech-graph/skills/tech-graph/templates/er-diagram.svg +0 -21
  103. package/plugins/tech-graph/skills/tech-graph/templates/flowchart.svg +0 -21
  104. package/plugins/tech-graph/skills/tech-graph/templates/sequence.svg +0 -20
  105. package/plugins/tech-graph/skills/tech-graph/templates/state-machine.svg +0 -20
  106. package/plugins/tech-graph/skills/tech-graph/templates/timeline.svg +0 -19
  107. package/plugins/tech-graph/skills/tech-graph/templates/use-case.svg +0 -21
@@ -1,348 +0,0 @@
1
- <!--
2
- template: 14-research-feature-explainer
3
- upstream: 14-research-feature-explainer.html
4
- slots:
5
- title: string # replaces first <h1> innerHTML
6
- -->
7
- <div class="page">
8
-
9
- <!-- โ”€โ”€ side nav โ”€โ”€ -->
10
- <nav>
11
- <div class="label">On this page</div>
12
- <a href="#tldr">TL;DR</a>
13
- <a href="#path">Request path</a>
14
- <a href="#path" class="l2">1. Identify</a>
15
- <a href="#path" class="l2">2. Bucket lookup</a>
16
- <a href="#path" class="l2">3. Consume</a>
17
- <a href="#path" class="l2">4. Reject</a>
18
- <a href="#config">Configuring a route</a>
19
- <a href="#gotchas">Gotchas</a>
20
- <a href="#faq">FAQ</a>
21
- <div class="files">
22
- <div class="label">Files read</div>
23
- <code>middleware/ratelimit.ts</code>
24
- <code>lib/tokenBucket.ts</code>
25
- <code>config/limits.yaml</code>
26
- <code>routes/*.ts</code>
27
- </div>
28
- </nav>
29
-
30
- <!-- โ”€โ”€ main โ”€โ”€ -->
31
- <main>
32
- <header>
33
- <div class="eyebrow">Research &amp; Learning ยท feature summary</div>
34
- <h1>{{title}}</h1>
35
- <div class="tldr" id="tldr">
36
- <b>TL;DR</b> โ€” Every request passes through <code>rateLimit()</code> middleware, which resolves the
37
- caller to a <em>bucket key</em>, fetches a token-bucket from Redis, and either consumes one token or
38
- returns <code>429</code>. Limits are declared per-route in <code>config/limits.yaml</code>; routes
39
- without an entry inherit the <code>default</code> tier (100 req/min per API key).
40
- </div>
41
- </header>
42
-
43
- <h2 id="path">The request path, step by step</h2>
44
- <p>Expand each step to see what runs and where it lives. The whole path is ~40 lines and adds about
45
- 0.4&nbsp;ms p50 to every request.</p>
46
-
47
- <details open>
48
- <summary>1 ยท Identify the caller <span class="where">middleware/ratelimit.ts:21</span></summary>
49
- <div class="body">
50
- <p>The middleware first reduces the request to a <code>bucketKey</code>: API key if an
51
- <code>Authorization</code> header is present, otherwise the client IP (via the
52
- <code>x-forwarded-for</code> chain, trusting only our own LB). Anonymous IP traffic gets a much
53
- lower default tier.</p>
54
- </div>
55
- </details>
56
-
57
- <details>
58
- <summary>2 ยท Look up the bucket <span class="where">lib/tokenBucket.ts:9</span></summary>
59
- <div class="body">
60
- <p>The route name plus bucket key map to a Redis hash (<code>rl:{route}:{key}</code>) holding
61
- <code>tokens</code> and <code>updatedAt</code>. If the key is missing it's created lazily at full
62
- capacity โ€” there's no warm-up.</p>
63
- </div>
64
- </details>
65
-
66
- <details>
67
- <summary>3 ยท Refill and consume <span class="where">lib/tokenBucket.ts:31</span></summary>
68
- <div class="body">
69
- <p>Refill is computed from elapsed time (<code>rate ร— ฮ”t</code>, capped at <code>burst</code>), then
70
- one token is subtracted. The whole read-modify-write runs as a single Lua script so concurrent
71
- requests can't double-spend.</p>
72
- </div>
73
- </details>
74
-
75
- <details>
76
- <summary>4 ยท Reject when empty <span class="where">middleware/ratelimit.ts:48</span></summary>
77
- <div class="body">
78
- <p>If the script returns <code>tokens &lt; 0</code> the middleware short-circuits with
79
- <code>429 Too Many Requests</code> and sets <code>Retry-After</code> to the seconds until one token
80
- refills. Successful responses always carry <code>X-RateLimit-Remaining</code>.</p>
81
- </div>
82
- </details>
83
-
84
- <h2 id="config">Configuring a limit on your route</h2>
85
- <p>You don't touch the middleware. Add an entry to <code>config/limits.yaml</code> keyed by route name,
86
- and (optionally) tag the route so the middleware can find it.</p>
87
-
88
- <div class="tabs" data-tabs>
89
- <div class="tabbar">
90
- <button class="on" data-t="0">limits.yaml</button>
91
- <button data-t="1">route.ts</button>
92
- <button data-t="2">client response</button>
93
- </div>
94
- <pre class="on"><span class="cm"># config/limits.yaml</span>
95
- default:
96
- rate: 100/min
97
- burst: 120
98
-
99
- <span class="hl">search.query</span>:
100
- rate: 20/min
101
- burst: 40
102
- key: api_key <span class="cm"># or: ip</span></pre>
103
- <pre><span class="cm">// routes/search.ts</span>
104
- router.post(
105
- "/search",
106
- <span class="hl">rateLimit("search.query")</span>,
107
- handler,
108
- );</pre>
109
- <pre>HTTP/1.1 429 Too Many Requests
110
- Retry-After: 17
111
- X-RateLimit-Limit: 20
112
- X-RateLimit-Remaining: 0
113
-
114
- { "error": "rate_limited", "retry_after": 17 }</pre>
115
- </div>
116
-
117
- <div class="callout">
118
- <span class="ico">โ˜…</span>
119
- <div>If you only need the default tier, you don't need a YAML entry at all โ€” just wrap the handler in
120
- <code>rateLimit()</code> with no argument. The route name is inferred from the path.</div>
121
- </div>
122
-
123
- <h2 id="gotchas">Gotchas worth knowing</h2>
124
- <ul style="padding-left:20px; max-width:680px;">
125
- <li style="margin-bottom:8px;"><b>Limits are per-process in dev.</b> The Redis client falls back to an
126
- in-memory map when <code>REDIS_URL</code> is unset, so local testing won't reflect real cluster
127
- behaviour.</li>
128
- <li style="margin-bottom:8px;"><b>Burst โ‰  rate.</b> <code>burst</code> is the bucket capacity; a caller
129
- idle for a minute can fire <code>burst</code> requests instantly even if <code>rate</code> is low.</li>
130
- <li><b>Streaming responses count once.</b> The token is consumed at request start; a 30-second SSE
131
- stream still costs one token.</li>
132
- </ul>
133
-
134
- <h2 id="faq">FAQ</h2>
135
- <dl class="faq">
136
- <dt>How do I exempt internal traffic?</dt>
137
- <dd>Set <code>x-acme-internal: 1</code> from the caller; the middleware checks it against the
138
- mTLS peer name and skips the bucket entirely.</dd>
139
-
140
- <dt>Where do I see who's getting limited?</dt>
141
- <dd>Every <code>429</code> emits a <code>ratelimit.rejected</code> metric tagged with route and key
142
- type. There's a Grafana panel under <em>API โ†’ Health</em>.</dd>
143
-
144
- <dt>Can a single user have a higher limit?</dt>
145
- <dd>Yes โ€” add their API key under <code>overrides:</code> in the YAML. Overrides are reloaded without
146
- a deploy.</dd>
147
- </dl>
148
- </main>
149
-
150
- </div>
151
-
152
- <style>
153
- * { box-sizing: border-box; margin: 0; padding: 0; }
154
- .page {
155
- max-width: 1100px;
156
- margin: 0 auto;
157
- display: grid;
158
- grid-template-columns: 200px minmax(0, 1fr);
159
- gap: 48px;
160
- }
161
- @media (max-width: 920px) { .page { grid-template-columns: 1fr; } nav { display: none; } }
162
- /* โ”€โ”€ nav โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
163
- nav {
164
- position: sticky;
165
- top: 32px;
166
- align-self: start;
167
- font-size: 13px;
168
- }
169
- nav .label {
170
- font-family: var(--mono);
171
- font-size: 10px;
172
- letter-spacing: 0.1em;
173
- text-transform: uppercase;
174
- color: var(--gray-500);
175
- margin-bottom: 12px;
176
- }
177
- nav a {
178
- display: block;
179
- padding: 5px 0 5px 12px;
180
- border-left: 2px solid var(--gray-300);
181
- color: var(--gray-700);
182
- text-decoration: none;
183
- }
184
- nav a:hover { color: var(--slate); border-color: var(--slate); }
185
- nav a.l2 { padding-left: 24px; font-size: 12.5px; color: var(--gray-500); }
186
- nav .files {
187
- margin-top: 28px;
188
- border-top: 1px solid var(--gray-300);
189
- padding-top: 16px;
190
- }
191
- nav .files code {
192
- display: block;
193
- font-family: var(--mono);
194
- font-size: 11px;
195
- color: var(--gray-500);
196
- padding: 3px 0;
197
- }
198
- /* โ”€โ”€ header โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
199
- header { margin-bottom: 12px; }
200
- .eyebrow {
201
- font-family: var(--mono);
202
- font-size: 11px;
203
- letter-spacing: 0.08em;
204
- text-transform: uppercase;
205
- color: var(--gray-500);
206
- margin-bottom: 10px;
207
- }
208
- h1 {
209
- font-family: var(--serif);
210
- font-weight: 500;
211
- font-size: 32px;
212
- color: var(--slate);
213
- letter-spacing: -0.01em;
214
- margin-bottom: 14px;
215
- }
216
- .tldr {
217
- border: 1.5px solid var(--gray-300);
218
- border-left: 3px solid var(--clay);
219
- border-radius: 10px;
220
- background: #fff;
221
- padding: 16px 18px;
222
- margin-bottom: 8px;
223
- }
224
- .tldr b { color: var(--slate); }
225
- h2 {
226
- font-family: var(--serif);
227
- font-weight: 500;
228
- font-size: 22px;
229
- color: var(--slate);
230
- margin: 40px 0 14px;
231
- scroll-margin-top: 24px;
232
- }
233
- p { margin-bottom: 12px; max-width: 680px; }
234
- code { font-family: var(--mono); font-size: 13px; }
235
- /* โ”€โ”€ collapsible โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
236
- details {
237
- border: 1.5px solid var(--gray-300);
238
- border-radius: 10px;
239
- background: #fff;
240
- margin: 14px 0;
241
- overflow: hidden;
242
- }
243
- summary {
244
- list-style: none;
245
- cursor: pointer;
246
- padding: 14px 16px;
247
- font-family: var(--serif);
248
- font-size: 16px;
249
- color: var(--slate);
250
- display: flex;
251
- align-items: baseline;
252
- gap: 10px;
253
- }
254
- summary::-webkit-details-marker { display: none; }
255
- summary::before {
256
- content: "โ–ธ";
257
- color: var(--clay);
258
- font-family: var(--sans);
259
- font-size: 12px;
260
- transition: transform 120ms;
261
- }
262
- details[open] summary::before { transform: rotate(90deg); }
263
- summary .where {
264
- font-family: var(--mono);
265
- font-size: 11px;
266
- color: var(--gray-500);
267
- margin-left: auto;
268
- }
269
- details .body { padding: 0 16px 16px; }
270
- details .body p { font-size: 14px; }
271
- /* โ”€โ”€ tabs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
272
- .tabs {
273
- border: 1.5px solid var(--gray-300);
274
- border-radius: 10px;
275
- background: #fff;
276
- margin: 16px 0 8px;
277
- overflow: hidden;
278
- }
279
- .tabbar {
280
- display: flex;
281
- border-bottom: 1px solid var(--gray-300);
282
- background: var(--gray-150);
283
- }
284
- .tabbar button {
285
- appearance: none;
286
- border: none;
287
- background: none;
288
- font-family: var(--mono);
289
- font-size: 12px;
290
- color: var(--gray-500);
291
- padding: 10px 16px;
292
- cursor: pointer;
293
- border-right: 1px solid var(--gray-300);
294
- }
295
- .tabbar button.on {
296
- background: #fff;
297
- color: var(--slate);
298
- border-bottom: 2px solid var(--clay);
299
- margin-bottom: -1px;
300
- }
301
- .tabs pre {
302
- display: none;
303
- margin: 0;
304
- padding: 16px 18px;
305
- font-family: var(--mono);
306
- font-size: 12.5px;
307
- line-height: 1.6;
308
- color: var(--slate);
309
- overflow-x: auto;
310
- }
311
- .tabs pre.on { display: block; }
312
- .hl { color: var(--clay); }
313
- .cm { color: var(--gray-500); }
314
- /* โ”€โ”€ callout โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
315
- .callout {
316
- display: flex;
317
- gap: 12px;
318
- border: 1.5px solid var(--oat);
319
- background: rgba(227,218,204,0.35);
320
- border-radius: 10px;
321
- padding: 14px 16px;
322
- margin: 18px 0;
323
- font-size: 14px;
324
- }
325
- .callout .ico { color: var(--clay); font-weight: 600; }
326
- /* โ”€โ”€ faq โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
327
- dl.faq { margin-top: 8px; }
328
- dl.faq dt {
329
- font-family: var(--serif);
330
- font-size: 16px;
331
- color: var(--slate);
332
- margin-top: 18px;
333
- }
334
- dl.faq dd { font-size: 14px; margin: 4px 0 0; max-width: 640px; }
335
- </style>
336
-
337
- <script>
338
- document.querySelectorAll("[data-tabs]").forEach(box => {
339
- const btns = box.querySelectorAll("button");
340
- const panes = box.querySelectorAll("pre");
341
- btns.forEach(b => b.addEventListener("click", () => {
342
- btns.forEach(x => x.classList.remove("on"));
343
- panes.forEach(x => x.classList.remove("on"));
344
- b.classList.add("on");
345
- panes[+b.dataset.t].classList.add("on");
346
- }));
347
- });
348
- </script>
@@ -1,334 +0,0 @@
1
- <!--
2
- template: 15-research-concept-explainer
3
- upstream: 15-research-concept-explainer.html
4
- slots:
5
- title: string # replaces first <h1> innerHTML
6
- -->
7
- <div class="page">
8
-
9
- <main>
10
- <div class="eyebrow">Research &amp; Learning ยท concept explainer</div>
11
- <h1>{{title}}</h1>
12
- <p class="lead">
13
- You have <em>K</em> keys spread across <em>N</em> cache servers. A server dies, or you add one. How many
14
- keys have to move? With naive <code>hash(key) mod N</code> the answer is "almost all of them." Consistent
15
- hashing gets it down to roughly <code>K&nbsp;/&nbsp;N</code>. Here's why.
16
- </p>
17
-
18
- <h2>The trick: hash onto a circle, not a line</h2>
19
- <p>
20
- Map both <span class="term" data-term="node">nodes</span> and keys onto the same
21
- <span class="term" data-term="ring">ring</span> (the hash output space, wrapped around). A key belongs to
22
- the first node found by walking clockwise from the key's position. When a node leaves, only the keys in
23
- its <span class="term" data-term="arc">arc</span> reassign โ€” to the next node round โ€” and everything else
24
- stays put.
25
- </p>
26
-
27
- <div class="demo">
28
- <div class="demo-grid">
29
- <svg class="ring" id="ring" viewBox="0 0 260 260"></svg>
30
- <div class="controls">
31
- <div class="row">
32
- <label>nodes</label>
33
- <input id="nSlider" type="range" min="2" max="8" value="4">
34
- <span class="val" id="nVal">4</span>
35
- </div>
36
- <div class="row">
37
- <label>keys</label>
38
- <input id="kSlider" type="range" min="10" max="60" value="32" step="2">
39
- <span class="val" id="kVal">32</span>
40
- </div>
41
- <div>
42
- <button id="rm">remove a node</button>
43
- <button id="add">add a node</button>
44
- <button id="reset">reset</button>
45
- </div>
46
- <div class="readout" id="readout">
47
- <b>4</b> nodes ยท <b>32</b> keys ยท <span class="moved">โ€”</span> moved on last change
48
- </div>
49
- </div>
50
- </div>
51
- </div>
52
- <p style="font-size:13px; color:var(--gray-500); max-width:640px;">
53
- Colored arcs show ownership. Removing a node hands its arc to its clockwise neighbor; every dot outside
54
- that arc keeps its color. That's the whole idea.
55
- </p>
56
-
57
- <h2>Versus <code>mod N</code></h2>
58
- <table>
59
- <thead><tr><th></th><th>hash mod N</th><th>consistent hashing</th></tr></thead>
60
- <tbody>
61
- <tr><td>Keys moved when Nโ†’N+1</td><td class="bad">~ (Nโˆ’1)/N of all keys</td><td class="good">~ 1/(N+1)</td></tr>
62
- <tr><td>Hot-spot risk</td><td class="good">even by construction</td><td>uneven โ€” fix with <span class="term" data-term="vnode">virtual nodes</span></td></tr>
63
- <tr><td>Lookup cost</td><td class="good">O(1)</td><td>O(log N) (binary search on ring)</td></tr>
64
- <tr><td>Used by</td><td>array sharding, simple LB</td><td>Dynamo, Cassandra, Memcached clients, Envoy</td></tr>
65
- </tbody>
66
- </table>
67
-
68
- <h2>Where you'll meet it</h2>
69
- <p>
70
- Any time you're spreading state across a pool that changes size: cache fleets, partitioned queues, object
71
- storage, request routing with sticky sessions. The
72
- <span class="term" data-term="vnode">virtual-node</span> variant (each physical node owns many small arcs
73
- instead of one big one) is what production systems actually run, because it smooths out load and makes
74
- rebalancing even gentler.
75
- </p>
76
- </main>
77
-
78
- <!-- glossary -->
79
- <aside>
80
- <div class="label">Glossary</div>
81
- <dl id="gloss">
82
- <dt data-g="ring">Ring</dt>
83
- <dd>The hash function's output range, treated as a circle so the value after <code>max</code> is <code>0</code>.</dd>
84
- <dt data-g="node">Node</dt>
85
- <dd>A server placed on the ring at <code>hash(node_id)</code>. Owns every key between it and its anticlockwise neighbor.</dd>
86
- <dt data-g="arc">Arc</dt>
87
- <dd>The stretch of ring a node owns. Removing a node merges its arc into the next node's.</dd>
88
- <dt data-g="vnode">Virtual node</dt>
89
- <dd>Placing each physical node at many ring positions so arcs are small and evenly sized.</dd>
90
- <dt data-g="successor">Successor</dt>
91
- <dd>The first node clockwise from a given point โ€” the owner of any key landing there.</dd>
92
- </dl>
93
- </aside>
94
-
95
- </div>
96
-
97
- <style>
98
- * { box-sizing: border-box; margin: 0; padding: 0; }
99
- .page {
100
- max-width: 1100px;
101
- margin: 0 auto;
102
- display: grid;
103
- grid-template-columns: minmax(0, 1fr) 240px;
104
- gap: 48px;
105
- }
106
- @media (max-width: 960px) { .page { grid-template-columns: 1fr; } aside { order: 2; position: static; } }
107
- /* โ”€โ”€ header โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
108
- .eyebrow {
109
- font-family: var(--mono);
110
- font-size: 11px;
111
- letter-spacing: 0.08em;
112
- text-transform: uppercase;
113
- color: var(--gray-500);
114
- margin-bottom: 10px;
115
- }
116
- h1 {
117
- font-family: var(--serif);
118
- font-weight: 500;
119
- font-size: 33px;
120
- color: var(--slate);
121
- letter-spacing: -0.01em;
122
- margin-bottom: 12px;
123
- }
124
- .lead { max-width: 640px; margin-bottom: 8px; }
125
- h2 {
126
- font-family: var(--serif);
127
- font-weight: 500;
128
- font-size: 22px;
129
- color: var(--slate);
130
- margin: 40px 0 12px;
131
- }
132
- p { margin-bottom: 12px; max-width: 680px; }
133
- code { font-family: var(--mono); font-size: 13px; }
134
- .term {
135
- border-bottom: 1.5px dotted var(--clay);
136
- cursor: help;
137
- color: var(--slate);
138
- }
139
- /* โ”€โ”€ demo panel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
140
- .demo {
141
- border: 1.5px solid var(--gray-300);
142
- border-radius: 14px;
143
- background: #fff;
144
- padding: 24px;
145
- margin: 12px 0 8px;
146
- }
147
- .demo-grid {
148
- display: grid;
149
- grid-template-columns: 320px 1fr;
150
- gap: 28px;
151
- align-items: center;
152
- }
153
- @media (max-width: 760px) { .demo-grid { grid-template-columns: 1fr; } }
154
- svg.ring { display: block; width: 100%; max-width: 320px; }
155
- .ring .track { fill: none; stroke: var(--gray-300); stroke-width: 14; }
156
- .ring .arc { fill: none; stroke-width: 14; transition: stroke-dasharray 300ms ease; }
157
- .ring .node { stroke: #fff; stroke-width: 2; transition: cx 300ms, cy 300ms, opacity 200ms; }
158
- .ring .key { fill: var(--slate); transition: cx 300ms, cy 300ms; }
159
- .ring .lbl { font-family: var(--mono); font-size: 10px; fill: var(--gray-500); }
160
- .controls { font-size: 13px; }
161
- .controls .row { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
162
- .controls label { width: 64px; color: var(--gray-500); font-family: var(--mono); font-size: 11px; }
163
- .controls .val { font-family: var(--mono); font-size: 12px; color: var(--slate); width: 28px; }
164
- .controls input[type=range] { flex: 1; accent-color: var(--clay); }
165
- .controls button {
166
- appearance: none;
167
- border: 1.5px solid var(--gray-300);
168
- background: var(--gray-150);
169
- border-radius: 6px;
170
- font-family: var(--mono);
171
- font-size: 11px;
172
- padding: 6px 10px;
173
- cursor: pointer;
174
- margin-right: 6px;
175
- }
176
- .controls button:hover { background: var(--oat); }
177
- .readout {
178
- border-top: 1px solid var(--gray-300);
179
- margin-top: 16px;
180
- padding-top: 14px;
181
- font-size: 13px;
182
- }
183
- .readout b { color: var(--slate); }
184
- .moved { color: var(--clay); font-family: var(--mono); }
185
- /* โ”€โ”€ compare table โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
186
- table {
187
- border-collapse: collapse;
188
- width: 100%;
189
- max-width: 640px;
190
- font-size: 13.5px;
191
- margin: 12px 0;
192
- }
193
- th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--gray-300); }
194
- th { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--gray-500); font-weight: 500; }
195
- td.bad { color: #B04A3F; }
196
- td.good { color: var(--olive); }
197
- /* โ”€โ”€ glossary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
198
- aside {
199
- position: sticky;
200
- top: 32px;
201
- align-self: start;
202
- border: 1.5px solid var(--gray-300);
203
- border-radius: 12px;
204
- background: #fff;
205
- padding: 18px 18px 8px;
206
- }
207
- aside .label {
208
- font-family: var(--mono);
209
- font-size: 10px;
210
- letter-spacing: 0.1em;
211
- text-transform: uppercase;
212
- color: var(--gray-500);
213
- margin-bottom: 12px;
214
- }
215
- aside dl dt {
216
- font-family: var(--serif);
217
- font-size: 15px;
218
- color: var(--slate);
219
- margin-top: 0;
220
- }
221
- aside dl dd {
222
- font-size: 12.5px;
223
- line-height: 1.5;
224
- color: var(--gray-700);
225
- margin: 2px 0 14px;
226
- }
227
- aside dl dt.hl, aside dl dt.hl + dd { background: rgba(217,119,87,0.10); margin-left: -8px; margin-right: -8px; padding-left: 8px; padding-right: 8px; border-radius: 4px; }
228
- </style>
229
-
230
- <script>
231
- /* โ”€โ”€ tiny deterministic hash so the demo is stable โ”€โ”€ */
232
- function h(s) {
233
- let x = 2166136261;
234
- for (let i = 0; i < s.length; i++) { x ^= s.charCodeAt(i); x = (x * 16777619) >>> 0; }
235
- return x / 4294967296;
236
- }
237
- const COLORS = ["#D97757", "#788C5D", "#6A8CAF", "#C2A83E", "#B04A3F", "#87867F", "#3D6E6E", "#A67C52"];
238
- const CX = 130, CY = 130, R = 100, RKEY = 78;
239
- const TAU = Math.PI * 2;
240
-
241
- const ring = document.getElementById("ring");
242
- const nSlider = document.getElementById("nSlider");
243
- const kSlider = document.getElementById("kSlider");
244
- const nVal = document.getElementById("nVal");
245
- const kVal = document.getElementById("kVal");
246
- const readout = document.getElementById("readout");
247
-
248
- let nodes = [], keys = [], lastOwner = {};
249
-
250
- function pt(r, t) { return [CX + r * Math.sin(t * TAU), CY - r * Math.cos(t * TAU)]; }
251
-
252
- function ownerOf(t) {
253
- const sorted = [...nodes].sort((a, b) => a.t - b.t);
254
- for (const n of sorted) if (n.t >= t) return n;
255
- return sorted[0];
256
- }
257
-
258
- function buildNodes(n) {
259
- nodes = [];
260
- for (let i = 0; i < n; i++) nodes.push({ id: "n" + i, t: h("node-" + i), color: COLORS[i % COLORS.length] });
261
- }
262
- function buildKeys(k) {
263
- keys = [];
264
- for (let i = 0; i < k; i++) keys.push({ id: "k" + i, t: h("key-" + i) });
265
- }
266
-
267
- function arcPath(t0, t1) {
268
- const [x0, y0] = pt(R, t0), [x1, y1] = pt(R, t1);
269
- let d = t1 - t0; if (d <= 0) d += 1;
270
- const large = d > 0.5 ? 1 : 0;
271
- return `M ${x0} ${y0} A ${R} ${R} 0 ${large} 1 ${x1} ${y1}`;
272
- }
273
-
274
- function render(moved) {
275
- const sorted = [...nodes].sort((a, b) => a.t - b.t);
276
- let svg = `<circle class="track" cx="${CX}" cy="${CY}" r="${R}"/>`;
277
-
278
- for (let i = 0; i < sorted.length; i++) {
279
- const cur = sorted[i];
280
- const prev = sorted[(i - 1 + sorted.length) % sorted.length];
281
- svg += `<path class="arc" stroke="${cur.color}" d="${arcPath(prev.t, cur.t)}"/>`;
282
- }
283
- for (const k of keys) {
284
- const o = ownerOf(k.t);
285
- const [x, y] = pt(RKEY, k.t);
286
- svg += `<circle class="key" r="3.5" cx="${x}" cy="${y}" fill="${o.color}"/>`;
287
- }
288
- for (const n of sorted) {
289
- const [x, y] = pt(R, n.t);
290
- svg += `<circle class="node" r="9" cx="${x}" cy="${y}" fill="${n.color}"/>`;
291
- }
292
- svg += `<text class="lbl" x="${CX}" y="18" text-anchor="middle">0</text>`;
293
- ring.innerHTML = svg;
294
-
295
- readout.innerHTML =
296
- `<b>${nodes.length}</b> nodes ยท <b>${keys.length}</b> keys ยท ` +
297
- `<span class="moved">${moved == null ? "โ€”" : moved + " (" + Math.round(moved / keys.length * 100) + "%)"}</span> moved on last change`;
298
- }
299
-
300
- function diffAndRender() {
301
- let moved = 0;
302
- for (const k of keys) {
303
- const o = ownerOf(k.t).id;
304
- if (lastOwner[k.id] && lastOwner[k.id] !== o) moved++;
305
- lastOwner[k.id] = o;
306
- }
307
- render(moved);
308
- }
309
-
310
- function reset() {
311
- buildNodes(+nSlider.value);
312
- buildKeys(+kSlider.value);
313
- lastOwner = {};
314
- for (const k of keys) lastOwner[k.id] = ownerOf(k.t).id;
315
- nVal.textContent = nSlider.value;
316
- kVal.textContent = kSlider.value;
317
- render(null);
318
- }
319
-
320
- nSlider.oninput = () => { nVal.textContent = nSlider.value; buildNodes(+nSlider.value); diffAndRender(); };
321
- kSlider.oninput = () => { kVal.textContent = kSlider.value; buildKeys(+kSlider.value); lastOwner = {}; for (const k of keys) lastOwner[k.id] = ownerOf(k.t).id; render(null); };
322
- document.getElementById("rm").onclick = () => { if (nodes.length > 2) { nodes.splice(Math.floor(Math.random() * nodes.length), 1); diffAndRender(); } };
323
- document.getElementById("add").onclick = () => { if (nodes.length < 12) { const i = nodes.length; nodes.push({ id: "n" + Date.now(), t: Math.random(), color: COLORS[i % COLORS.length] }); diffAndRender(); } };
324
- document.getElementById("reset").onclick = reset;
325
-
326
- /* glossary highlight */
327
- document.querySelectorAll(".term").forEach(el => {
328
- const g = el.dataset.term;
329
- el.addEventListener("mouseenter", () => document.querySelector(`dt[data-g="${g}"]`)?.classList.add("hl"));
330
- el.addEventListener("mouseleave", () => document.querySelector(`dt[data-g="${g}"]`)?.classList.remove("hl"));
331
- });
332
-
333
- reset();
334
- </script>