roast-my-design-system 3.5.0 → 3.6.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/README.md CHANGED
@@ -24,11 +24,11 @@ Your AI agent (Claude, Cursor, Copilot) builds UI by imitating what's already in
24
24
 
25
25
  The full report for vercel/ai-chatbot, top to bottom:
26
26
 
27
- ![The full diagnosis report for vercel/ai-chatbot in dark mode: health score, three-yardstick tiles, palette forensics, spacing receipts, typography, offenders, duplicates, and the where-to-start close](https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/report-full-dark.png?v=3.5.0)
27
+ ![The full diagnosis report for vercel/ai-chatbot in dark mode: health score, three-yardstick tiles, palette forensics, spacing receipts, typography, offenders, duplicates, and the where-to-start close](https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/report-full-dark.png?v=3.6.0)
28
28
 
29
29
  The same report in light mode (one file, built-in toggle):
30
30
 
31
- ![The diagnosis report in light mode](https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/report-light-hero.png?v=3.5.0)
31
+ ![The diagnosis report in light mode](https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/report-light-hero.png?v=3.6.0)
32
32
 
33
33
  ## What makes the numbers trustworthy
34
34
 
@@ -127,4 +127,4 @@ Yes, the median repo is already a mess. That's the point.
127
127
 
128
128
  MIT
129
129
 
