claude-dev-env 1.82.0 → 1.84.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.
- package/CLAUDE.md +16 -13
- package/_shared/pr-loop/audit-contract.md +24 -12
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
- package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
- package/agents/clean-coder.md +3 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +16 -8
- package/bin/install.test.mjs +17 -6
- package/commands/CLAUDE.md +0 -1
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -4
- package/hooks/blocking/code_rules_constants_config.py +164 -1
- package/hooks/blocking/code_rules_docstrings.py +428 -15
- package/hooks/blocking/code_rules_enforcer.py +33 -0
- package/hooks/blocking/code_rules_imports_logging.py +867 -1
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +152 -6
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/code_rules_string_magic.py +68 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/reviewer_spawn_gate.py +182 -0
- package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
- package/hooks/blocking/state_description_blocker.py +96 -5
- package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
- package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
- package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
- package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
- package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
- package/hooks/blocking/test_code_rules_magic_values.py +54 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
- package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
- package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
- package/hooks/blocking/test_state_description_blocker.py +135 -0
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/CLAUDE.md +3 -4
- package/hooks/hooks_constants/blocking_check_limits.py +46 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
- package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
- package/hooks/validation/post_tool_use_dispatcher.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
- package/hooks/workflow/CLAUDE.md +2 -8
- package/package.json +1 -1
- package/rules/CLAUDE.md +5 -2
- package/rules/claude-md-orphan-file.md +5 -0
- package/rules/docstring-prose-matches-implementation.md +10 -1
- package/rules/env-var-table-code-drift.md +5 -0
- package/rules/es-exe-file-search.md +17 -0
- package/rules/no-historical-clutter.md +12 -1
- package/rules/orphan-css-class.md +5 -0
- package/rules/package-inventory-stale-entry.md +10 -0
- package/rules/paired-test-coverage.md +5 -0
- package/rules/plain-illustrative-docstrings.md +40 -1
- package/rules/verify-before-asking.md +7 -0
- package/rules/verify-runtime-state.md +40 -0
- package/rules/windows-filesystem-safe.md +8 -0
- package/rules/workers-done-before-complete.md +33 -0
- package/rules/workflow-substitution-slots.md +5 -0
- package/skills/CLAUDE.md +7 -2
- package/skills/autoconverge/SKILL.md +48 -61
- package/skills/autoconverge/reference/closing-report.md +6 -6
- package/skills/autoconverge/reference/convergence.md +17 -15
- package/skills/autoconverge/reference/gotchas.md +6 -3
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
- package/skills/autoconverge/workflow/converge.contract.test.mjs +153 -10
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +55 -19
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +167 -106
- package/skills/autoconverge/workflow/render_report.py +7 -11
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +25 -72
- package/skills/bugteam/reference/CLAUDE.md +1 -3
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -7
- package/skills/bugteam/scripts/CLAUDE.md +0 -6
- package/skills/bugteam/scripts/README.md +0 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
- package/skills/code/SKILL.md +2 -0
- package/skills/copilot-review/CLAUDE.md +1 -1
- package/skills/copilot-review/SKILL.md +25 -23
- package/skills/everything-search/SKILL.md +5 -0
- package/skills/findbugs/CLAUDE.md +2 -2
- package/skills/findbugs/SKILL.md +22 -83
- package/skills/fixbugs/SKILL.md +2 -4
- package/skills/log-audit/CLAUDE.md +20 -0
- package/skills/log-audit/SKILL.md +68 -0
- package/skills/log-audit/reference/CLAUDE.md +9 -0
- package/skills/log-audit/reference/charter.md +52 -0
- package/skills/log-audit/scripts/CLAUDE.md +27 -0
- package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
- package/skills/log-audit/scripts/collect_log_window.py +199 -0
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
- package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
- package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
- package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
- package/skills/monitor-open-prs/SKILL.md +2 -2
- package/skills/post-audit-findings/SKILL.md +84 -0
- package/skills/pr-converge/CLAUDE.md +2 -0
- package/skills/pr-converge/SKILL.md +72 -59
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +16 -19
- package/skills/pr-converge/reference/examples.md +5 -5
- package/skills/pr-converge/reference/fix-protocol.md +16 -43
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +24 -45
- package/skills/pr-converge/reference/state-schema.md +15 -0
- package/skills/pr-converge/scripts/README.md +3 -5
- package/skills/pr-fix-protocol/SKILL.md +70 -0
- package/skills/pr-loop-lifecycle/SKILL.md +73 -0
- package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
- package/skills/pr-scope-resolve/SKILL.md +48 -0
- package/skills/qbug/CLAUDE.md +4 -4
- package/skills/qbug/SKILL.md +46 -144
- package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
- package/skills/reviewer-gates/SKILL.md +96 -0
- package/skills/session-log/CLAUDE.md +7 -7
- package/skills/session-log/SKILL.md +27 -44
- package/skills/test_markdown_link_integrity.py +103 -0
- package/commands/doc-gist.md +0 -16
- package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
- package/hooks/blocking/conftest.py +0 -30
- package/hooks/blocking/md_path_exemptions.py +0 -224
- package/hooks/blocking/md_to_html_blocker.py +0 -155
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
- package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
- package/hooks/hooks_constants/html_companion_constants.py +0 -20
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
- package/hooks/workflow/doc_gist_auto_publish.py +0 -144
- package/hooks/workflow/md_to_html_companion.py +0 -358
- package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
- package/hooks/workflow/test_md_to_html_companion.py +0 -613
- package/skills/bugteam/reference/audit-contract.md +0 -163
- package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
- package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
- package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
- package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
- package/skills/doc-gist/CLAUDE.md +0 -25
- package/skills/doc-gist/SKILL.md +0 -97
- package/skills/doc-gist/references/CLAUDE.md +0 -9
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
- package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
- package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
- package/skills/doc-gist/references/examples/05-design-system.html +0 -629
- package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
- package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
- package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
- package/skills/doc-gist/references/examples/11-status-report.html +0 -528
- package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
- package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
- package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
- package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
- package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
- package/skills/doc-gist/references/examples/README.md +0 -5
- package/skills/doc-gist/scripts/CLAUDE.md +0 -27
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
- package/skills/doc-gist/scripts/gist_upload.py +0 -177
- package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
- /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
|
@@ -1,381 +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>How rate limiting works in birchline/api</title>
|
|
7
|
-
<style>
|
|
8
|
-
:root {
|
|
9
|
-
--ivory: #FAF9F5;
|
|
10
|
-
--slate: #141413;
|
|
11
|
-
--clay: #D97757;
|
|
12
|
-
--oat: #E3DACC;
|
|
13
|
-
--olive: #788C5D;
|
|
14
|
-
--gray-150:#F0EEE6;
|
|
15
|
-
--gray-300:#D1CFC5;
|
|
16
|
-
--gray-500:#87867F;
|
|
17
|
-
--gray-700:#3D3D3A;
|
|
18
|
-
--serif: ui-serif, Georgia, "Times New Roman", serif;
|
|
19
|
-
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
20
|
-
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
21
|
-
}
|
|
22
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
23
|
-
body {
|
|
24
|
-
background: var(--ivory);
|
|
25
|
-
color: var(--gray-700);
|
|
26
|
-
font-family: var(--sans);
|
|
27
|
-
font-size: 15px;
|
|
28
|
-
line-height: 1.65;
|
|
29
|
-
-webkit-font-smoothing: antialiased;
|
|
30
|
-
padding: 56px 24px 120px;
|
|
31
|
-
}
|
|
32
|
-
.page {
|
|
33
|
-
max-width: 1100px;
|
|
34
|
-
margin: 0 auto;
|
|
35
|
-
display: grid;
|
|
36
|
-
grid-template-columns: 200px minmax(0, 1fr);
|
|
37
|
-
gap: 48px;
|
|
38
|
-
}
|
|
39
|
-
@media (max-width: 920px) { .page { grid-template-columns: 1fr; } nav { display: none; } }
|
|
40
|
-
|
|
41
|
-
/* ── nav ──────────────────────────────── */
|
|
42
|
-
nav {
|
|
43
|
-
position: sticky;
|
|
44
|
-
top: 32px;
|
|
45
|
-
align-self: start;
|
|
46
|
-
font-size: 13px;
|
|
47
|
-
}
|
|
48
|
-
nav .label {
|
|
49
|
-
font-family: var(--mono);
|
|
50
|
-
font-size: 10px;
|
|
51
|
-
letter-spacing: 0.1em;
|
|
52
|
-
text-transform: uppercase;
|
|
53
|
-
color: var(--gray-500);
|
|
54
|
-
margin-bottom: 12px;
|
|
55
|
-
}
|
|
56
|
-
nav a {
|
|
57
|
-
display: block;
|
|
58
|
-
padding: 5px 0 5px 12px;
|
|
59
|
-
border-left: 2px solid var(--gray-300);
|
|
60
|
-
color: var(--gray-700);
|
|
61
|
-
text-decoration: none;
|
|
62
|
-
}
|
|
63
|
-
nav a:hover { color: var(--slate); border-color: var(--slate); }
|
|
64
|
-
nav a.l2 { padding-left: 24px; font-size: 12.5px; color: var(--gray-500); }
|
|
65
|
-
nav .files {
|
|
66
|
-
margin-top: 28px;
|
|
67
|
-
border-top: 1px solid var(--gray-300);
|
|
68
|
-
padding-top: 16px;
|
|
69
|
-
}
|
|
70
|
-
nav .files code {
|
|
71
|
-
display: block;
|
|
72
|
-
font-family: var(--mono);
|
|
73
|
-
font-size: 11px;
|
|
74
|
-
color: var(--gray-500);
|
|
75
|
-
padding: 3px 0;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/* ── header ───────────────────────────── */
|
|
79
|
-
header { margin-bottom: 12px; }
|
|
80
|
-
.eyebrow {
|
|
81
|
-
font-family: var(--mono);
|
|
82
|
-
font-size: 11px;
|
|
83
|
-
letter-spacing: 0.08em;
|
|
84
|
-
text-transform: uppercase;
|
|
85
|
-
color: var(--gray-500);
|
|
86
|
-
margin-bottom: 10px;
|
|
87
|
-
}
|
|
88
|
-
h1 {
|
|
89
|
-
font-family: var(--serif);
|
|
90
|
-
font-weight: 500;
|
|
91
|
-
font-size: 32px;
|
|
92
|
-
color: var(--slate);
|
|
93
|
-
letter-spacing: -0.01em;
|
|
94
|
-
margin-bottom: 14px;
|
|
95
|
-
}
|
|
96
|
-
.tldr {
|
|
97
|
-
border: 1.5px solid var(--gray-300);
|
|
98
|
-
border-left: 3px solid var(--clay);
|
|
99
|
-
border-radius: 10px;
|
|
100
|
-
background: #fff;
|
|
101
|
-
padding: 16px 18px;
|
|
102
|
-
margin-bottom: 8px;
|
|
103
|
-
}
|
|
104
|
-
.tldr b { color: var(--slate); }
|
|
105
|
-
|
|
106
|
-
h2 {
|
|
107
|
-
font-family: var(--serif);
|
|
108
|
-
font-weight: 500;
|
|
109
|
-
font-size: 22px;
|
|
110
|
-
color: var(--slate);
|
|
111
|
-
margin: 40px 0 14px;
|
|
112
|
-
scroll-margin-top: 24px;
|
|
113
|
-
}
|
|
114
|
-
p { margin-bottom: 12px; max-width: 680px; }
|
|
115
|
-
code { font-family: var(--mono); font-size: 13px; }
|
|
116
|
-
|
|
117
|
-
/* ── collapsible ─────────────────────── */
|
|
118
|
-
details {
|
|
119
|
-
border: 1.5px solid var(--gray-300);
|
|
120
|
-
border-radius: 10px;
|
|
121
|
-
background: #fff;
|
|
122
|
-
margin: 14px 0;
|
|
123
|
-
overflow: hidden;
|
|
124
|
-
}
|
|
125
|
-
summary {
|
|
126
|
-
list-style: none;
|
|
127
|
-
cursor: pointer;
|
|
128
|
-
padding: 14px 16px;
|
|
129
|
-
font-family: var(--serif);
|
|
130
|
-
font-size: 16px;
|
|
131
|
-
color: var(--slate);
|
|
132
|
-
display: flex;
|
|
133
|
-
align-items: baseline;
|
|
134
|
-
gap: 10px;
|
|
135
|
-
}
|
|
136
|
-
summary::-webkit-details-marker { display: none; }
|
|
137
|
-
summary::before {
|
|
138
|
-
content: "▸";
|
|
139
|
-
color: var(--clay);
|
|
140
|
-
font-family: var(--sans);
|
|
141
|
-
font-size: 12px;
|
|
142
|
-
transition: transform 120ms;
|
|
143
|
-
}
|
|
144
|
-
details[open] summary::before { transform: rotate(90deg); }
|
|
145
|
-
summary .where {
|
|
146
|
-
font-family: var(--mono);
|
|
147
|
-
font-size: 11px;
|
|
148
|
-
color: var(--gray-500);
|
|
149
|
-
margin-left: auto;
|
|
150
|
-
}
|
|
151
|
-
details .body { padding: 0 16px 16px; }
|
|
152
|
-
details .body p { font-size: 14px; }
|
|
153
|
-
|
|
154
|
-
/* ── tabs ─────────────────────────────── */
|
|
155
|
-
.tabs {
|
|
156
|
-
border: 1.5px solid var(--gray-300);
|
|
157
|
-
border-radius: 10px;
|
|
158
|
-
background: #fff;
|
|
159
|
-
margin: 16px 0 8px;
|
|
160
|
-
overflow: hidden;
|
|
161
|
-
}
|
|
162
|
-
.tabbar {
|
|
163
|
-
display: flex;
|
|
164
|
-
border-bottom: 1px solid var(--gray-300);
|
|
165
|
-
background: var(--gray-150);
|
|
166
|
-
}
|
|
167
|
-
.tabbar button {
|
|
168
|
-
appearance: none;
|
|
169
|
-
border: none;
|
|
170
|
-
background: none;
|
|
171
|
-
font-family: var(--mono);
|
|
172
|
-
font-size: 12px;
|
|
173
|
-
color: var(--gray-500);
|
|
174
|
-
padding: 10px 16px;
|
|
175
|
-
cursor: pointer;
|
|
176
|
-
border-right: 1px solid var(--gray-300);
|
|
177
|
-
}
|
|
178
|
-
.tabbar button.on {
|
|
179
|
-
background: #fff;
|
|
180
|
-
color: var(--slate);
|
|
181
|
-
border-bottom: 2px solid var(--clay);
|
|
182
|
-
margin-bottom: -1px;
|
|
183
|
-
}
|
|
184
|
-
.tabs pre {
|
|
185
|
-
display: none;
|
|
186
|
-
margin: 0;
|
|
187
|
-
padding: 16px 18px;
|
|
188
|
-
font-family: var(--mono);
|
|
189
|
-
font-size: 12.5px;
|
|
190
|
-
line-height: 1.6;
|
|
191
|
-
color: var(--slate);
|
|
192
|
-
overflow-x: auto;
|
|
193
|
-
}
|
|
194
|
-
.tabs pre.on { display: block; }
|
|
195
|
-
.hl { color: var(--clay); }
|
|
196
|
-
.cm { color: var(--gray-500); }
|
|
197
|
-
|
|
198
|
-
/* ── callout ──────────────────────────── */
|
|
199
|
-
.callout {
|
|
200
|
-
display: flex;
|
|
201
|
-
gap: 12px;
|
|
202
|
-
border: 1.5px solid var(--oat);
|
|
203
|
-
background: rgba(227,218,204,0.35);
|
|
204
|
-
border-radius: 10px;
|
|
205
|
-
padding: 14px 16px;
|
|
206
|
-
margin: 18px 0;
|
|
207
|
-
font-size: 14px;
|
|
208
|
-
}
|
|
209
|
-
.callout .ico { color: var(--clay); font-weight: 600; }
|
|
210
|
-
|
|
211
|
-
/* ── faq ──────────────────────────────── */
|
|
212
|
-
dl.faq { margin-top: 8px; }
|
|
213
|
-
dl.faq dt {
|
|
214
|
-
font-family: var(--serif);
|
|
215
|
-
font-size: 16px;
|
|
216
|
-
color: var(--slate);
|
|
217
|
-
margin-top: 18px;
|
|
218
|
-
}
|
|
219
|
-
dl.faq dd { font-size: 14px; margin: 4px 0 0; max-width: 640px; }
|
|
220
|
-
</style>
|
|
221
|
-
</head>
|
|
222
|
-
<body>
|
|
223
|
-
<div class="page">
|
|
224
|
-
|
|
225
|
-
<!-- ── side nav ── -->
|
|
226
|
-
<nav>
|
|
227
|
-
<div class="label">On this page</div>
|
|
228
|
-
<a href="#tldr">TL;DR</a>
|
|
229
|
-
<a href="#path">Request path</a>
|
|
230
|
-
<a href="#path" class="l2">1. Identify</a>
|
|
231
|
-
<a href="#path" class="l2">2. Bucket lookup</a>
|
|
232
|
-
<a href="#path" class="l2">3. Consume</a>
|
|
233
|
-
<a href="#path" class="l2">4. Reject</a>
|
|
234
|
-
<a href="#config">Configuring a route</a>
|
|
235
|
-
<a href="#gotchas">Gotchas</a>
|
|
236
|
-
<a href="#faq">FAQ</a>
|
|
237
|
-
<div class="files">
|
|
238
|
-
<div class="label">Files read</div>
|
|
239
|
-
<code>middleware/ratelimit.ts</code>
|
|
240
|
-
<code>lib/tokenBucket.ts</code>
|
|
241
|
-
<code>config/limits.yaml</code>
|
|
242
|
-
<code>routes/*.ts</code>
|
|
243
|
-
</div>
|
|
244
|
-
</nav>
|
|
245
|
-
|
|
246
|
-
<!-- ── main ── -->
|
|
247
|
-
<main>
|
|
248
|
-
<header>
|
|
249
|
-
<div class="eyebrow">Research & Learning · feature summary</div>
|
|
250
|
-
<h1>How rate limiting works in <code>birchline/api</code></h1>
|
|
251
|
-
<div class="tldr" id="tldr">
|
|
252
|
-
<b>TL;DR</b> — Every request passes through <code>rateLimit()</code> middleware, which resolves the
|
|
253
|
-
caller to a <em>bucket key</em>, fetches a token-bucket from Redis, and either consumes one token or
|
|
254
|
-
returns <code>429</code>. Limits are declared per-route in <code>config/limits.yaml</code>; routes
|
|
255
|
-
without an entry inherit the <code>default</code> tier (100 req/min per API key).
|
|
256
|
-
</div>
|
|
257
|
-
</header>
|
|
258
|
-
|
|
259
|
-
<h2 id="path">The request path, step by step</h2>
|
|
260
|
-
<p>Expand each step to see what runs and where it lives. The whole path is ~40 lines and adds about
|
|
261
|
-
0.4 ms p50 to every request.</p>
|
|
262
|
-
|
|
263
|
-
<details open>
|
|
264
|
-
<summary>1 · Identify the caller <span class="where">middleware/ratelimit.ts:21</span></summary>
|
|
265
|
-
<div class="body">
|
|
266
|
-
<p>The middleware first reduces the request to a <code>bucketKey</code>: API key if an
|
|
267
|
-
<code>Authorization</code> header is present, otherwise the client IP (via the
|
|
268
|
-
<code>x-forwarded-for</code> chain, trusting only our own LB). Anonymous IP traffic gets a much
|
|
269
|
-
lower default tier.</p>
|
|
270
|
-
</div>
|
|
271
|
-
</details>
|
|
272
|
-
|
|
273
|
-
<details>
|
|
274
|
-
<summary>2 · Look up the bucket <span class="where">lib/tokenBucket.ts:9</span></summary>
|
|
275
|
-
<div class="body">
|
|
276
|
-
<p>The route name plus bucket key map to a Redis hash (<code>rl:{route}:{key}</code>) holding
|
|
277
|
-
<code>tokens</code> and <code>updatedAt</code>. If the key is missing it's created lazily at full
|
|
278
|
-
capacity — there's no warm-up.</p>
|
|
279
|
-
</div>
|
|
280
|
-
</details>
|
|
281
|
-
|
|
282
|
-
<details>
|
|
283
|
-
<summary>3 · Refill and consume <span class="where">lib/tokenBucket.ts:31</span></summary>
|
|
284
|
-
<div class="body">
|
|
285
|
-
<p>Refill is computed from elapsed time (<code>rate × Δt</code>, capped at <code>burst</code>), then
|
|
286
|
-
one token is subtracted. The whole read-modify-write runs as a single Lua script so concurrent
|
|
287
|
-
requests can't double-spend.</p>
|
|
288
|
-
</div>
|
|
289
|
-
</details>
|
|
290
|
-
|
|
291
|
-
<details>
|
|
292
|
-
<summary>4 · Reject when empty <span class="where">middleware/ratelimit.ts:48</span></summary>
|
|
293
|
-
<div class="body">
|
|
294
|
-
<p>If the script returns <code>tokens < 0</code> the middleware short-circuits with
|
|
295
|
-
<code>429 Too Many Requests</code> and sets <code>Retry-After</code> to the seconds until one token
|
|
296
|
-
refills. Successful responses always carry <code>X-RateLimit-Remaining</code>.</p>
|
|
297
|
-
</div>
|
|
298
|
-
</details>
|
|
299
|
-
|
|
300
|
-
<h2 id="config">Configuring a limit on your route</h2>
|
|
301
|
-
<p>You don't touch the middleware. Add an entry to <code>config/limits.yaml</code> keyed by route name,
|
|
302
|
-
and (optionally) tag the route so the middleware can find it.</p>
|
|
303
|
-
|
|
304
|
-
<div class="tabs" data-tabs>
|
|
305
|
-
<div class="tabbar">
|
|
306
|
-
<button class="on" data-t="0">limits.yaml</button>
|
|
307
|
-
<button data-t="1">route.ts</button>
|
|
308
|
-
<button data-t="2">client response</button>
|
|
309
|
-
</div>
|
|
310
|
-
<pre class="on"><span class="cm"># config/limits.yaml</span>
|
|
311
|
-
default:
|
|
312
|
-
rate: 100/min
|
|
313
|
-
burst: 120
|
|
314
|
-
|
|
315
|
-
<span class="hl">search.query</span>:
|
|
316
|
-
rate: 20/min
|
|
317
|
-
burst: 40
|
|
318
|
-
key: api_key <span class="cm"># or: ip</span></pre>
|
|
319
|
-
<pre><span class="cm">// routes/search.ts</span>
|
|
320
|
-
router.post(
|
|
321
|
-
"/search",
|
|
322
|
-
<span class="hl">rateLimit("search.query")</span>,
|
|
323
|
-
handler,
|
|
324
|
-
);</pre>
|
|
325
|
-
<pre>HTTP/1.1 429 Too Many Requests
|
|
326
|
-
Retry-After: 17
|
|
327
|
-
X-RateLimit-Limit: 20
|
|
328
|
-
X-RateLimit-Remaining: 0
|
|
329
|
-
|
|
330
|
-
{ "error": "rate_limited", "retry_after": 17 }</pre>
|
|
331
|
-
</div>
|
|
332
|
-
|
|
333
|
-
<div class="callout">
|
|
334
|
-
<span class="ico">★</span>
|
|
335
|
-
<div>If you only need the default tier, you don't need a YAML entry at all — just wrap the handler in
|
|
336
|
-
<code>rateLimit()</code> with no argument. The route name is inferred from the path.</div>
|
|
337
|
-
</div>
|
|
338
|
-
|
|
339
|
-
<h2 id="gotchas">Gotchas worth knowing</h2>
|
|
340
|
-
<ul style="padding-left:20px; max-width:680px;">
|
|
341
|
-
<li style="margin-bottom:8px;"><b>Limits are per-process in dev.</b> The Redis client falls back to an
|
|
342
|
-
in-memory map when <code>REDIS_URL</code> is unset, so local testing won't reflect real cluster
|
|
343
|
-
behaviour.</li>
|
|
344
|
-
<li style="margin-bottom:8px;"><b>Burst ≠ rate.</b> <code>burst</code> is the bucket capacity; a caller
|
|
345
|
-
idle for a minute can fire <code>burst</code> requests instantly even if <code>rate</code> is low.</li>
|
|
346
|
-
<li><b>Streaming responses count once.</b> The token is consumed at request start; a 30-second SSE
|
|
347
|
-
stream still costs one token.</li>
|
|
348
|
-
</ul>
|
|
349
|
-
|
|
350
|
-
<h2 id="faq">FAQ</h2>
|
|
351
|
-
<dl class="faq">
|
|
352
|
-
<dt>How do I exempt internal traffic?</dt>
|
|
353
|
-
<dd>Set <code>x-birchline-internal: 1</code> from the caller; the middleware checks it against the
|
|
354
|
-
mTLS peer name and skips the bucket entirely.</dd>
|
|
355
|
-
|
|
356
|
-
<dt>Where do I see who's getting limited?</dt>
|
|
357
|
-
<dd>Every <code>429</code> emits a <code>ratelimit.rejected</code> metric tagged with route and key
|
|
358
|
-
type. There's a Grafana panel under <em>API → Health</em>.</dd>
|
|
359
|
-
|
|
360
|
-
<dt>Can a single user have a higher limit?</dt>
|
|
361
|
-
<dd>Yes — add their API key under <code>overrides:</code> in the YAML. Overrides are reloaded without
|
|
362
|
-
a deploy.</dd>
|
|
363
|
-
</dl>
|
|
364
|
-
</main>
|
|
365
|
-
|
|
366
|
-
</div>
|
|
367
|
-
|
|
368
|
-
<script>
|
|
369
|
-
document.querySelectorAll("[data-tabs]").forEach(box => {
|
|
370
|
-
const btns = box.querySelectorAll("button");
|
|
371
|
-
const panes = box.querySelectorAll("pre");
|
|
372
|
-
btns.forEach(b => b.addEventListener("click", () => {
|
|
373
|
-
btns.forEach(x => x.classList.remove("on"));
|
|
374
|
-
panes.forEach(x => x.classList.remove("on"));
|
|
375
|
-
b.classList.add("on");
|
|
376
|
-
panes[+b.dataset.t].classList.add("on");
|
|
377
|
-
}));
|
|
378
|
-
});
|
|
379
|
-
</script>
|
|
380
|
-
</body>
|
|
381
|
-
</html>
|