claude-dev-env 1.83.0 → 1.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/_shared/pr-loop/audit-contract.md +24 -12
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +8 -0
  6. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
  7. package/_shared/pr-loop/scripts/terminology_sweep.py +85 -39
  8. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
  9. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
  12. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
  13. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +94 -4
  14. package/agents/clean-coder.md +3 -0
  15. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  16. package/bin/install.mjs +16 -8
  17. package/bin/install.test.mjs +17 -6
  18. package/commands/CLAUDE.md +0 -1
  19. package/docs/CODE_RULES.md +1 -1
  20. package/hooks/blocking/CLAUDE.md +0 -4
  21. package/hooks/blocking/claude_md_orphan_file_blocker.py +1 -1
  22. package/hooks/blocking/code_rules_constants_config.py +5 -0
  23. package/hooks/blocking/code_rules_docstrings.py +133 -19
  24. package/hooks/blocking/code_rules_enforcer.py +4 -0
  25. package/hooks/blocking/code_rules_imports_logging.py +10 -6
  26. package/hooks/blocking/code_rules_magic_values.py +5 -0
  27. package/hooks/blocking/code_rules_naming_collection.py +15 -8
  28. package/hooks/blocking/code_rules_paired_test.py +3 -2
  29. package/hooks/blocking/code_rules_shared.py +34 -0
  30. package/hooks/blocking/code_rules_string_magic.py +1 -1
  31. package/hooks/blocking/code_rules_unused_imports.py +2 -2
  32. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
  33. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +29 -0
  34. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
  35. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
  36. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +24 -0
  37. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
  38. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
  39. package/hooks/blocking/test_code_rules_magic_values.py +54 -0
  40. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
  41. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
  42. package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
  43. package/hooks/hooks_constants/CLAUDE.md +0 -3
  44. package/hooks/hooks_constants/blocking_check_limits.py +4 -0
  45. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  46. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
  47. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
  48. package/hooks/validation/post_tool_use_dispatcher.py +1 -1
  49. package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
  50. package/hooks/workflow/CLAUDE.md +2 -8
  51. package/package.json +1 -1
  52. package/rules/CLAUDE.md +1 -1
  53. package/rules/docstring-prose-matches-implementation.md +1 -1
  54. package/rules/env-var-table-code-drift.md +1 -1
  55. package/rules/package-inventory-stale-entry.md +1 -1
  56. package/rules/paired-test-coverage.md +1 -1
  57. package/rules/plain-illustrative-docstrings.md +35 -1
  58. package/skills/CLAUDE.md +6 -1
  59. package/skills/_shared/pr-loop/CLAUDE.md +1 -0
  60. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  61. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  62. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +45 -0
  63. package/skills/_shared/pr-loop/scripts/test_write_handoff.py +201 -0
  64. package/skills/_shared/pr-loop/scripts/write_handoff.py +309 -0
  65. package/skills/autoconverge/SKILL.md +82 -58
  66. package/skills/autoconverge/reference/closing-report.md +6 -6
  67. package/skills/autoconverge/reference/convergence.md +17 -15
  68. package/skills/autoconverge/reference/gotchas.md +6 -3
  69. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
  70. package/skills/autoconverge/workflow/converge.contract.test.mjs +87 -13
  71. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
  72. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
  73. package/skills/autoconverge/workflow/converge.mjs +71 -74
  74. package/skills/autoconverge/workflow/render_report.py +7 -11
  75. package/skills/bugteam/CLAUDE.md +1 -1
  76. package/skills/bugteam/PROMPTS.md +7 -6
  77. package/skills/bugteam/SKILL.md +25 -72
  78. package/skills/bugteam/reference/CLAUDE.md +1 -3
  79. package/skills/bugteam/reference/README.md +1 -1
  80. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  81. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
  82. package/skills/bugteam/reference/team-setup.md +8 -7
  83. package/skills/bugteam/scripts/CLAUDE.md +0 -6
  84. package/skills/bugteam/scripts/README.md +0 -4
  85. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
  86. package/skills/code/SKILL.md +2 -0
  87. package/skills/copilot-review/CLAUDE.md +1 -1
  88. package/skills/copilot-review/SKILL.md +25 -23
  89. package/skills/findbugs/CLAUDE.md +2 -2
  90. package/skills/findbugs/SKILL.md +22 -83
  91. package/skills/fixbugs/SKILL.md +2 -4
  92. package/skills/log-audit/CLAUDE.md +20 -0
  93. package/skills/log-audit/SKILL.md +68 -0
  94. package/skills/log-audit/reference/CLAUDE.md +9 -0
  95. package/skills/log-audit/reference/charter.md +52 -0
  96. package/skills/log-audit/scripts/CLAUDE.md +27 -0
  97. package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
  98. package/skills/log-audit/scripts/collect_log_window.py +199 -0
  99. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
  100. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
  101. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
  102. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
  103. package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
  104. package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
  105. package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
  106. package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
  107. package/skills/monitor-open-prs/SKILL.md +2 -2
  108. package/skills/post-audit-findings/SKILL.md +84 -0
  109. package/skills/pr-converge/CLAUDE.md +2 -0
  110. package/skills/pr-converge/SKILL.md +99 -60
  111. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  112. package/skills/pr-converge/reference/convergence-gates.md +16 -19
  113. package/skills/pr-converge/reference/examples.md +5 -5
  114. package/skills/pr-converge/reference/fix-protocol.md +16 -43
  115. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
  116. package/skills/pr-converge/reference/per-tick.md +24 -45
  117. package/skills/pr-converge/reference/state-schema.md +27 -0
  118. package/skills/pr-converge/scripts/README.md +3 -5
  119. package/skills/pr-fix-protocol/SKILL.md +70 -0
  120. package/skills/pr-loop-lifecycle/SKILL.md +73 -0
  121. package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
  122. package/skills/pr-scope-resolve/SKILL.md +48 -0
  123. package/skills/qbug/CLAUDE.md +4 -4
  124. package/skills/qbug/SKILL.md +46 -144
  125. package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
  126. package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
  127. package/skills/reviewer-gates/SKILL.md +96 -0
  128. package/skills/session-log/CLAUDE.md +7 -7
  129. package/skills/session-log/SKILL.md +27 -44
  130. package/skills/test_markdown_link_integrity.py +103 -0
  131. package/commands/doc-gist.md +0 -16
  132. package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
  133. package/hooks/blocking/conftest.py +0 -30
  134. package/hooks/blocking/md_path_exemptions.py +0 -224
  135. package/hooks/blocking/md_to_html_blocker.py +0 -155
  136. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
  137. package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
  138. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
  139. package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
  140. package/hooks/hooks_constants/html_companion_constants.py +0 -20
  141. package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
  142. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
  143. package/hooks/workflow/doc_gist_auto_publish.py +0 -144
  144. package/hooks/workflow/md_to_html_companion.py +0 -358
  145. package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
  146. package/hooks/workflow/test_md_to_html_companion.py +0 -613
  147. package/skills/bugteam/reference/audit-contract.md +0 -163
  148. package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
  149. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
  150. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
  151. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
  152. package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
  153. package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
  154. package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
  155. package/skills/doc-gist/CLAUDE.md +0 -25
  156. package/skills/doc-gist/SKILL.md +0 -97
  157. package/skills/doc-gist/references/CLAUDE.md +0 -9
  158. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
  159. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
  160. package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
  161. package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
  162. package/skills/doc-gist/references/examples/05-design-system.html +0 -629
  163. package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
  164. package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
  165. package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
  166. package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
  167. package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
  168. package/skills/doc-gist/references/examples/11-status-report.html +0 -528
  169. package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
  170. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
  171. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
  172. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
  173. package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
  174. package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
  175. package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
  176. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
  177. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
  178. package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
  179. package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
  180. package/skills/doc-gist/references/examples/README.md +0 -5
  181. package/skills/doc-gist/scripts/CLAUDE.md +0 -27
  182. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
  183. package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
  184. package/skills/doc-gist/scripts/gist_upload.py +0 -177
  185. package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
  186. /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