130
- Built and designed by <a href="https://gregkozakiewicz.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/gk-mark-dark.png?v=3.5.0"><img src="https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/gk-mark.png?v=3.5.0" height="15" alt="GK mark"></picture> Greg Kozakiewicz</a>.
130
+ Built and designed by <a href="https://gregkozakiewicz.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/gk-mark-dark.png?v=3.6.0"><img src="https://raw.githubusercontent.com/pencilrebel/roast-my-design-system/main/assets/gk-mark.png?v=3.6.0" height="15" alt="GK mark"></picture> Greg Kozakiewicz</a>.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roast-my-design-system",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Roast your design system with real data. A deterministic, zero-dependency scanner that counts every colour, grey, spacing value, typeface, duplicated component, inline style and arbitrary Tailwind value in your repo, scores it 0-100 against Ideal Design System norms and a 30-repo benchmark, generates a shareable HTML diagnosis, and with --rules writes the agent rules file (CLAUDE.md, .cursor/rules) that stops the mess coming back.",
5
5
  "keywords": [
6
6
  "design-system",
@@ -12,6 +12,7 @@ import { resolve, basename, join, dirname } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { distinctTypefaces } from '../lib/typefaces.mjs';
14
14
  import { rulesMarkdown } from '../rules/build.mjs';
15
+ import { nearColorPairs } from '../lib/nearpairs.mjs';
15
16
 
16
17
  // The benchmark (Ideal-2026 norms + scanned-repo stats) ships next to the
17
18
  // code so the page works offline; degrade gracefully when absent.
@@ -34,7 +35,7 @@ const GK_MASK = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqa
34
35
  const GK_MARK = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAACXBIWXMAAAAAAAAAAQCEeRdzAAACPklEQVR4nOVWTYh5URR/PlJYEKV8LKwkZalsmI2dZmMnK8pKmaZ87JSanexEkayUhSxsZyfJNBQ7UYqVhSShfM+v/6mXmtU81+p/Frfze+/d3++ce88993Hcf2cikehZ1GazWafTPUVDLBZjLBQKnU6HMTVvcrl8Op3ebrdsNgsokUiYUVP4Pp/v9s96vd79cwZGK95sNkkgGAwC2u12lhrY3u12C/b1ei2TyV5eXjabjV6vZ6BBa51MJin8arUKWKlU4H99fSkUCo5JUQ2HQxJwuVzIYLVawcdIVSs8CQoNpMQ+Go0Aw+EwwXw+D+h2u4Vr0LRSqUSMqVQKsN1uE3Q4HEql8ng8BgIBTnDhqlSqxWIBuvP5bDAYLBbL5XIBHAwGeBuJRODvdjun0/nnPOjraDRK8TYaDcBMJkMwFosBfn9/E0wkEn9OgjYgnU6fTidQwAGcTCbw9/u9VqvFUYB/vV5RskajkRNcTiaTKR6PI9j5fE7xlstlPM/lcgTr9ToneJ/5oOB4PB70O8Tb7/f9fv9sNqPkXl9fhQsQ9f1ktVqNvUVCxA6ZRxsf5pMAxnsuVBSOwvv7+6Phk0PsBNn0OOLC9haLReqdvBL/wUNKNPnt7Q0LjeVutVqhUEij0TwU9W8BVA4JUEUul8tarcZGhpbIarWiSeA0HQ4Hkvn4+GB56VMvov6DA+z1eplR89btdsE+Ho9tNhv3jOv+8/MTFz06M3ypVMqMnTfcwPhn4djG/tue+Mf4RGom9gMt6lAx16huIwAAAABJRU5ErkJggg==';
35
36
 
36
37
  // Shown in the report footer; keep in step with plugin.json when releasing.
37
- const VERSION = '3.5.0';
38
+ const VERSION = '3.6.0';
38
39
 
39
40
  // Two shipped skins, same layout: 'dark' (navy glass, mint accent) and
40
41
  // 'light' (lilac wash, white glass, violet accent). --theme picks one.
@@ -151,6 +152,22 @@ const greyStrays = greys.filter((c) => !c.isToken).length;
151
152
  const arbitrary = h.tokens.tailwind?.arbitrary ?? [];
152
153
  const arbitraryCount = arbitrary.reduce((sum, a) => sum + a.count, 0);
153
154
 
155
+ // !important declarations: the cascade admitting defeat.
156
+ const important = h.tokens.important ?? { count: 0, files: [] };
157
+
158
+ const nearPairs = nearColorPairs(colors);
159
+
160
+ // Components defined but never imported — scoped to the design-system
161
+ // directory, where "nobody imports this" is meaningful. File-local
162
+ // subcomponents elsewhere legitimately never get imported by name.
163
+ const dsDirRe = h.profile?.uiDir
164
+ ? new RegExp(`^${h.profile.uiDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`)
165
+ : /(^|\/)(packages\/ui|design-system|ui-kit)\//;
166
+ const usedFiles = new Set(reusable.filter((c) => c.usageCount > 0).map((c) => c.file));
167
+ const neverImported = reusable.filter((c) => !c.usageCount && dsDirRe.test(c.file)
168
+ && !/(^|\/)icons?\//.test(c.file) // glyph sets are deliberately complete
169
+ && !usedFiles.has(c.file)); // compound API: an adopted file's sub-exports are surface, not dead
170
+
154
171
  // Benchmark helpers: for a metric value, where does this repo sit among the
155
172
  // scanned fleet? ("more colours than 90% of scanned repos")
156
173
  function percentile(metric, value) {
@@ -204,6 +221,8 @@ if (effGreys > 13 * 1.25) candidates.push({ ratio: effGreys / 13, text: tokenLed
204
221
  ? `${greyStrays} hardcoded greys outside the token set`
205
222
  : `${greys.length} shades of grey doing the job of 13` });
206
223
  if (spacingTotal > 15) candidates.push({ ratio: spacingTotal / 12, text: `${spacingTotal} off-scale spacing values where a dozen would do` });
224
+ if (nearPairs.length >= 3) candidates.push({ ratio: 1 + nearPairs.length / 6, text: `${nearPairs.length} colour pairs are nearly identical (${nearPairs[0].a.value} next to ${nearPairs[0].b.value}), copy-paste, not decisions` });
225
+ if (important.count >= 10) candidates.push({ ratio: 1 + important.count / 30, text: `${n(important.count)} !important declarations, the cascade admitting defeat` });
207
226
  if (iconCollisions.length >= 5) candidates.push({ ratio: 1 + iconCollisions.length / 8, text: `two icon sets collide on ${iconCollisions.length} names` });
208
227
  if (hardDupes.length > 0) candidates.push({ ratio: 1 + hardDupes.length / 5, text: `${hardDupes.length} component${hardDupes.length > 1 ? 's' : ''} implemented more than once` });
209
228
  if (inline.count > 20) candidates.push({ ratio: inline.count / 20, text: `${n(inline.count)} inline style blocks bypassing every system` });
@@ -361,6 +380,9 @@ function paletteSection() {
361
380
  </div>
362
381
  <div class="usage-bar">${cells}${restCell}</div>
363
382
  ${ramp}
383
+ ${nearPairs.length ? `
384
+ <div class="receipts">${eyebrow(`${nearPairs.length} nearly identical pair${nearPairs.length === 1 ? '' : 's'} · copy-paste, not decisions`)}
385
+ <div class="chips-row">${nearPairs.slice(0, 8).map((pr) => `<span class="vchip" title="every channel within ${pr.d} of its twin"><i class="ndot" style="background:${esc(pr.a.value)}"></i>${esc(pr.a.value)} ×${pr.a.count} <b class="nsim">≈</b> <i class="ndot" style="background:${esc(pr.b.value)}"></i>${esc(pr.b.value)} ×${pr.b.count}</span>`).join('')}${nearPairs.length > 8 ? `<span class="vchip dim">+${nearPairs.length - 8} more</span>` : ''}</div></div>` : ''}
364
386
  </div>
365
387
  </section>`;
366
388
  }
@@ -457,20 +479,24 @@ function offendersSection() {
457
479
  }
458
480
 
459
481
  function inlineSection() {
460
- if (inline.count < 5) return '';
482
+ if (inline.count < 5 && important.count < 5) return '';
461
483
  const max = Math.max(...inline.files.slice(0, 6).map((f) => f.count), 1);
462
- return `<div class="glass pad half">
463
- ${sectionHead(`${n(inline.count)} inline style blocks`, 'styling no system can see')}
464
- <div class="fam-rows">${inline.files.slice(0, 6).map((f) => `
484
+ const importantRow = important.count ? `
485
+ <div class="receipts">${eyebrow(`${n(important.count)} !important declaration${important.count === 1 ? '' : 's'} · the cascade admitting defeat`)}
486
+ <div class="chips-row">${important.files.map((f) => `<span class="vchip bad" title="${esc(f.file)}">${esc(basename(f.file))} ×${f.count}</span>`).join('')}</div></div>` : '';
487
+ const blocks = inline.count >= 5 ? `<div class="fam-rows">${inline.files.slice(0, 6).map((f) => `
465
488
  <div class="mini-card col">
466
489
  <div class="mc-row">${fileLink(f.file)}<span class="mc-count">×${f.count}</span></div>
467
490
  <div class="mc-track"><div class="mc-bar" style="width:${Math.round((f.count / max) * 100)}%"></div></div>
468
- </div>`).join('')}</div></div>`;
491
+ </div>`).join('')}</div>` : '';
492
+ return `<div class="glass pad half">
493
+ ${sectionHead(inline.count >= 5 ? `${n(inline.count)} inline style blocks` : `${n(important.count)} !important declarations`, 'styling no system can see')}
494
+ ${blocks}${importantRow}</div>`;
469
495
  }
470
496
 
471
497
  function componentsSection() {
472
498
  const top = reusable.filter((c) => c.usageCount > 0).slice(0, 10);
473
- if (!top.length) return '';
499
+ if (!top.length && neverImported.length < 2) return '';
474
500
  const rows = top.map((c) => `
475
501
  <tr><td class="mono strong">&lt;${esc(c.name)}&gt;</td>
476
502
  <td><span class="pill pill-mint">${c.usageCount}×</span></td>
@@ -478,7 +504,11 @@ function componentsSection() {
478
504
  <td>${c.propsHint?.named?.length ? c.propsHint.named.slice(0, 5).map((p) => `<span class="chip chip-xs">${esc(p)}</span>`).join(' ') : '<span class="dim">none</span>'}</td></tr>`).join('');
479
505
  return `<section class="glass pad">
480
506
  ${sectionHead('What you actually use', `${n(reusable.length)} components defined · top by adoption · the real system, buried in here`)}
481
- <div class="tbl-wrap"><table><thead><tr><th>component</th><th>used</th><th>defined in</th><th>props</th></tr></thead><tbody>${rows}</tbody></table></div></section>`;
507
+ ${top.length ? `<div class="tbl-wrap"><table><thead><tr><th>component</th><th>used</th><th>defined in</th><th>props</th></tr></thead><tbody>${rows}</tbody></table></div>` : ''}
508
+ ${neverImported.length >= 2 ? `
509
+ <div class="receipts">${eyebrow(`${n(neverImported.length)} components defined but never imported · candidates for deletion, or the system nobody found`)}
510
+ <div class="chips-row">${neverImported.slice(0, 8).map((c) => `<span class="vchip" title="${esc(c.file)}">&lt;${esc(c.name)}&gt;</span>`).join('')}${neverImported.length > 8 ? `<span class="vchip dim">+${neverImported.length - 8} more</span>` : ''}</div>
511
+ <p class="sub" style="margin-top:8px">Routers, dynamic imports and barrel files can hide real usage, so treat this as a shortlist to check, not a demolition order.</p>` : ''}</section>`;
482
512
  }
483
513
 
484
514
  // "Where to start" — at most three moves, every one derived from this repo's
@@ -830,17 +860,24 @@ const html = `<!doctype html>
830
860
  footer .brand { color:var(--accent); font-weight:700; text-decoration:none; }
831
861
  footer .brand:hover { text-decoration:underline; }
832
862
  footer .creds { display:flex; gap:16px; font:700 9.5px/1.6 var(--sans); letter-spacing:.16em; text-transform:uppercase; color:var(--dim2); }
863
+ .ndot { display:inline-block; width:9px; height:9px; border-radius:3px; margin-right:4px; vertical-align:-1px; box-shadow:inset 0 0 0 1px var(--cell-ring); }
864
+ .nsim { color:var(--dim2); font-weight:400; padding:0 2px; }
833
865
  .gift-sec { margin-top:16px; }
834
866
  .gift-stage { position:relative; display:grid; place-items:center; min-height:120px; }
835
867
  .gift-stage.open { min-height:0; }
836
868
  .gift[hidden] { display:none; }
837
- .gift { background:none; border:0; cursor:pointer; display:grid; place-items:center; gap:9px; padding:14px; color:var(--dim);
869
+ .gift { position:relative; background:none; border:0; cursor:pointer; display:grid; place-items:center; gap:9px; padding:14px; color:var(--dim);
838
870
  transition:transform .25s ease, opacity .25s ease, filter .25s ease; }
839
871
  .gift svg { width:72px; height:72px; }
840
872
  .gift .ribbon { stroke:var(--accent); }
841
873
  .gift:hover { transform:translateY(-3px) scale(1.04); color:var(--text); }
842
874
  .gift.gone { transform:scale(.5); opacity:0; filter:blur(5px); }
843
875
  .gift-hint { font:700 10px/1 var(--sans); letter-spacing:.18em; text-transform:uppercase; }
876
+ .gift::after { content:'your agent rules file, generated from this scan'; position:absolute; top:100%; left:50%;
877
+ transform:translateX(-50%); white-space:nowrap; font:400 11.5px/1 var(--sans); letter-spacing:0; text-transform:none;
878
+ color:var(--dim2); opacity:0; transition:opacity .3s ease .6s; pointer-events:none; }
879
+ .gift:hover::after { opacity:1; }
880
+ .gift.gone::after { opacity:0; transition:none; }
844
881
  .gift-pop { position:absolute; inset:0; pointer-events:none; }
845
882
  .gift-pop i { position:absolute; left:50%; top:42%; width:7px; height:10px; border-radius:2px; opacity:0;
846
883
  animation:gpop .85s cubic-bezier(.16,.8,.32,1) forwards; }
@@ -129,6 +129,8 @@ export function harvestTokens(root, styleFiles, codeFiles) {
129
129
  const twArbitrary = new Tally();
130
130
  let inlineStyleCount = 0;
131
131
  const inlineStyleFiles = new Map();
132
+ // !important is the cascade admitting defeat; counted per style file.
133
+ const importantFiles = new Map();
132
134
 
133
135
  // Colours that appear in a CSS custom-property DEFINITION (--grey-100: #f5f5f5)
134
136
  // are deliberate tokens; everything else is a hardcoded stray. The diagnosis
@@ -137,6 +139,8 @@ export function harvestTokens(root, styleFiles, codeFiles) {
137
139
  const tokenDefsPerFile = new Map();
138
140
 
139
141
  const scanCssText = (text, file) => {
142
+ const imp = (text.match(/!\s*important/gi) ?? []).length;
143
+ if (imp) importantFiles.set(file, (importantFiles.get(file) ?? 0) + imp);
140
144
  for (const m of text.matchAll(/--[\w-]+\s*:\s*([^;{}]+)[;}]/g)) {
141
145
  tokenDefsPerFile.set(file, (tokenDefsPerFile.get(file) ?? 0) + 1);
142
146
  for (const c of m[1].matchAll(HEX_RE)) tokenDefined.add(normalizeHex(c[0]));
@@ -303,6 +307,11 @@ export function harvestTokens(root, styleFiles, codeFiles) {
303
307
  textSizes: twTextSizes.toJSON(),
304
308
  arbitrary: twArbitrary.toJSON(),
305
309
  },
310
+ important: {
311
+ count: [...importantFiles.values()].reduce((a, b) => a + b, 0),
312
+ files: [...importantFiles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6)
313
+ .map(([file, count]) => ({ file, count })),
314
+ },
306
315
  inlineStyles: {
307
316
  count: inlineStyleCount,
308
317
  files: [...inlineStyleFiles.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10)
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Near-identical colour pairs (#f5f5f5 next to #f6f6f6): copy-paste, not
3
+ * decisions. Hex-parseable colours only; a pair is "near" when every channel
4
+ * sits within 8 of its twin and alpha matches. Two TOKENS sitting close are a
5
+ * designed ramp (every good grey scale has near neighbours) and are skipped;
6
+ * drift needs at least one hardcoded stray. Shared by diagnose and rules.
7
+ */
8
+ export function hexRgb(v) {
9
+ const m = /^#([0-9a-f]{3,8})$/i.exec(v);
10
+ if (!m) return null;
11
+ let x = m[1];
12
+ if (x.length === 3 || x.length === 4) x = [...x].map((c) => c + c).join('');
13
+ if (x.length !== 6 && x.length !== 8) return null;
14
+ return { r: parseInt(x.slice(0, 2), 16), g: parseInt(x.slice(2, 4), 16), b: parseInt(x.slice(4, 6), 16),
15
+ a: x.length === 8 ? x.slice(6, 8) : 'ff' };
16
+ }
17
+
18
+ export function nearColorPairs(colorList) {
19
+ const hexes = colorList.map((c) => ({ ...c, rgb: hexRgb(c.value) })).filter((c) => c.rgb);
20
+ const pairs = [];
21
+ for (let i = 0; i < hexes.length; i++) {
22
+ for (let j = i + 1; j < hexes.length; j++) {
23
+ const a = hexes[i].rgb, b = hexes[j].rgb;
24
+ if (a.a !== b.a) continue;
25
+ if (hexes[i].isToken && hexes[j].isToken) continue;
26
+ const d = Math.max(Math.abs(a.r - b.r), Math.abs(a.g - b.g), Math.abs(a.b - b.b));
27
+ if (d > 0 && d <= 8) pairs.push({ a: hexes[i], b: hexes[j], d });
28
+ }
29
+ }
30
+ return pairs.sort((x, y) => (y.a.count + y.b.count) - (x.a.count + x.b.count));
31
+ }
@@ -5,6 +5,7 @@
5
5
  * with a receipt from the scanned repo.
6
6
  */
7
7
  import { distinctTypefaces } from '../lib/typefaces.mjs';
8
+ import { nearColorPairs } from './../lib/nearpairs.mjs';
8
9
 
9
10
  export function rulesMarkdown(h) {
10
11
  const repoName = h.profile?.name ?? 'this repo';
@@ -36,6 +37,8 @@ const repoName = h.profile?.name ?? 'this repo';
36
37
  } else if ((t.colors ?? []).length) {
37
38
  rule(`There is no token file yet. Until one exists, reuse the colours already in the codebase instead of introducing new ones (${t.colors.length} distinct colours are already in play).`);
38
39
  }
40
+ const near = nearColorPairs(t.colors ?? []);
41
+ if (near.length) rule(`Never eyeball a colour from memory: the scan found ${near.length} nearly identical pair${near.length === 1 ? '' : 's'} (like ${near[0].a.value} next to ${near[0].b.value}). Look the exact value up, or better, use its token.`);
39
42
  const ds = h.profile?.designSystem;
40
43
  if (ds?.kind && !['none', 'custom'].includes(ds.kind) && ds.confidence !== 'low') {
41
44
  rule(`This repo uses ${ds.name ?? ds.kind}${h.profile.uiDir ? `; its components live in \`${h.profile.uiDir}\`` : ''}. Prefer extending it over building parallel pieces.`);
@@ -84,7 +87,20 @@ const repoName = h.profile?.name ?? 'this repo';
84
87
  if (icons.length) rule(`Two icon sets collide on ${icons.length} name${icons.length === 1 ? '' : 's'}. Before adding any icon, check which set the surrounding file already imports and stay with it.`);
85
88
  }
86
89
 
87
- // ---------- spacing ----------
90
+ // ---------- never-imported components ----------
91
+ const dsDirRe = h.profile?.uiDir
92
+ ? new RegExp(`^${h.profile.uiDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`)
93
+ : /(^|\/)(packages\/ui|design-system|ui-kit)\//;
94
+ const usedFiles = new Set(reusable.filter((c) => c.usageCount > 0).map((c) => c.file));
95
+ const neverImported = reusable.filter((c) => !c.usageCount && dsDirRe.test(c.file)
96
+ && !/(^|\/)icons?\//.test(c.file) // glyph sets are deliberately complete
97
+ && !usedFiles.has(c.file)); // compound API: an adopted file's sub-exports are surface, not dead
98
+ if (neverImported.length >= 3) {
99
+ section('Components nobody imports');
100
+ rule(`${neverImported.length} components are defined but never imported (${neverImported.slice(0, 3).map((c) => `\`<${c.name}>\``).join(', ')}…). Before writing any new component, check this list first; adopt one or flag it for deletion instead of adding another.`);
101
+ }
102
+
103
+ // ---------- spacing ----------
88
104
  const arbitrary = t.tailwind?.arbitrary ?? [];
89
105
  const arbCount = arbitrary.reduce((s, a) => s + a.count, 0);
90
106
  const offScale = (t.spacing ?? []).length;
@@ -106,6 +122,7 @@ const repoName = h.profile?.name ?? 'this repo';
106
122
  section('Styling discipline');
107
123
  rule(`Never write ${reusable.length ? '`style={{ ... }}`' : 'inline `style="..."` attributes'} for static values; styling belongs to classes and tokens where the system can see it.`);
108
124
  if ((t.inlineStyles?.count ?? 0) > 0) lines.push(` (${t.inlineStyles.count} static inline block${t.inlineStyles.count === 1 ? '' : 's'} already exist; do not add to them.)`);
125
+ if ((t.important?.count ?? 0) >= 3) rule(`Never write !important; the scan found ${t.important.count} declaration${t.important.count === 1 ? '' : 's'} already. When a style does not apply, fix the selector or the source of the conflict instead of shouting over it.`);
109
126
  rule('Before styling anything new, look at a neighbouring component and match how it does it. Consistency with the repo beats personal preference.');
110
127
 
111
128
  lines.push('');