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