@@ -1,368 +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">
6
- <title>Consistent hashing — an interactive explainer</title>
7
- <style>
8
- :root {
9
- --ivory: #FAF9F5;
10
- --slate: #141413;
11
- --clay: #D97757;
12
- --oat: #E3DACC;
13
- --olive: #788C5D;
14
- --sky: #6A8CAF;
15
- --gray-150:#F0EEE6;
16
- --gray-300:#D1CFC5;
17
- --gray-500:#87867F;
18
- --gray-700:#3D3D3A;
19
- --serif: ui-serif, Georgia, "Times New Roman", serif;
20
- --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
21
- --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
22
- }
23
- * { box-sizing: border-box; margin: 0; padding: 0; }
24
- body {
25
- background: var(--ivory);
26
- color: var(--gray-700);
27
- font-family: var(--sans);
28
- font-size: 15px;
29
- line-height: 1.65;
30
- -webkit-font-smoothing: antialiased;
31
- padding: 56px 24px 120px;
32
- }
33
- .page {
34
- max-width: 1100px;
35
- margin: 0 auto;
36
- display: grid;
37
- grid-template-columns: minmax(0, 1fr) 240px;
38
- gap: 48px;
39
- }
40
- @media (max-width: 960px) { .page { grid-template-columns: 1fr; } aside { order: 2; position: static; } }
41
-
42
- /* ── header ───────────────────────────── */
43
- .eyebrow {
44
- font-family: var(--mono);
45
- font-size: 11px;
46
- letter-spacing: 0.08em;
47
- text-transform: uppercase;
48
- color: var(--gray-500);
49
- margin-bottom: 10px;
50
- }
51
- h1 {
52
- font-family: var(--serif);
53
- font-weight: 500;
54
- font-size: 33px;
55
- color: var(--slate);
56
- letter-spacing: -0.01em;
57
- margin-bottom: 12px;
58
- }
59
- .lead { max-width: 640px; margin-bottom: 8px; }
60
- h2 {
61
- font-family: var(--serif);
62
- font-weight: 500;
63
- font-size: 22px;
64
- color: var(--slate);
65
- margin: 40px 0 12px;
66
- }
67
- p { margin-bottom: 12px; max-width: 680px; }
68
- code { font-family: var(--mono); font-size: 13px; }
69
- .term {
70
- border-bottom: 1.5px dotted var(--clay);
71
- cursor: help;
72
- color: var(--slate);
73
- }
74
-
75
- /* ── demo panel ───────────────────────── */
76
- .demo {
77
- border: 1.5px solid var(--gray-300);
78
- border-radius: 14px;
79
- background: #fff;
80
- padding: 24px;
81
- margin: 12px 0 8px;
82
- }
83
- .demo-grid {
84
- display: grid;
85
- grid-template-columns: 320px 1fr;
86
- gap: 28px;
87
- align-items: center;
88
- }
89
- @media (max-width: 760px) { .demo-grid { grid-template-columns: 1fr; } }
90
-
91
- svg.ring { display: block; width: 100%; max-width: 320px; }
92
- .ring .track { fill: none; stroke: var(--gray-300); stroke-width: 14; }
93
- .ring .arc { fill: none; stroke-width: 14; transition: stroke-dasharray 300ms ease; }
94
- .ring .node { stroke: #fff; stroke-width: 2; transition: cx 300ms, cy 300ms, opacity 200ms; }
95
- .ring .key { fill: var(--slate); transition: cx 300ms, cy 300ms; }
96
- .ring .lbl { font-family: var(--mono); font-size: 10px; fill: var(--gray-500); }
97
-
98
- .controls { font-size: 13px; }
99
- .controls .row { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
100
- .controls label { width: 64px; color: var(--gray-500); font-family: var(--mono); font-size: 11px; }
101
- .controls .val { font-family: var(--mono); font-size: 12px; color: var(--slate); width: 28px; }
102
- .controls input[type=range] { flex: 1; accent-color: var(--clay); }
103
- .controls button {
104
- appearance: none;
105
- border: 1.5px solid var(--gray-300);
106
- background: var(--gray-150);
107
- border-radius: 6px;
108
- font-family: var(--mono);
109
- font-size: 11px;
110
- padding: 6px 10px;
111
- cursor: pointer;
112
- margin-right: 6px;
113
- }
114
- .controls button:hover { background: var(--oat); }
115
-
116
- .readout {
117
- border-top: 1px solid var(--gray-300);
118
- margin-top: 16px;
119
- padding-top: 14px;
120
- font-size: 13px;
121
- }
122
- .readout b { color: var(--slate); }
123
- .moved { color: var(--clay); font-family: var(--mono); }
124
-
125
- /* ── compare table ────────────────────── */
126
- table {
127
- border-collapse: collapse;
128
- width: 100%;
129
- max-width: 640px;
130
- font-size: 13.5px;
131
- margin: 12px 0;
132
- }
133
- th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--gray-300); }
134
- th { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--gray-500); font-weight: 500; }
135
- td.bad { color: #B04A3F; }
136
- td.good { color: var(--olive); }
137
-
138
- /* ── glossary ─────────────────────────── */
139
- aside {
140
- position: sticky;
141
- top: 32px;
142
- align-self: start;
143
- border: 1.5px solid var(--gray-300);
144
- border-radius: 12px;
145
- background: #fff;
146
- padding: 18px 18px 8px;
147
- }
148
- aside .label {
149
- font-family: var(--mono);
150
- font-size: 10px;
151
- letter-spacing: 0.1em;
152
- text-transform: uppercase;
153
- color: var(--gray-500);
154
- margin-bottom: 12px;
155
- }
156
- aside dl dt {
157
- font-family: var(--serif);
158
- font-size: 15px;
159
- color: var(--slate);
160
- margin-top: 0;
161
- }
162
- aside dl dd {
163
- font-size: 12.5px;
164
- line-height: 1.5;
165
- color: var(--gray-700);
166
- margin: 2px 0 14px;
167
- }
168
- 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; }
169
- </style>
170
- </head>
171
- <body>
172
- <div class="page">
173
-
174
- <main>
175
- <div class="eyebrow">Research &amp; Learning · concept explainer</div>
176
- <h1>Consistent hashing, in one ring</h1>
177
- <p class="lead">
178
- You have <em>K</em> keys spread across <em>N</em> cache servers. A server dies, or you add one. How many
179
- keys have to move? With naive <code>hash(key) mod N</code> the answer is "almost all of them." Consistent
180
- hashing gets it down to roughly <code>K&nbsp;/&nbsp;N</code>. Here's why.
181
- </p>
182
-
183
- <h2>The trick: hash onto a circle, not a line</h2>
184
- <p>
185
- Map both <span class="term" data-term="node">nodes</span> and keys onto the same
186
- <span class="term" data-term="ring">ring</span> (the hash output space, wrapped around). A key belongs to
187
- the first node found by walking clockwise from the key's position. When a node leaves, only the keys in
188
- its <span class="term" data-term="arc">arc</span> reassign — to the next node round — and everything else
189
- stays put.
190
- </p>
191
-
192
- <div class="demo">
193
- <div class="demo-grid">
194
- <svg class="ring" id="ring" viewBox="0 0 260 260"></svg>
195
- <div class="controls">
196
- <div class="row">
197
- <label>nodes</label>
198
- <input id="nSlider" type="range" min="2" max="8" value="4">
199
- <span class="val" id="nVal">4</span>
200
- </div>
201
- <div class="row">
202
- <label>keys</label>
203
- <input id="kSlider" type="range" min="10" max="60" value="32" step="2">
204
- <span class="val" id="kVal">32</span>
205
- </div>
206
- <div>
207
- <button id="rm">remove a node</button>
208
- <button id="add">add a node</button>
209
- <button id="reset">reset</button>
210
- </div>
211
- <div class="readout" id="readout">
212
- <b>4</b> nodes · <b>32</b> keys · <span class="moved">—</span> moved on last change
213
- </div>
214
- </div>
215
- </div>
216
- </div>
217
- <p style="font-size:13px; color:var(--gray-500); max-width:640px;">
218
- Colored arcs show ownership. Removing a node hands its arc to its clockwise neighbor; every dot outside
219
- that arc keeps its color. That's the whole idea.
220
- </p>
221
-
222
- <h2>Versus <code>mod N</code></h2>
223
- <table>
224
- <thead><tr><th></th><th>hash mod N</th><th>consistent hashing</th></tr></thead>
225
- <tbody>
226
- <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>
227
- <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>
228
- <tr><td>Lookup cost</td><td class="good">O(1)</td><td>O(log N) (binary search on ring)</td></tr>
229
- <tr><td>Used by</td><td>array sharding, simple LB</td><td>Dynamo, Cassandra, Memcached clients, Envoy</td></tr>
230
- </tbody>
231
- </table>
232
-
233
- <h2>Where you'll meet it</h2>
234
- <p>
235
- Any time you're spreading state across a pool that changes size: cache fleets, partitioned queues, object
236
- storage, request routing with sticky sessions. The
237
- <span class="term" data-term="vnode">virtual-node</span> variant (each physical node owns many small arcs
238
- instead of one big one) is what production systems actually run, because it smooths out load and makes
239
- rebalancing even gentler.
240
- </p>
241
- </main>
242
-
243
- <!-- glossary -->
244
- <aside>
245
- <div class="label">Glossary</div>
246
- <dl id="gloss">
247
- <dt data-g="ring">Ring</dt>
248
- <dd>The hash function's output range, treated as a circle so the value after <code>max</code> is <code>0</code>.</dd>
249
- <dt data-g="node">Node</dt>
250
- <dd>A server placed on the ring at <code>hash(node_id)</code>. Owns every key between it and its anticlockwise neighbor.</dd>
251
- <dt data-g="arc">Arc</dt>
252
- <dd>The stretch of ring a node owns. Removing a node merges its arc into the next node's.</dd>
253
- <dt data-g="vnode">Virtual node</dt>
254
- <dd>Placing each physical node at many ring positions so arcs are small and evenly sized.</dd>
255
- <dt data-g="successor">Successor</dt>
256
- <dd>The first node clockwise from a given point — the owner of any key landing there.</dd>
257
- </dl>
258
- </aside>
259
-
260
- </div>
261
-
262
- <script>
263
- /* ── tiny deterministic hash so the demo is stable ── */
264
- function h(s) {
265
- let x = 2166136261;
266
- for (let i = 0; i < s.length; i++) { x ^= s.charCodeAt(i); x = (x * 16777619) >>> 0; }
267
- return x / 4294967296;
268
- }
269
- const COLORS = ["#D97757", "#788C5D", "#6A8CAF", "#C2A83E", "#B04A3F", "#87867F", "#3D6E6E", "#A67C52"];
270
- const CX = 130, CY = 130, R = 100, RKEY = 78;
271
- const TAU = Math.PI * 2;
272
-
273
- const ring = document.getElementById("ring");
274
- const nSlider = document.getElementById("nSlider");
275
- const kSlider = document.getElementById("kSlider");
276
- const nVal = document.getElementById("nVal");
277
- const kVal = document.getElementById("kVal");
278
- const readout = document.getElementById("readout");
279
-
280
- let nodes = [], keys = [], lastOwner = {};
281
-
282
- function pt(r, t) { return [CX + r * Math.sin(t * TAU), CY - r * Math.cos(t * TAU)]; }
283
-
284
- function ownerOf(t) {
285
- const sorted = [...nodes].sort((a, b) => a.t - b.t);
286
- for (const n of sorted) if (n.t >= t) return n;
287
- return sorted[0];
288
- }
289
-
290
- function buildNodes(n) {
291
- nodes = [];
292
- for (let i = 0; i < n; i++) nodes.push({ id: "n" + i, t: h("node-" + i), color: COLORS[i % COLORS.length] });
293
- }
294
- function buildKeys(k) {
295
- keys = [];
296
- for (let i = 0; i < k; i++) keys.push({ id: "k" + i, t: h("key-" + i) });
297
- }
298
-
299
- function arcPath(t0, t1) {
300
- const [x0, y0] = pt(R, t0), [x1, y1] = pt(R, t1);
301
- let d = t1 - t0; if (d <= 0) d += 1;
302
- const large = d > 0.5 ? 1 : 0;
303
- return `M ${x0} ${y0} A ${R} ${R} 0 ${large} 1 ${x1} ${y1}`;
304
- }
305
-
306
- function render(moved) {
307
- const sorted = [...nodes].sort((a, b) => a.t - b.t);
308
- let svg = `<circle class="track" cx="${CX}" cy="${CY}" r="${R}"/>`;
309
-
310
- for (let i = 0; i < sorted.length; i++) {
311
- const cur = sorted[i];
312
- const prev = sorted[(i - 1 + sorted.length) % sorted.length];
313
- svg += `<path class="arc" stroke="${cur.color}" d="${arcPath(prev.t, cur.t)}"/>`;
314
- }
315
- for (const k of keys) {
316
- const o = ownerOf(k.t);
317
- const [x, y] = pt(RKEY, k.t);
318
- svg += `<circle class="key" r="3.5" cx="${x}" cy="${y}" fill="${o.color}"/>`;
319
- }
320
- for (const n of sorted) {
321
- const [x, y] = pt(R, n.t);
322
- svg += `<circle class="node" r="9" cx="${x}" cy="${y}" fill="${n.color}"/>`;
323
- }
324
- svg += `<text class="lbl" x="${CX}" y="18" text-anchor="middle">0</text>`;
325
- ring.innerHTML = svg;
326
-
327
- readout.innerHTML =
328
- `<b>${nodes.length}</b> nodes · <b>${keys.length}</b> keys · ` +
329
- `<span class="moved">${moved == null ? "—" : moved + " (" + Math.round(moved / keys.length * 100) + "%)"}</span> moved on last change`;
330
- }
331
-
332
- function diffAndRender() {
333
- let moved = 0;
334
- for (const k of keys) {
335
- const o = ownerOf(k.t).id;
336
- if (lastOwner[k.id] && lastOwner[k.id] !== o) moved++;
337
- lastOwner[k.id] = o;
338
- }
339
- render(moved);
340
- }
341
-
342
- function reset() {
343
- buildNodes(+nSlider.value);
344
- buildKeys(+kSlider.value);
345
- lastOwner = {};
346
- for (const k of keys) lastOwner[k.id] = ownerOf(k.t).id;
347
- nVal.textContent = nSlider.value;
348
- kVal.textContent = kSlider.value;
349
- render(null);
350
- }
351
-
352
- nSlider.oninput = () => { nVal.textContent = nSlider.value; buildNodes(+nSlider.value); diffAndRender(); };
353
- kSlider.oninput = () => { kVal.textContent = kSlider.value; buildKeys(+kSlider.value); lastOwner = {}; for (const k of keys) lastOwner[k.id] = ownerOf(k.t).id; render(null); };
354
- document.getElementById("rm").onclick = () => { if (nodes.length > 2) { nodes.splice(Math.floor(Math.random() * nodes.length), 1); diffAndRender(); } };
355
- 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(); } };
356
- document.getElementById("reset").onclick = reset;
357
-
358
- /* glossary highlight */
359
- document.querySelectorAll(".term").forEach(el => {
360
- const g = el.dataset.term;
361
- el.addEventListener("mouseenter", () => document.querySelector(`dt[data-g="${g}"]`)?.classList.add("hl"));
362
- el.addEventListener("mouseleave", () => document.querySelector(`dt[data-g="${g}"]`)?.classList.remove("hl"));
363
- });
364
-
365
- reset();
366
- </script>
367
- </body>
368
- </html>