claude-dev-env 1.83.0 → 1.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/_shared/pr-loop/audit-contract.md +24 -12
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +8 -0
  6. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
  7. package/_shared/pr-loop/scripts/terminology_sweep.py +85 -39
  8. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
  9. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
  12. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
  13. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +94 -4
  14. package/agents/clean-coder.md +3 -0
  15. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  16. package/bin/install.mjs +16 -8
  17. package/bin/install.test.mjs +17 -6
  18. package/commands/CLAUDE.md +0 -1
  19. package/docs/CODE_RULES.md +1 -1
  20. package/hooks/blocking/CLAUDE.md +0 -4
  21. package/hooks/blocking/claude_md_orphan_file_blocker.py +1 -1
  22. package/hooks/blocking/code_rules_constants_config.py +5 -0
  23. package/hooks/blocking/code_rules_docstrings.py +133 -19
  24. package/hooks/blocking/code_rules_enforcer.py +4 -0
  25. package/hooks/blocking/code_rules_imports_logging.py +10 -6
  26. package/hooks/blocking/code_rules_magic_values.py +5 -0
  27. package/hooks/blocking/code_rules_naming_collection.py +15 -8
  28. package/hooks/blocking/code_rules_paired_test.py +3 -2
  29. package/hooks/blocking/code_rules_shared.py +34 -0
  30. package/hooks/blocking/code_rules_string_magic.py +1 -1
  31. package/hooks/blocking/code_rules_unused_imports.py +2 -2
  32. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
  33. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +29 -0
  34. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
  35. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
  36. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +24 -0
  37. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
  38. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
  39. package/hooks/blocking/test_code_rules_magic_values.py +54 -0
  40. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
  41. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
  42. package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
  43. package/hooks/hooks_constants/CLAUDE.md +0 -3
  44. package/hooks/hooks_constants/blocking_check_limits.py +4 -0
  45. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  46. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
  47. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
  48. package/hooks/validation/post_tool_use_dispatcher.py +1 -1
  49. package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
  50. package/hooks/workflow/CLAUDE.md +2 -8
  51. package/package.json +1 -1
  52. package/rules/CLAUDE.md +1 -1
  53. package/rules/docstring-prose-matches-implementation.md +1 -1
  54. package/rules/env-var-table-code-drift.md +1 -1
  55. package/rules/package-inventory-stale-entry.md +1 -1
  56. package/rules/paired-test-coverage.md +1 -1
  57. package/rules/plain-illustrative-docstrings.md +35 -1
  58. package/skills/CLAUDE.md +6 -1
  59. package/skills/_shared/pr-loop/CLAUDE.md +1 -0
  60. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  61. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  62. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +45 -0
  63. package/skills/_shared/pr-loop/scripts/test_write_handoff.py +201 -0
  64. package/skills/_shared/pr-loop/scripts/write_handoff.py +309 -0
  65. package/skills/autoconverge/SKILL.md +82 -58
  66. package/skills/autoconverge/reference/closing-report.md +6 -6
  67. package/skills/autoconverge/reference/convergence.md +17 -15
  68. package/skills/autoconverge/reference/gotchas.md +6 -3
  69. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
  70. package/skills/autoconverge/workflow/converge.contract.test.mjs +87 -13
  71. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
  72. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
  73. package/skills/autoconverge/workflow/converge.mjs +71 -74
  74. package/skills/autoconverge/workflow/render_report.py +7 -11
  75. package/skills/bugteam/CLAUDE.md +1 -1
  76. package/skills/bugteam/PROMPTS.md +7 -6
  77. package/skills/bugteam/SKILL.md +25 -72
  78. package/skills/bugteam/reference/CLAUDE.md +1 -3
  79. package/skills/bugteam/reference/README.md +1 -1
  80. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  81. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
  82. package/skills/bugteam/reference/team-setup.md +8 -7
  83. package/skills/bugteam/scripts/CLAUDE.md +0 -6
  84. package/skills/bugteam/scripts/README.md +0 -4
  85. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
  86. package/skills/code/SKILL.md +2 -0
  87. package/skills/copilot-review/CLAUDE.md +1 -1
  88. package/skills/copilot-review/SKILL.md +25 -23
  89. package/skills/findbugs/CLAUDE.md +2 -2
  90. package/skills/findbugs/SKILL.md +22 -83
  91. package/skills/fixbugs/SKILL.md +2 -4
  92. package/skills/log-audit/CLAUDE.md +20 -0
  93. package/skills/log-audit/SKILL.md +68 -0
  94. package/skills/log-audit/reference/CLAUDE.md +9 -0
  95. package/skills/log-audit/reference/charter.md +52 -0
  96. package/skills/log-audit/scripts/CLAUDE.md +27 -0
  97. package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
  98. package/skills/log-audit/scripts/collect_log_window.py +199 -0
  99. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
  100. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
  101. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
  102. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
  103. package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
  104. package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
  105. package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
  106. package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
  107. package/skills/monitor-open-prs/SKILL.md +2 -2
  108. package/skills/post-audit-findings/SKILL.md +84 -0
  109. package/skills/pr-converge/CLAUDE.md +2 -0
  110. package/skills/pr-converge/SKILL.md +99 -60
  111. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  112. package/skills/pr-converge/reference/convergence-gates.md +16 -19
  113. package/skills/pr-converge/reference/examples.md +5 -5
  114. package/skills/pr-converge/reference/fix-protocol.md +16 -43
  115. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
  116. package/skills/pr-converge/reference/per-tick.md +24 -45
  117. package/skills/pr-converge/reference/state-schema.md +27 -0
  118. package/skills/pr-converge/scripts/README.md +3 -5
  119. package/skills/pr-fix-protocol/SKILL.md +70 -0
  120. package/skills/pr-loop-lifecycle/SKILL.md +73 -0
  121. package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
  122. package/skills/pr-scope-resolve/SKILL.md +48 -0
  123. package/skills/qbug/CLAUDE.md +4 -4
  124. package/skills/qbug/SKILL.md +46 -144
  125. package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
  126. package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
  127. package/skills/reviewer-gates/SKILL.md +96 -0
  128. package/skills/session-log/CLAUDE.md +7 -7
  129. package/skills/session-log/SKILL.md +27 -44
  130. package/skills/test_markdown_link_integrity.py +103 -0
  131. package/commands/doc-gist.md +0 -16
  132. package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
  133. package/hooks/blocking/conftest.py +0 -30
  134. package/hooks/blocking/md_path_exemptions.py +0 -224
  135. package/hooks/blocking/md_to_html_blocker.py +0 -155
  136. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
  137. package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
  138. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
  139. package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
  140. package/hooks/hooks_constants/html_companion_constants.py +0 -20
  141. package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
  142. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
  143. package/hooks/workflow/doc_gist_auto_publish.py +0 -144
  144. package/hooks/workflow/md_to_html_companion.py +0 -358
  145. package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
  146. package/hooks/workflow/test_md_to_html_companion.py +0 -613
  147. package/skills/bugteam/reference/audit-contract.md +0 -163
  148. package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
  149. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
  150. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
  151. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
  152. package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
  153. package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
  154. package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
  155. package/skills/doc-gist/CLAUDE.md +0 -25
  156. package/skills/doc-gist/SKILL.md +0 -97
  157. package/skills/doc-gist/references/CLAUDE.md +0 -9
  158. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
  159. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
  160. package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
  161. package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
  162. package/skills/doc-gist/references/examples/05-design-system.html +0 -629
  163. package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
  164. package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
  165. package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
  166. package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
  167. package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
  168. package/skills/doc-gist/references/examples/11-status-report.html +0 -528
  169. package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
  170. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
  171. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
  172. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
  173. package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
  174. package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
  175. package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
  176. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
  177. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
  178. package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
  179. package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
  180. package/skills/doc-gist/references/examples/README.md +0 -5
  181. package/skills/doc-gist/scripts/CLAUDE.md +0 -27
  182. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
  183. package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
  184. package/skills/doc-gist/scripts/gist_upload.py +0 -177
  185. package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
  186. /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
