@hegemonart/get-design-done 1.48.0 → 1.50.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 (70) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +8 -2
  3. package/CHANGELOG.md +93 -0
  4. package/README.md +4 -0
  5. package/SKILL.md +2 -1
  6. package/agents/design-auditor.md +37 -4
  7. package/agents/design-context-builder.md +2 -0
  8. package/agents/design-debt-crawler.md +36 -5
  9. package/agents/design-executor.md +2 -0
  10. package/agents/design-fixer.md +4 -1
  11. package/agents/design-planner.md +2 -0
  12. package/agents/design-reflector.md +2 -0
  13. package/agents/design-research-synthesizer.md +2 -0
  14. package/agents/design-verifier.md +7 -15
  15. package/dist/claude-code/.claude/skills/audit/SKILL.md +1 -1
  16. package/dist/claude-code/.claude/skills/brief/SKILL.md +1 -1
  17. package/dist/claude-code/.claude/skills/compare/SKILL.md +1 -1
  18. package/dist/claude-code/.claude/skills/connections/SKILL.md +1 -1
  19. package/dist/claude-code/.claude/skills/darkmode/SKILL.md +1 -1
  20. package/dist/claude-code/.claude/skills/design/SKILL.md +1 -1
  21. package/dist/claude-code/.claude/skills/discover/SKILL.md +1 -1
  22. package/dist/claude-code/.claude/skills/do/SKILL.md +1 -1
  23. package/dist/claude-code/.claude/skills/explore/SKILL.md +1 -1
  24. package/dist/claude-code/.claude/skills/fast/SKILL.md +1 -1
  25. package/dist/claude-code/.claude/skills/health/SKILL.md +2 -2
  26. package/dist/claude-code/.claude/skills/live/SKILL.md +1 -1
  27. package/dist/claude-code/.claude/skills/new-skill/SKILL.md +90 -0
  28. package/dist/claude-code/.claude/skills/plan/SKILL.md +1 -1
  29. package/dist/claude-code/.claude/skills/progress/SKILL.md +9 -1
  30. package/dist/claude-code/.claude/skills/quick/SKILL.md +1 -1
  31. package/dist/claude-code/.claude/skills/scan/SKILL.md +1 -1
  32. package/dist/claude-code/.claude/skills/ship/SKILL.md +1 -1
  33. package/dist/claude-code/.claude/skills/verify/SKILL.md +1 -1
  34. package/hooks/gdd-design-quality-check.js +340 -0
  35. package/hooks/hooks.json +9 -0
  36. package/package.json +12 -2
  37. package/reference/anti-slop-rubric.md +173 -0
  38. package/reference/audit-scoring.md +4 -0
  39. package/reference/debt-categories.md +20 -1
  40. package/reference/registry.json +28 -0
  41. package/reference/reviewer-confidence-gate.md +108 -0
  42. package/reference/skill-authoring-contract.md +97 -15
  43. package/reference/skill-graph.md +118 -0
  44. package/reference/visual-tells.md +383 -0
  45. package/scripts/lib/confidence-route.cjs +60 -0
  46. package/scripts/lib/manifest/scaffolder.cjs +261 -0
  47. package/scripts/lib/manifest/schemas/skills.schema.json +14 -0
  48. package/scripts/lib/manifest/skills.json +26 -18
  49. package/scripts/lib/worktree-resolve.cjs +221 -0
  50. package/sdk/mcp/gdd-state/server.js +37 -4
  51. package/sdk/mcp/gdd-state/tools/shared.ts +61 -0
  52. package/skills/audit/SKILL.md +1 -1
  53. package/skills/brief/SKILL.md +1 -1
  54. package/skills/compare/SKILL.md +1 -1
  55. package/skills/connections/SKILL.md +1 -1
  56. package/skills/darkmode/SKILL.md +1 -1
  57. package/skills/design/SKILL.md +1 -1
  58. package/skills/discover/SKILL.md +1 -1
  59. package/skills/do/SKILL.md +1 -1
  60. package/skills/explore/SKILL.md +1 -1
  61. package/skills/fast/SKILL.md +1 -1
  62. package/skills/health/SKILL.md +2 -2
  63. package/skills/live/SKILL.md +1 -1
  64. package/skills/new-skill/SKILL.md +90 -0
  65. package/skills/plan/SKILL.md +1 -1
  66. package/skills/progress/SKILL.md +9 -1
  67. package/skills/quick/SKILL.md +1 -1
  68. package/skills/scan/SKILL.md +1 -1
  69. package/skills/ship/SKILL.md +1 -1
  70. package/skills/verify/SKILL.md +1 -1
