claude-dev-env 1.38.1 → 1.39.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 (270) hide show
  1. package/CLAUDE.md +10 -36
  2. package/_shared/pr-loop/audit-reply-template.md +147 -0
  3. package/_shared/pr-loop/fix-protocol.md +25 -4
  4. package/_shared/pr-loop/gh-payloads.md +37 -50
  5. package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
  6. package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +189 -0
  7. package/_shared/pr-loop/scripts/post_audit_thread.py +947 -0
  8. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
  9. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +923 -0
  10. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
  11. package/_shared/pr-loop/state-schema.md +1 -1
  12. package/agents/clean-coder.md +2 -2
  13. package/bin/install.mjs +6 -7
  14. package/bin/install.test.mjs +8 -0
  15. package/commands/doc-gist.md +16 -0
  16. package/commands/plan.md +0 -2
  17. package/commands/review-plan.md +1 -1
  18. package/docs/CODE_RULES.md +122 -2
  19. package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
  20. package/hooks/blocking/code_rules_enforcer.py +1143 -129
  21. package/hooks/blocking/convergence_gate_blocker.py +130 -0
  22. package/hooks/blocking/destructive_command_blocker.py +74 -0
  23. package/hooks/blocking/gh_body_arg_blocker.py +30 -0
  24. package/hooks/blocking/md_to_html_blocker.py +119 -0
  25. package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
  26. package/hooks/blocking/test_code_rules_enforcer.py +21 -0
  27. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
  28. package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
  29. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
  30. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
  31. package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
  32. package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
  33. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  34. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
  35. package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
  36. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
  37. package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
  38. package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
  39. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
  40. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
  41. package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
  42. package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
  43. package/hooks/blocking/test_destructive_command_blocker.py +146 -0
  44. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
  45. package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
  46. package/hooks/blocking/test_md_to_html_blocker.py +317 -0
  47. package/hooks/config/any_type_config.py +7 -0
  48. package/hooks/config/banned_identifiers_constants.py +11 -0
  49. package/hooks/config/blocking_check_limits.py +38 -0
  50. package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
  51. package/hooks/config/code_rules_enforcer_constants.py +53 -0
  52. package/hooks/config/convergence_branch_constants.py +9 -0
  53. package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
  54. package/hooks/config/html_companion_constants.py +20 -0
  55. package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
  56. package/hooks/config/test_banned_identifiers_constants.py +17 -0
  57. package/hooks/hooks.json +28 -20
  58. package/hooks/pyproject.toml +69 -0
  59. package/hooks/validators/mypy_integration.py +47 -1
  60. package/hooks/validators/run_all_validators.py +3 -3
  61. package/hooks/validators/test_mypy_integration.py +50 -1
  62. package/hooks/workflow/doc_gist_auto_publish.py +144 -0
  63. package/hooks/workflow/md_to_html_companion.py +365 -0
  64. package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
  65. package/hooks/workflow/test_md_to_html_companion.py +452 -0
  66. package/package.json +1 -1
  67. package/rules/gh-body-file.md +2 -0
  68. package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
  69. package/scripts/check.ps1 +106 -0
  70. package/scripts/config/timing.py +11 -0
  71. package/scripts/sweep_empty_dirs.py +138 -0
  72. package/scripts/sync_to_cursor/rules.py +1 -1
  73. package/scripts/test_sweep_empty_dirs.py +183 -0
  74. package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
  75. package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
  76. package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
  77. package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
  78. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
  79. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
  80. package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
  81. package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
  82. package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
  83. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
  84. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
  85. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
  86. package/skills/bugteam/CONSTRAINTS.md +21 -22
  87. package/skills/bugteam/EXAMPLES.md +3 -3
  88. package/skills/bugteam/PROMPTS.md +227 -67
  89. package/skills/bugteam/SKILL.md +114 -455
  90. package/skills/bugteam/reference/README.md +1 -1
  91. package/skills/bugteam/reference/audit-and-teammates.md +112 -39
  92. package/skills/bugteam/reference/audit-contract.md +4 -22
  93. package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
  94. package/skills/bugteam/reference/design-rationale.md +2 -2
  95. package/skills/bugteam/reference/github-pr-reviews.md +50 -57
  96. package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
  97. package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
  98. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
  99. package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
  100. package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
  101. package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
  102. package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
  103. package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
  104. package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
  105. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
  106. package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
  107. package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
  108. package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
  109. package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
  110. package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
  111. package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
  112. package/skills/bugteam/reference/team-setup.md +106 -9
  113. package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
  114. package/skills/bugteam/scripts/README.md +60 -0
  115. package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
  116. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
  117. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
  118. package/skills/bugteam/scripts/bugteam_preflight.py +294 -0
  119. package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
  120. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
  121. package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
  122. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
  123. package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
  124. package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
  125. package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
  126. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
  127. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
  128. package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
  129. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
  130. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
  131. package/skills/bugteam/scripts/test_bugteam_preflight.py +268 -0
  132. package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
  133. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
  134. package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
  135. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
  136. package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
  137. package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
  138. package/skills/bugteam/test_skill_additions.py +1 -11
  139. package/skills/code/SKILL.md +176 -0
  140. package/skills/doc-gist/SKILL.md +99 -0
  141. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
  142. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
  143. package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
  144. package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
  145. package/skills/doc-gist/references/examples/05-design-system.html +629 -0
  146. package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
  147. package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
  148. package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
  149. package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
  150. package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
  151. package/skills/doc-gist/references/examples/11-status-report.html +528 -0
  152. package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
  153. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
  154. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
  155. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
  156. package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
  157. package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
  158. package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
  159. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
  160. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
  161. package/skills/doc-gist/references/examples/README.md +5 -0
  162. package/skills/doc-gist/scripts/config/__init__.py +0 -0
  163. package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
  164. package/skills/doc-gist/scripts/gist_upload.py +177 -0
  165. package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
  166. package/skills/findbugs/SKILL.md +68 -2
  167. package/skills/monitor-open-prs/SKILL.md +13 -32
  168. package/skills/monitor-open-prs/test_skill_contract.py +0 -11
  169. package/skills/pr-consistency-audit/SKILL.md +112 -0
  170. package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
  171. package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
  172. package/skills/pr-converge/SKILL.md +227 -23
  173. package/skills/pr-converge/config/__init__.py +0 -0
  174. package/skills/pr-converge/config/constants.py +62 -0
  175. package/skills/pr-converge/reference/convergence-gates.md +138 -44
  176. package/skills/pr-converge/reference/examples.md +43 -11
  177. package/skills/pr-converge/reference/fix-protocol.md +6 -5
  178. package/skills/pr-converge/reference/ground-rules.md +5 -3
  179. package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
  180. package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
  181. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
  182. package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
  183. package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
  184. package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
  185. package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
  186. package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
  187. package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
  188. package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
  189. package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
  190. package/skills/pr-converge/reference/per-tick.md +90 -31
  191. package/skills/pr-converge/reference/state-schema.md +22 -1
  192. package/skills/pr-converge/reference/stop-conditions.md +9 -7
  193. package/skills/pr-converge/scripts/README.md +34 -46
  194. package/skills/pr-converge/scripts/check_bugbot_ci.py +174 -0
  195. package/skills/pr-converge/scripts/check_convergence.py +497 -0
  196. package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
  197. package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
  198. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
  199. package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
  200. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
  201. package/skills/qbug/SKILL.md +132 -27
  202. package/skills/session-log/SKILL.md +216 -114
  203. package/skills/session-tidy/SKILL.md +1 -1
  204. package/skills/skill-builder/SKILL.md +138 -56
  205. package/skills/skill-builder/references/delegation-map.md +72 -113
  206. package/skills/skill-builder/references/progressive-disclosure.md +122 -0
  207. package/skills/skill-builder/references/self-audit-checklist.md +92 -0
  208. package/skills/skill-builder/references/skill-types.md +228 -0
  209. package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
  210. package/skills/skill-builder/templates/gap-analysis.md +15 -8
  211. package/skills/skill-builder/workflows/improve-skill.md +86 -57
  212. package/skills/skill-builder/workflows/new-skill.md +80 -168
  213. package/skills/skill-builder/workflows/polish-skill.md +78 -54
  214. package/skills/structure-prompt/SKILL.md +50 -0
  215. package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
  216. package/skills/structure-prompt/reference/block-classification.md +27 -0
  217. package/skills/structure-prompt/reference/canonical-case.md +48 -0
  218. package/skills/structure-prompt/reference/citation-depth.md +70 -0
  219. package/skills/structure-prompt/reference/cleanup.md +33 -0
  220. package/skills/structure-prompt/reference/constraints.md +33 -0
  221. package/skills/structure-prompt/reference/directives.md +37 -0
  222. package/skills/structure-prompt/reference/examples.md +72 -0
  223. package/skills/structure-prompt/reference/instantiation.md +51 -0
  224. package/skills/structure-prompt/reference/output-contract.md +72 -0
  225. package/skills/structure-prompt/reference/per-category.md +23 -0
  226. package/skills/structure-prompt/reference/persona.md +38 -0
  227. package/skills/structure-prompt/reference/research.md +33 -0
  228. package/skills/structure-prompt/reference/structure.md +28 -0
  229. package/agents/code-standards-agent.md +0 -93
  230. package/agents/groq-coder.md +0 -113
  231. package/agents/plan-executor.md +0 -226
  232. package/agents/project-docs-analyzer.md +0 -53
  233. package/agents/project-structure-organizer-agent.md +0 -72
  234. package/agents/skill-to-agent-converter.md +0 -370
  235. package/agents/skill-writer-agent.md +0 -470
  236. package/agents/user-docs-writer.md +0 -67
  237. package/agents/workflow-visual-documenter.md +0 -82
  238. package/commands/readability-review.md +0 -20
  239. package/hooks/mypy.ini +0 -2
  240. package/hooks/notification/attention_needed_notify.py +0 -71
  241. package/hooks/notification/claude_notification_handler.py +0 -67
  242. package/hooks/notification/notification_utils.py +0 -267
  243. package/hooks/notification/subagent_complete_notify.py +0 -381
  244. package/hooks/notification/test_attention_needed_notify.py +0 -47
  245. package/hooks/notification/test_claude_notification_handler.py +0 -54
  246. package/hooks/notification/test_notification_utils.py +0 -91
  247. package/hooks/notification/test_subagent_complete_notify.py +0 -79
  248. package/scripts/config/groq_bugteam_config.py +0 -230
  249. package/scripts/config/test_groq_bugteam_config.py +0 -83
  250. package/scripts/config/test_spec_implementer_prompt.py +0 -32
  251. package/scripts/groq_bugteam.README.md +0 -131
  252. package/scripts/groq_bugteam.py +0 -647
  253. package/scripts/groq_bugteam_dotenv.py +0 -40
  254. package/scripts/groq_bugteam_spec.py +0 -226
  255. package/scripts/test_groq_bugteam.py +0 -529
  256. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
  257. package/scripts/test_groq_bugteam_dotenv.py +0 -66
  258. package/scripts/test_groq_bugteam_spec.py +0 -338
  259. package/skills/bugteam/SKILL_EVALS.md +0 -309
  260. package/skills/dream/SKILL.md +0 -118
  261. package/skills/ingest/SKILL.md +0 -40
  262. package/skills/npm-creator/SKILL.md +0 -187
  263. package/skills/readability-review/SKILL.md +0 -127
  264. package/skills/resume-review/SKILL.md +0 -261
  265. package/skills/rule-audit/SKILL.md +0 -307
  266. package/skills/rule-creator/SKILL.md +0 -150
  267. package/skills/searching-obsidian-vault/SKILL.md +0 -131
  268. package/skills/skill-writer/REFERENCE.md +0 -284
  269. package/skills/skill-writer/SKILL.md +0 -222
  270. package/skills/tdd-team/SKILL.md +0 -128
@@ -0,0 +1,368 @@
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>