@@ -1,663 +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>Birchline — flags.production.json</title>
7
- <style>
8
- :root {
9
- --ivory: #FAF9F5;
10
- --slate: #141413;
11
- --clay: #D97757;
12
- --clay-d: #B85C3E;
13
- --oat: #E3DACC;
14
- --olive: #788C5D;
15
- --gray-50: #F0EEE6;
16
- --gray-200: #D1CFC5;
17
- --gray-500: #87867F;
18
- --gray-800: #3D3D3A;
19
- --white: #ffffff;
20
- --serif: ui-serif, Georgia, "Times New Roman", serif;
21
- --sans: system-ui, -apple-system, "Segoe UI", sans-serif;
22
- --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
23
- }
24
- * { box-sizing: border-box; }
25
- html, body {
26
- margin: 0;
27
- background: var(--ivory);
28
- color: var(--slate);
29
- font-family: var(--sans);
30
- font-size: 14px;
31
- line-height: 1.5;
32
- }
33
- .wrap {
34
- max-width: 1080px;
35
- margin: 0 auto;
36
- padding: 48px 32px 64px;
37
- }
38
-
39
- /* ---------- header ---------- */
40
- header { margin-bottom: 28px; }
41
- .eyebrow {
42
- font-family: var(--mono);
43
- font-size: 11px;
44
- letter-spacing: 0.08em;
45
- text-transform: uppercase;
46
- color: var(--gray-500);
47
- }
48
- h1 {
49
- font-family: var(--serif);
50
- font-weight: 500;
51
- font-size: 30px;
52
- letter-spacing: -0.01em;
53
- margin: 6px 0 4px;
54
- }
55
- .sub {
56
- color: var(--gray-500);
57
- max-width: 600px;
58
- }
59
-
60
- /* ---------- layout ---------- */
61
- .layout {
62
- display: grid;
63
- grid-template-columns: 1fr 320px;
64
- gap: 24px;
65
- align-items: start;
66
- }
67
- @media (max-width: 880px) {
68
- .layout { grid-template-columns: 1fr; }
69
- .sidebar { position: static !important; }
70
- }
71
-
72
- /* ---------- warning banner ---------- */
73
- .banner {
74
- grid-column: 1 / -1;
75
- display: none;
76
- align-items: baseline;
77
- gap: 8px;
78
- padding: 10px 14px;
79
- border: 1.5px solid #E8C2AE;
80
- background: #F8E9E0;
81
- color: var(--clay-d);
82
- border-radius: 10px;
83
- font-size: 13px;
84
- }
85
- .banner.show { display: flex; }
86
- .banner .glyph { font-size: 13px; line-height: 1; transform: translateY(1px); }
87
-
88
- /* ---------- groups / panels ---------- */
89
- .form-col { display: flex; flex-direction: column; gap: 16px; }
90
- .group {
91
- background: var(--white);
92
- border: 1.5px solid var(--gray-200);
93
- border-radius: 12px;
94
- overflow: hidden;
95
- }
96
- .group-head {
97
- font-family: var(--mono);
98
- font-size: 11px;
99
- letter-spacing: 0.08em;
100
- text-transform: uppercase;
101
- color: var(--gray-500);
102
- padding: 12px 18px 11px;
103
- border-bottom: 1.5px solid var(--gray-50);
104
- display: flex;
105
- justify-content: space-between;
106
- gap: 12px;
107
- }
108
- .group-head .name { color: var(--gray-800); }
109
- .group-head .meta { letter-spacing: 0.04em; }
110
-
111
- /* ---------- flag rows ---------- */
112
- .flag {
113
- display: flex;
114
- align-items: flex-start;
115
- gap: 16px;
116
- padding: 14px 18px;
117
- border-bottom: 1px solid var(--gray-50);
118
- border-left: 3px solid transparent;
119
- transition: border-left-color 140ms ease, background 140ms ease;
120
- }
121
- .flag:last-child { border-bottom: none; }
122
- .flag.warn {
123
- border-left-color: var(--clay);
124
- background: #FBF3EE;
125
- }
126
-
127
- .flag-info { flex: 1; min-width: 0; }
128
- .flag-key {
129
- font-family: var(--mono);
130
- font-size: 13px;
131
- color: var(--gray-800);
132
- display: flex;
133
- align-items: center;
134
- gap: 7px;
135
- flex-wrap: wrap;
136
- }
137
- .dot {
138
- display: none;
139
- width: 6px;
140
- height: 6px;
141
- border-radius: 50%;
142
- background: var(--clay);
143
- flex: none;
144
- }
145
- .flag.changed .dot { display: inline-block; }
146
- .rollout {
147
- font-family: var(--mono);
148
- font-size: 10px;
149
- letter-spacing: 0.03em;
150
- color: var(--gray-500);
151
- background: var(--gray-50);
152
- border: 1px solid var(--gray-200);
153
- border-radius: 999px;
154
- padding: 1px 7px 2px;
155
- }
156
- .flag-desc {
157
- color: var(--gray-500);
158
- font-size: 12.5px;
159
- margin-top: 2px;
160
- }
161
- .req-chip {
162
- display: inline-flex;
163
- align-items: baseline;
164
- gap: 5px;
165
- margin-top: 7px;
166
- font-family: var(--mono);
167
- font-size: 11px;
168
- color: var(--gray-500);
169
- background: var(--gray-50);
170
- border: 1px solid var(--gray-200);
171
- border-radius: 999px;
172
- padding: 2px 9px 3px;
173
- }
174
- .req-chip .warn-glyph { display: none; }
175
- .flag.warn .req-chip {
176
- color: var(--clay-d);
177
- background: #F8E1D5;
178
- border-color: #E8C2AE;
179
- }
180
- .flag.warn .req-chip .warn-glyph { display: inline; }
181
-
182
- /* ---------- toggle switch ---------- */
183
- .toggle {
184
- position: relative;
185
- flex: none;
186
- width: 38px;
187
- height: 22px;
188
- margin-top: 1px;
189
- }
190
- .toggle input {
191
- position: absolute;
192
- inset: 0;
193
- width: 100%;
194
- height: 100%;
195
- margin: 0;
196
- opacity: 0;
197
- cursor: pointer;
198
- }
199
- .toggle .track {
200
- position: absolute;
201
- inset: 0;
202
- background: var(--gray-200);
203
- border-radius: 999px;
204
- transition: background 160ms ease;
205
- pointer-events: none;
206
- }
207
- .toggle .track::after {
208
- content: "";
209
- position: absolute;
210
- top: 3px;
211
- left: 3px;
212
- width: 16px;
213
- height: 16px;
214
- border-radius: 50%;
215
- background: var(--white);
216
- box-shadow: 0 1px 2px rgba(20,20,19,0.18);
217
- transition: transform 160ms ease;
218
- }
219
- .toggle input:checked + .track { background: var(--olive); }
220
- .toggle input:checked + .track::after { transform: translateX(16px); }
221
- .toggle input:focus-visible + .track {
222
- outline: 2px solid var(--slate);
223
- outline-offset: 2px;
224
- }
225
-
226
- /* ---------- sidebar ---------- */
227
- .sidebar {
228
- position: sticky;
229
- top: 24px;
230
- background: var(--white);
231
- border: 1.5px solid var(--gray-200);
232
- border-radius: 12px;
233
- padding: 18px;
234
- display: flex;
235
- flex-direction: column;
236
- gap: 14px;
237
- }
238
- .side-label {
239
- font-family: var(--mono);
240
- font-size: 11px;
241
- letter-spacing: 0.08em;
242
- text-transform: uppercase;
243
- color: var(--gray-500);
244
- }
245
- .side-count {
246
- font-family: var(--serif);
247
- font-weight: 500;
248
- font-size: 19px;
249
- letter-spacing: -0.01em;
250
- }
251
- .side-count .warn-n { color: var(--clay-d); }
252
- .side-count .sep { color: var(--gray-200); margin: 0 4px; }
253
- .diff {
254
- margin: 0;
255
- font-family: var(--mono);
256
- font-size: 12px;
257
- line-height: 1.7;
258
- background: var(--gray-50);
259
- border: 1px solid var(--gray-200);
260
- border-radius: 8px;
261
- padding: 12px 14px;
262
- overflow-x: auto;
263
- max-height: 320px;
264
- overflow-y: auto;
265
- white-space: pre;
266
- }
267
- .diff .minus { color: var(--gray-500); }
268
- .diff .plus { color: var(--olive); }
269
- .diff .empty { color: var(--gray-500); }
270
-
271
- .btn {
272
- display: block;
273
- width: 100%;
274
- border: none;
275
- border-radius: 999px;
276
- font-family: var(--mono);
277
- font-size: 12px;
278
- padding: 9px 16px;
279
- cursor: pointer;
280
- text-align: center;
281
- transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
282
- }
283
- .btn.primary {
284
- background: var(--slate);
285
- color: var(--ivory);
286
- }
287
- .btn.primary:hover:not(:disabled) { background: var(--gray-800); }
288
- .btn.primary:disabled {
289
- background: var(--gray-200);
290
- color: var(--gray-500);
291
- cursor: not-allowed;
292
- }
293
- .btn.primary.copied { background: var(--olive); }
294
- .btn.ghost {
295
- background: transparent;
296
- color: var(--gray-800);
297
- border: 1.5px solid var(--gray-200);
298
- }
299
- .btn.ghost:hover { border-color: var(--gray-500); }
300
- .btn.ghost.copied { border-color: var(--olive); color: var(--olive); }
301
- .btn-stack { display: flex; flex-direction: column; gap: 8px; }
302
- </style>
303
- </head>
304
- <body>
305
- <div class="wrap">
306
-
307
- <header>
308
- <div class="eyebrow">Birchline / editor / feature-flags</div>
309
- <h1>flags.production.json</h1>
310
- <p class="sub">A form-based editor for the production feature-flag config.
311
- Toggle flags, fix dependency warnings as they surface, then copy out
312
- only the lines that changed.</p>
313
- </header>
314
-
315
- <div class="layout">
316
- <div class="banner" id="banner">
317
- <span class="glyph">&#9888;</span>
318
- <span id="bannerText">1 flag is enabled without its prerequisite</span>
319
- </div>
320
-
321
- <div class="form-col" id="formCol"><!-- groups injected --></div>
322
-
323
- <aside class="sidebar">
324
- <div>
325
- <div class="side-label">Pending changes</div>
326
- <div class="side-count" id="sideCount">0 changed</div>
327
- </div>
328
- <pre class="diff" id="diff"><span class="empty">// no changes yet</span></pre>
329
- <div class="btn-stack">
330
- <button class="btn primary" id="copyDiff" disabled>Copy diff</button>
331
- <button class="btn ghost" id="copyJson">Copy full JSON</button>
332
- <button class="btn ghost" id="reset">Reset</button>
333
- </div>
334
- </aside>
335
- </div>
336
-
337
- </div>
338
-
339
- <script>
340
- "use strict";
341
-
342
- // ---------------------------------------------------------------------------
343
- // data — the production flag set, as it exists right now
344
- // ---------------------------------------------------------------------------
345
-
346
- const GROUPS = [
347
- {
348
- id: "onboarding",
349
- label: "Onboarding",
350
- flags: [
351
- { key: "onboarding.checklist_v2",
352
- desc: "New three-step setup checklist replacing the modal tour.",
353
- on: true },
354
- { key: "onboarding.invite_nudge",
355
- desc: "Nudge owners to invite teammates after creating their first project.",
356
- on: true },
357
- { key: "onboarding.workspace_templates",
358
- desc: "Offer prebuilt workspace templates during signup.",
359
- on: false, requires: "onboarding.checklist_v2" },
360
- { key: "onboarding.skip_email_verify",
361
- desc: "Let SSO-domain users in before email verification completes.",
362
- on: false }
363
- ]
364
- },
365
- {
366
- id: "sync",
367
- label: "Sync engine",
368
- flags: [
369
- { key: "sync.delta_compression",
370
- desc: "Send field-level deltas instead of full document snapshots.",
371
- on: true },
372
- { key: "sync.offline_queue_v2",
373
- desc: "Persist offline edits to IndexedDB and replay on reconnect.",
374
- on: false, requires: "sync.delta_compression" },
375
- { key: "sync.presence_cursors",
376
- desc: "Show live collaborator cursors in board and doc views.",
377
- on: true },
378
- { key: "sync.conflict_banner",
379
- desc: "Surface a merge banner instead of silently last-write-wins.",
380
- on: false, requires: "sync.offline_queue_v2" },
381
- { key: "sync.binary_ws_frames",
382
- desc: "Switch the realtime channel to binary WebSocket frames.",
383
- on: false, rollout: 10 }
384
- ]
385
- },
386
- {
387
- id: "billing",
388
- label: "Billing",
389
- flags: [
390
- { key: "billing.usage_meter",
391
- desc: "Show per-seat usage meter on the workspace billing page.",
392
- on: false },
393
- { key: "billing.annual_discount_banner",
394
- desc: "Promote the annual-plan discount in the upgrade flow.",
395
- on: true },
396
- { key: "billing.proration_preview",
397
- desc: "Preview the prorated charge before confirming a plan change.",
398
- on: false, requires: "billing.usage_meter" },
399
- { key: "billing.dunning_emails_v3",
400
- desc: "Use the rewritten dunning sequence with a 14-day grace window.",
401
- on: true, rollout: 25 }
402
- ]
403
- },
404
- {
405
- id: "internal",
406
- label: "Internal",
407
- flags: [
408
- { key: "internal.shadow_traffic",
409
- desc: "Mirror 1% of API traffic to the staging cluster for diffing.",
410
- on: false },
411
- { key: "internal.query_tracing",
412
- desc: "Attach OpenTelemetry spans to every Postgres query.",
413
- on: true },
414
- { key: "internal.kill_switch_ui",
415
- desc: "Expose the emergency kill-switch panel in the admin console.",
416
- on: true, requires: "internal.query_tracing" }
417
- ]
418
- }
419
- ];
420
-
421
- // flat lookup, plus a frozen copy of initial state
422
- const FLAGS = {};
423
- const INITIAL = {};
424
- GROUPS.forEach(g => g.flags.forEach(f => {
425
- FLAGS[f.key] = f;
426
- INITIAL[f.key] = f.on;
427
- }));
428
-
429
- // ---------------------------------------------------------------------------
430
- // render the form once; state lives in the checkboxes
431
- // ---------------------------------------------------------------------------
432
-
433
- const formCol = document.getElementById("formCol");
434
- const banner = document.getElementById("banner");
435
- const bannerTxt = document.getElementById("bannerText");
436
- const sideCount = document.getElementById("sideCount");
437
- const diffEl = document.getElementById("diff");
438
- const copyDiffBtn = document.getElementById("copyDiff");
439
- const copyJsonBtn = document.getElementById("copyJson");
440
- const resetBtn = document.getElementById("reset");
441
-
442
- function esc(s) {
443
- return String(s).replace(/[&<>"]/g, c =>
444
- ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
445
- }
446
-
447
- function buildForm() {
448
- formCol.innerHTML = "";
449
- for (const g of GROUPS) {
450
- const panel = document.createElement("section");
451
- panel.className = "group";
452
- panel.dataset.group = g.id;
453
-
454
- const head = document.createElement("div");
455
- head.className = "group-head";
456
- head.innerHTML =
457
- '<span class="name">' + esc(g.label) + '</span>' +
458
- '<span class="meta" data-meta></span>';
459
- panel.appendChild(head);
460
-
461
- for (const f of g.flags) {
462
- const row = document.createElement("div");
463
- row.className = "flag";
464
- row.dataset.key = f.key;
465
-
466
- const rolloutBadge = (typeof f.rollout === "number")
467
- ? '<span class="rollout">' + f.rollout + '%</span>'
468
- : "";
469
-
470
- const reqChip = f.requires
471
- ? '<div class="req-chip"><span class="warn-glyph">&#9888;</span>' +
472
- '<span>requires ' + esc(f.requires) + '</span></div>'
473
- : "";
474
-
475
- row.innerHTML =
476
- '<label class="toggle">' +
477
- '<input type="checkbox" data-key="' + esc(f.key) + '"' +
478
- (f.on ? " checked" : "") +
479
- ' aria-label="' + esc(f.key) + '">' +
480
- '<span class="track"></span>' +
481
- '</label>' +
482
- '<div class="flag-info">' +
483
- '<div class="flag-key"><span class="dot"></span>' +
484
- '<span>' + esc(f.key) + '</span>' + rolloutBadge +
485
- '</div>' +
486
- '<div class="flag-desc">' + esc(f.desc) + '</div>' +
487
- reqChip +
488
- '</div>';
489
-
490
- panel.appendChild(row);
491
- }
492
- formCol.appendChild(panel);
493
- }
494
- }
495
-
496
- // ---------------------------------------------------------------------------
497
- // state helpers
498
- // ---------------------------------------------------------------------------
499
-
500
- function currentState() {
501
- const state = {};
502
- formCol.querySelectorAll("input[type=checkbox]").forEach(cb => {
503
- state[cb.dataset.key] = cb.checked;
504
- });
505
- return state;
506
- }
507
-
508
- function computeDiff(state) {
509
- const lines = [];
510
- for (const g of GROUPS) {
511
- for (const f of g.flags) {
512
- if (state[f.key] !== INITIAL[f.key]) {
513
- lines.push({ key: f.key, from: INITIAL[f.key], to: state[f.key] });
514
- }
515
- }
516
- }
517
- return lines;
518
- }
519
-
520
- function diffText(lines) {
521
- return lines.map(l =>
522
- '- "' + l.key + '": ' + l.from + '\n' +
523
- '+ "' + l.key + '": ' + l.to
524
- ).join("\n");
525
- }
526
-
527
- function fullJson(state) {
528
- // grouped, ordered, pretty-printed — what you'd actually paste into the file
529
- let out = "{\n";
530
- GROUPS.forEach((g, gi) => {
531
- g.flags.forEach((f, fi) => {
532
- const last = gi === GROUPS.length - 1 && fi === g.flags.length - 1;
533
- out += ' "' + f.key + '": ' + state[f.key] + (last ? "" : ",") + "\n";
534
- });
535
- });
536
- out += "}";
537
- return out;
538
- }
539
-
540
- // ---------------------------------------------------------------------------
541
- // the live update pass
542
- // ---------------------------------------------------------------------------
543
-
544
- function update() {
545
- const state = currentState();
546
- let warnCount = 0;
547
-
548
- // per-row decorations
549
- for (const g of GROUPS) {
550
- let onCount = 0;
551
- for (const f of g.flags) {
552
- const row = formCol.querySelector('.flag[data-key="' + CSS.escape(f.key) + '"]');
553
- const on = state[f.key];
554
- if (on) onCount++;
555
-
556
- const changed = on !== INITIAL[f.key];
557
- row.classList.toggle("changed", changed);
558
-
559
- let warn = false;
560
- if (f.requires && on && !state[f.requires]) warn = true;
561
- row.classList.toggle("warn", warn);
562
- if (warn) warnCount++;
563
- }
564
- const meta = formCol.querySelector(
565
- '.group[data-group="' + g.id + '"] [data-meta]');
566
- meta.textContent = g.flags.length + " flags, " + onCount + " on";
567
- }
568
-
569
- // diff + sidebar
570
- const lines = computeDiff(state);
571
- if (lines.length === 0) {
572
- diffEl.innerHTML = '<span class="empty">// no changes yet</span>';
573
- copyDiffBtn.disabled = true;
574
- } else {
575
- diffEl.innerHTML = lines.map(l =>
576
- '<span class="minus">- "' + esc(l.key) + '": ' + l.from + '</span>\n' +
577
- '<span class="plus">+ "' + esc(l.key) + '": ' + l.to + '</span>'
578
- ).join("\n");
579
- copyDiffBtn.disabled = false;
580
- }
581
-
582
- sideCount.innerHTML =
583
- lines.length + " changed" +
584
- '<span class="sep">&middot;</span>' +
585
- '<span class="' + (warnCount ? "warn-n" : "") + '">' +
586
- warnCount + " warning" + (warnCount === 1 ? "" : "s") +
587
- "</span>";
588
-
589
- // banner
590
- if (warnCount > 0) {
591
- bannerTxt.textContent =
592
- warnCount === 1
593
- ? "1 flag is enabled without its prerequisite"
594
- : warnCount + " flags are enabled without their prerequisites";
595
- banner.classList.add("show");
596
- } else {
597
- banner.classList.remove("show");
598
- }
599
- }
600
-
601
- // ---------------------------------------------------------------------------
602
- // clipboard + buttons
603
- // ---------------------------------------------------------------------------
604
-
605
- function flash(btn, label) {
606
- const orig = btn.textContent;
607
- btn.textContent = label;
608
- btn.classList.add("copied");
609
- btn.disabled = true;
610
- setTimeout(() => {
611
- btn.textContent = orig;
612
- btn.classList.remove("copied");
613
- btn.disabled = false;
614
- update(); // restore disabled state on copyDiff if needed
615
- }, 1200);
616
- }
617
-
618
- function writeClipboard(text) {
619
- if (navigator.clipboard && navigator.clipboard.writeText) {
620
- return navigator.clipboard.writeText(text);
621
- }
622
- // fallback for file:// contexts without clipboard permission
623
- const ta = document.createElement("textarea");
624
- ta.value = text;
625
- ta.style.position = "fixed";
626
- ta.style.left = "-9999px";
627
- document.body.appendChild(ta);
628
- ta.select();
629
- try { document.execCommand("copy"); } catch (e) { /* ignore */ }
630
- document.body.removeChild(ta);
631
- return Promise.resolve();
632
- }
633
-
634
- copyDiffBtn.addEventListener("click", () => {
635
- const lines = computeDiff(currentState());
636
- if (!lines.length) return;
637
- writeClipboard(diffText(lines)).then(() => flash(copyDiffBtn, "Copied ✓"));
638
- });
639
-
640
- copyJsonBtn.addEventListener("click", () => {
641
- writeClipboard(fullJson(currentState())).then(() => flash(copyJsonBtn, "Copied ✓"));
642
- });
643
-
644
- resetBtn.addEventListener("click", () => {
645
- formCol.querySelectorAll("input[type=checkbox]").forEach(cb => {
646
- cb.checked = INITIAL[cb.dataset.key];
647
- });
648
- update();
649
- });
650
-
651
- formCol.addEventListener("change", e => {
652
- if (e.target.matches("input[type=checkbox]")) update();
653
- });
654
-
655
- // ---------------------------------------------------------------------------
656
- // boot
657
- // ---------------------------------------------------------------------------
658
-
659
- buildForm();
660
- update();
661
- </script>
662
- </body>
663
- </html>