@@ -0,0 +1,383 @@
1
+ # Visual Tells Catalog (v2)
2
+
3
+ The default-AI aesthetic has a fingerprint. When a model generates a front-end
4
+ without a brand brief, it falls back to the same handful of moves it saw most in
5
+ its training set. This catalog names those moves so the cheap regex floor in
6
+ `hooks/gdd-design-quality-check.js` can flag them on every `.tsx` / `.vue` /
7
+ `.svelte` / `.astro` write.
8
+
9
+ This catalog grows commit by commit. The v1 floor named the eight loudest tells the
10
+ hook matches today; v2 adds five more identified through hands-on usage. Newer
11
+ categories may be catalog-only (documented and cross-linked, not yet wired to a hook
12
+ rule) until a regex earns its place. Each entry below names a **primary axis** from
13
+ `reference/anti-slop-rubric.md`, so a tell connects to the verb axis it most degrades.
14
+
15
+ Severity here is WARN (advisory), in the vocabulary of `reference/audit-scoring.md`.
16
+ A WARN is not a FLAG: it does not block the write and it does not fail a gate. It
17
+ is a nudge that asks "did you choose this, or did the model default to it?" Each
18
+ category below maps 1:1 to a rule id in the hook. For the harder bans behind some
19
+ of these tells, see `reference/anti-patterns.md` (BAN-NN and SLOP-NN).
20
+
21
+ How to read each entry: a short description, three to five concrete instances of
22
+ the tell in the wild, the diagnostic regex the hook runs (where one applies), and
23
+ a remediation pattern you can paste in.
24
+
25
+ ---
26
+
27
+ ## default-AI-hero
28
+
29
+ Rule id: `generic-cta`
30
+
31
+ The stock landing-page hero: a centered headline, one line of filler subtext, and
32
+ a button that says "Get Started". The copy carries no subject and no verb specific
33
+ to the product. It reads like every template because it is every template.
34
+
35
+ Instances:
36
+
37
+ 1. Button label "Get Started" with no object ("Get started with what?").
38
+ 2. Headline opener "Welcome to [Product]" instead of a value statement.
39
+ 3. "Learn More" as the secondary call to action, pointing nowhere specific.
40
+ 4. "Lorem ipsum" body copy shipped past the mockup stage.
41
+ 5. The triplet subhead with no verb (see SLOP-11 in `reference/anti-patterns.md`).
42
+
43
+ Diagnostic regex (hook): `\b(?:Get Started|Welcome to|Lorem ipsum|Learn More)\b`
44
+ (case-insensitive, word-boundaried).
45
+
46
+ Remediation: write the specific promise and the specific next step. "Start a free
47
+ trial" beats "Get Started". "Ship your first audit in ten minutes" beats "Welcome
48
+ to GDD". Name the noun and the verb. Delete placeholder copy before review.
49
+
50
+ ---
51
+
52
+ ## gradient-spam
53
+
54
+ Rule id: `gradient-spam`
55
+
56
+ One tasteful gradient can anchor a page. Three or more on a single screen reads as
57
+ decoration standing in for hierarchy. The model reaches for `bg-gradient-to-r` on
58
+ the hero, again on the cards, again on the footer, because gradients look busy and
59
+ busy looks "designed".
60
+
61
+ Instances:
62
+
63
+ 1. Hero background, card backgrounds, and a CTA all using a direction gradient.
64
+ 2. `bg-gradient-to-br` on every feature tile in a grid.
65
+ 3. A gradient on text plus a gradient on its container (double application).
66
+ 4. Gradient borders faked with a gradient background and an inset child.
67
+
68
+ Diagnostic regex (hook): `\bbg-gradient-to-(?:r|br|tr|b|bl|l|tl|t)\b`, flagged at
69
+ a count of three or more occurrences in one file.
70
+
71
+ Remediation: pick one surface to carry a gradient and make the rest solid. Use
72
+ weight, size, and spacing for hierarchy instead of color washes. If you keep a
73
+ gradient, give it a documented role (one hero, one accent) rather than spraying it.
74
+ For the gradient-text ban specifically, see BAN-02.
75
+
76
+ ---
77
+
78
+ ## isometric-illustration-fallback
79
+
80
+ Rule id: `isometric-illustration-fallback`
81
+
82
+ Pastel isometric scenes with floating icons, usually pulled straight from a free
83
+ clip-art set. The undraw.co style is the strongest tell: flat shapes, two-tone
84
+ palette, no brand character. It signals that no real screenshot or photography was
85
+ available, so a stock scene filled the hole.
86
+
87
+ Instances:
88
+
89
+ 1. `src="/illustrations/undraw_dashboard.svg"` in an empty state.
90
+ 2. An `isometric-hero.png` asset behind the headline.
91
+ 3. A row of undraw spot illustrations as feature icons.
92
+ 4. The same illustration set reused across unrelated products.
93
+
94
+ Diagnostic regex (hook): `\b(?:undraw|isometric)[\w./-]*` (matches the marker in an
95
+ asset path or `src`).
96
+
97
+ Remediation: show the actual product. A real screenshot, a short screen capture,
98
+ or a purpose-drawn illustration with brand character all beat stock clip art. See
99
+ SLOP-12 in `reference/anti-patterns.md` for the longer argument.
100
+
101
+ ---
102
+
103
+ ## centered-everything-syndrome
104
+
105
+ Rule id: `centered-everything-syndrome`
106
+
107
+ `mx-auto` plus `text-center` on block after block. Centered text is fine for a
108
+ single short hero line. Applied to body copy, feature lists, and multi-line cards
109
+ it destroys the reading edge: the eye loses the left margin it scans against, and
110
+ every block competes for the same axis.
111
+
112
+ Instances:
113
+
114
+ 1. A hero, a feature grid, and a testimonial section all centered.
115
+ 2. Centered multi-line paragraphs longer than two lines.
116
+ 3. A centered card with centered heading, centered body, and a centered button.
117
+ 4. Centered form labels above left-aligned inputs (axis mismatch).
118
+
119
+ Diagnostic regex (hook): a quoted class string containing both `mx-auto` and
120
+ `text-center`, in either order.
121
+
122
+ Remediation: center the hero line only. Left-align body copy and lists so they
123
+ share a reading edge. Reserve centering for short, single-line, high-emphasis text.
124
+ Center the container for width control, but left-align its text content.
125
+
126
+ ---
127
+
128
+ ## inter-everything
129
+
130
+ Rule id: `inter-everything`
131
+
132
+ Inter as the default with no documented reason. Inter is a fine typeface, which is
133
+ exactly why it is the safe pick a model makes when no brand font is specified. The
134
+ tell is not Inter itself: it is Inter used alone, with no second font and no token
135
+ that records a choice.
136
+
137
+ Instances:
138
+
139
+ 1. `font-inter` on the root with no display or brand font anywhere.
140
+ 2. `font-family: 'Inter'` in CSS with no second family in the stack file.
141
+ 3. Inter paired with no `--font-display` or `--font-body` token.
142
+ 4. DM Sans, Space Grotesk, or Plus Jakarta Sans in the same role (see SLOP-05).
143
+
144
+ Diagnostic regex (hook): `\bfont-inter\b` or `font-family:\s*['"]?Inter`, warned
145
+ only when no sibling custom-font token (a `font-<name>` utility, a `--font-*`
146
+ variable, or a second `font-family`) appears in the same file.
147
+
148
+ Remediation: choose a typeface you can defend in three sentences against the brand,
149
+ and record it as a token (`--font-display`, `--font-body`). If Inter is the right
150
+ call, pair it with a distinct display face or a deliberate weight system so the
151
+ choice is visible. Keeping the token is what turns a default into a decision.
152
+
153
+ ---
154
+
155
+ ## purple-violet-default
156
+
157
+ Rule id: `purple-violet-default`
158
+
159
+ `bg-purple-600` and `bg-violet-600` are the colors a model picks when no palette is
160
+ given. Combined with indigo and cyan they form the exact accent set that ships on a
161
+ large share of generated UIs (SLOP-01). The tell is the raw Tailwind shade used as
162
+ the brand color with no theme token in sight.
163
+
164
+ Instances:
165
+
166
+ 1. `bg-violet-600` on the primary button with no `bg-primary` token defined.
167
+ 2. `bg-purple-500` headers across the app, hardcoded per component.
168
+ 3. Purple-to-blue accent pairing on hero plus buttons (SLOP-02).
169
+ 4. `text-violet-600` links with no semantic link color.
170
+
171
+ Diagnostic regex (hook): `\bbg-(?:purple|violet)-(?:500|600|700)\b`, warned only
172
+ when no theme-token class (`bg-primary`, `bg-brand`, `bg-accent`, or a
173
+ `bg-[var(--...)]` / `oklch` / `hsl` arbitrary value) is present in the file.
174
+
175
+ Remediation: route color through a token (`bg-primary`, `--color-accent`) so the
176
+ brand hue lives in one place. If purple is genuinely the brand, define it as the
177
+ token and reference the token, not the raw shade. Pick one primary accent and apply
178
+ it consistently. See the Color System rubric in `reference/audit-scoring.md`.
179
+
180
+ ---
181
+
182
+ ## glassmorphism-spam
183
+
184
+ Rule id: `glassmorphism-spam`
185
+
186
+ Frosted-glass panels stacked everywhere: `backdrop-blur` on cards, on the nav, on
187
+ modals, plus `bg-white/10` fills. One blurred overlay over busy content is a valid
188
+ move. Three or more blur or low-alpha-white treatments in one file is glass used as
189
+ the default surface, which hides the fact that no real layout system exists.
190
+
191
+ Instances:
192
+
193
+ 1. `backdrop-blur-lg` on every card in a grid.
194
+ 2. `bg-white/10` panels layered three deep.
195
+ 3. A blurred nav over a blurred hero over a blurred section.
196
+ 4. Glass cards on a flat background where there is nothing to blur.
197
+
198
+ Diagnostic regex (hook): `\bbackdrop-blur(?:-\w+)?\b` or `\bbg-white\/(?:10|20|30)\b`,
199
+ flagged at a count of three or more occurrences in one file.
200
+
201
+ Remediation: keep blur for cases where it has a job, such as a modal dimming the
202
+ content behind it, a floating command palette, or a sticky header over scrolling
203
+ content. Give other surfaces solid fills and a real elevation system. See SLOP-04
204
+ for the valid-use list.
205
+
206
+ ---
207
+
208
+ ## decorative-motion-without-intent
209
+
210
+ Rule id: `decorative-motion-without-intent`
211
+
212
+ `animate-pulse`, `animate-bounce`, and `animate-spin` are loading affordances. The
213
+ tell is using them as ambient decoration: a pulsing hero badge, a bouncing arrow
214
+ that never stops, a spinning accent that encodes no progress. Motion that loops
215
+ forever with no state behind it reads as filler and fights `prefers-reduced-motion`.
216
+
217
+ Instances:
218
+
219
+ 1. `animate-pulse` on a static "New" badge in the hero.
220
+ 2. `animate-bounce` on a scroll-down chevron looping with no end.
221
+ 3. `animate-spin` on a decorative ring that is not a spinner.
222
+ 4. A pulsing gradient blob behind the headline.
223
+
224
+ Diagnostic regex (hook, conservative): a quoted class string containing
225
+ `animate-(pulse|bounce|spin)` that does not also contain a loading signal
226
+ (`loading`, `loader`, `spinner`, `skeleton`, `icon`, `i-`, or `sr-only`).
227
+
228
+ Remediation: tie motion to a state. Use `animate-pulse` on skeletons while data
229
+ loads, `animate-spin` on a real spinner during a request, and drop ambient loops.
230
+ Respect `prefers-reduced-motion`. Enter with `ease-out`, exit shorter than enter,
231
+ and never animate keyboard-driven actions. See the Motion Anti-Patterns section of
232
+ `reference/anti-patterns.md`.
233
+
234
+ ---
235
+
236
+ ## stock-photo-people
237
+
238
+ Rule id: `stock-photo-people` (catalog-only)
239
+
240
+ Generic smiling-team and handshake stock photography dropped in where a real product
241
+ view belongs. The diverse-team-around-a-laptop shot, the lone founder gazing out a
242
+ window, the abstract handshake: these read as filler the moment a viewer asks what the
243
+ product actually does. Like the isometric fallback, the tell is that no real screen was
244
+ available, so a stock human filled the hole.
245
+
246
+ Instances:
247
+
248
+ 1. A hero photo of four people laughing at one laptop, captioned with a value prop.
249
+ 2. `src="/images/team-meeting.jpg"` standing in for a product screenshot.
250
+ 3. A testimonial section using stock headshots instead of real customer faces.
251
+ 4. An unsplash or shutterstock asset path in a primary marketing surface.
252
+ 5. The same stock photo reused across unrelated sections of one page.
253
+
254
+ Diagnostic regex (where applicable): `\b(?:unsplash|shutterstock|istockphoto|gettyimages|stock[-_]?photo)[\w./-]*` in an asset path or `src`.
255
+
256
+ Remediation: show the real product or real people connected to it. A genuine screenshot,
257
+ a real customer portrait with permission, or a purpose-shot photo all beat a stock human.
258
+ If no real asset exists yet, a clean illustration with brand character beats a stock smile.
259
+
260
+ Primary axis: Authenticity.
261
+
262
+ ---
263
+
264
+ ## badge-spam
265
+
266
+ Rule id: `badge-spam` (catalog-only)
267
+
268
+ Decorative pills stacked to manufacture importance: "New", "Beta", "Pro", "AI-powered",
269
+ "v2" scattered across cards and nav with no state behind them. One badge that reflects a
270
+ real status orients the user. Four badges in a row are noise that the model adds because
271
+ badges look like product maturity. The tell is a cluster of status pills none of which
272
+ encode a true, current state.
273
+
274
+ Instances:
275
+
276
+ 1. A card carrying "New", "Hot", and "AI-powered" badges at once.
277
+ 2. A nav item with a permanent "Beta" pill that has shipped for a year.
278
+ 3. Every feature tile in a grid wearing the same "Pro" badge.
279
+ 4. An "AI-powered" badge on a feature with no AI in it.
280
+ 5. Three or more `<Badge>` or `.badge` elements rendered in one small region.
281
+
282
+ Diagnostic regex (where applicable): a quoted label matching `\b(?:New|Beta|Pro|AI-powered|Hot|Coming soon)\b` inside a badge or chip element, flagged at three or more in one component.
283
+
284
+ Remediation: keep one badge that carries a real, current status, and delete the rest.
285
+ A badge should change when state changes. If a label never changes, it is decoration, not
286
+ status, and belongs in the copy or not at all.
287
+
288
+ Primary axis: Authenticity.
289
+
290
+ ---
291
+
292
+ ## oversized-single-word
293
+
294
+ Rule id: `oversized-single-word` (catalog-only)
295
+
296
+ A single word blown up to display size to fill a section that has no content to carry it.
297
+ "Build.", "Ship.", "Scale." at `text-8xl` with nothing beneath but air. Large type is a
298
+ strong tool for one real headline; the tell is a one-word slab standing in for a sentence
299
+ the writer did not write, sized for drama rather than meaning.
300
+
301
+ Instances:
302
+
303
+ 1. A section whose only content is "Innovate." at `text-9xl`.
304
+ 2. A stack of one-word slabs ("Fast." "Simple." "Powerful.") each filling a viewport.
305
+ 3. A giant verb with no object, no supporting line, and 300px of padding around it.
306
+ 4. Display-size text where the word count is one and the surrounding region is empty.
307
+
308
+ Diagnostic regex (where applicable): an element with `text-(?:7xl|8xl|9xl)` whose text node is a single word.
309
+
310
+ Remediation: write the sentence the word was standing in for, then size the headline to
311
+ serve reading. If a one-word moment is genuinely the design, give it a supporting line and
312
+ a reason to be that large. Density should fit the content, not inflate to fill space.
313
+
314
+ Primary axis: Density.
315
+
316
+ ---
317
+
318
+ ## motion-without-content-intent
319
+
320
+ Rule id: `motion-without-content-intent` (catalog-only)
321
+
322
+ Scroll-triggered fades, parallax layers, and entrance animations applied to every block
323
+ because motion reads as "modern". This is the storytelling cousin of the ambient-loop tell:
324
+ where `decorative-motion-without-intent` flags a forever-looping spinner, this flags motion
325
+ wired to scroll or load that carries no content reason. Every element fading up in sequence
326
+ delays the reader and signals motion chosen as decoration, not to direct attention.
327
+
328
+ Instances:
329
+
330
+ 1. Every section wrapped in a `whileInView` fade-up with a staggered delay.
331
+ 2. Parallax on three background layers that encode no depth meaning.
332
+ 3. An entrance animation on body copy that the reader is waiting to read.
333
+ 4. Scroll-jacking that overrides native scroll for a "cinematic" reveal.
334
+
335
+ Diagnostic regex (where applicable): `whileInView|data-aos|scroll-trigger|parallax` applied broadly across blocks without a reduced-motion guard in the same file.
336
+
337
+ Remediation: tie motion to a content reason. Animate the one element that should draw the
338
+ eye, let the rest render immediately, and respect `prefers-reduced-motion`. Reading content
339
+ should never wait on a decorative reveal. See the Motion Anti-Patterns section of
340
+ `reference/anti-patterns.md`.
341
+
342
+ Primary axis: Hierarchy.
343
+
344
+ ---
345
+
346
+ ## narrator-from-a-distance-UI
347
+
348
+ Rule id: `narrator-from-a-distance-UI` (catalog-only)
349
+
350
+ Copy that describes the product from the outside in a detached marketing voice instead of
351
+ speaking to the user doing a task. "Our platform empowers teams to achieve more" is a
352
+ narrator at a distance; "Invite your team and assign the first task" speaks to the person
353
+ in front of the screen. The tell is third-person product description where a second-person
354
+ instruction belongs, especially inside the product rather than on a landing page.
355
+
356
+ Instances:
357
+
358
+ 1. A dashboard empty state reading "The platform helps you organize your work" instead of "Create your first board".
359
+ 2. In-product copy in third person ("Users can export reports") rather than direct address.
360
+ 3. A feature headline describing the feature to an investor, not to its user.
361
+ 4. Onboarding steps narrated about the product instead of instructing the new user.
362
+
363
+ Diagnostic regex (where applicable): in-product strings matching `\b(?:our platform|the platform|users can|we help you|empowers)\b` outside marketing routes.
364
+
365
+ Remediation: write to the user in second person and name the next action. Inside the
366
+ product, copy should instruct and orient, not pitch. Reserve the outside-in description for
367
+ the marketing surface, and even there, lead with what the reader can do. See
368
+ `reference/copy-quality.md` for the per-surface voice contract.
369
+
370
+ Primary axis: Directness.
371
+
372
+ ---
373
+
374
+ ## Notes
375
+
376
+ This is a v2 catalog, growing commit by commit, not a ceiling. The hook regexes favor
377
+ precision over recall: they aim to fire only on the obvious cases so the WARN stays
378
+ trustworthy. The five v2 categories above are catalog-only until a regex earns a place in
379
+ the hook; they still give review a named tell and a primary axis to score against. A clean
380
+ pass here does not mean the design is good. It means the loudest default-AI tells are
381
+ absent. Real review still applies the full rubric in `reference/audit-scoring.md`, the verb
382
+ axes in `reference/anti-slop-rubric.md`, and the BAN / SLOP catalog in
383
+ `reference/anti-patterns.md`.
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+ /**
3
+ * scripts/lib/confidence-route.cjs — pure routing helper for the reviewer
4
+ * confidence gate (Phase 49). Decides where a single finding/gap goes based on
5
+ * its severity, its `confidence` score (0.0-1.0), and whether the reviewer
6
+ * parked it in the `## Tentative` section.
7
+ *
8
+ * Canonical rule (mirrors reference/reviewer-confidence-gate.md):
9
+ * - A finding in `## Tentative` -> 'drop' (never reaches design-fixer)
10
+ * - confidence < 0.5 -> 'drop' (low-confidence floor; stays tentative)
11
+ * - HIGH/CRITICAL (BLOCKER|MAJOR) needs -> 'fix' only when confidence >= 0.8,
12
+ * otherwise 'user-review'
13
+ * - confidence in [0.5, 0.8) -> 'user-review' (surfaced, not auto-fixed)
14
+ * - confidence >= 0.8 -> 'fix'
15
+ *
16
+ * Returns one of: 'fix' | 'user-review' | 'drop'. Dependency-free and side
17
+ * effect free so the routing matrix is unit-testable in isolation.
18
+ */
19
+
20
+ const HIGH_FLOOR = 0.8; // BLOCKER/MAJOR must clear this to auto-fix
21
+ const SURFACE_FLOOR = 0.5; // below this a finding is dropped (stays tentative)
22
+
23
+ // Severity labels that count as HIGH/CRITICAL for the auto-fix floor.
24
+ const HIGH_SEVERITIES = new Set(['blocker', 'major', 'high', 'critical']);
25
+
26
+ function isHighSeverity(severity) {
27
+ if (typeof severity !== 'string') return false;
28
+ return HIGH_SEVERITIES.has(severity.trim().toLowerCase());
29
+ }
30
+
31
+ /**
32
+ * Route a finding/gap.
33
+ * @param {object} finding
34
+ * @param {string} finding.severity - BLOCKER | MAJOR | MINOR | COSMETIC (case-insensitive).
35
+ * @param {number} finding.confidence - 0.0-1.0 confidence score.
36
+ * @param {boolean} [finding.tentative] - true when the finding sits in `## Tentative`.
37
+ * @returns {'fix'|'user-review'|'drop'}
38
+ */
39
+ function route({ severity, confidence, tentative = false } = {}) {
40
+ // 1. Tentative findings never reach the fixer, regardless of score.
41
+ if (tentative === true) return 'drop';
42
+
43
+ // 2. A missing/non-numeric confidence is treated as the lowest tier: surface
44
+ // for user review rather than silently auto-fixing or dropping.
45
+ const c = typeof confidence === 'number' && Number.isFinite(confidence) ? confidence : 0;
46
+
47
+ // 3. Low-confidence floor: anything under 0.5 is dropped (stays tentative).
48
+ if (c < SURFACE_FLOOR) return 'drop';
49
+
50
+ // 4. HIGH/CRITICAL findings must clear the 0.8 floor to auto-fix; otherwise
51
+ // they are routed to the user instead of the fixer.
52
+ if (isHighSeverity(severity)) {
53
+ return c >= HIGH_FLOOR ? 'fix' : 'user-review';
54
+ }
55
+
56
+ // 5. Lower-severity findings: 0.5-0.8 surfaces for review, >= 0.8 auto-fixes.
57
+ return c >= HIGH_FLOOR ? 'fix' : 'user-review';
58
+ }
59
+
60
+ module.exports = { route, isHighSeverity, HIGH_FLOOR, SURFACE_FLOOR };