clud-bug 0.7.0-rc.2 → 0.7.0-rc.21
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 +20 -0
- package/data/canonical-v1.json +37 -0
- package/dist/cli/configure-github.d.ts +58 -0
- package/dist/cli/configure-github.d.ts.map +1 -0
- package/dist/cli/configure-github.js +242 -0
- package/dist/cli/configure-github.js.map +1 -0
- package/dist/cli/hooks.d.ts +51 -0
- package/dist/cli/hooks.d.ts.map +1 -0
- package/dist/cli/hooks.js +138 -0
- package/dist/cli/hooks.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +949 -35
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/post-check-run.d.ts +16 -0
- package/dist/cli/post-check-run.d.ts.map +1 -0
- package/dist/cli/post-check-run.js +93 -0
- package/dist/cli/post-check-run.js.map +1 -0
- package/dist/cli/review-prompt.d.ts +43 -0
- package/dist/cli/review-prompt.d.ts.map +1 -0
- package/dist/cli/review-prompt.js +287 -0
- package/dist/cli/review-prompt.js.map +1 -0
- package/dist/cli/skills.d.ts +7 -0
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +42 -2
- package/dist/cli/skills.js.map +1 -1
- package/dist/cli/update-notifier.d.ts +17 -0
- package/dist/cli/update-notifier.d.ts.map +1 -0
- package/dist/cli/update-notifier.js +88 -0
- package/dist/cli/update-notifier.js.map +1 -0
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +60 -11
- package/dist/cli/update.js.map +1 -1
- package/dist/core/auto-resolve.d.ts +235 -0
- package/dist/core/auto-resolve.d.ts.map +1 -0
- package/dist/core/auto-resolve.js +293 -0
- package/dist/core/auto-resolve.js.map +1 -0
- package/dist/core/budget-plan.d.ts +125 -0
- package/dist/core/budget-plan.d.ts.map +1 -0
- package/dist/core/budget-plan.js +211 -0
- package/dist/core/budget-plan.js.map +1 -0
- package/dist/core/check-verdict.d.ts +31 -0
- package/dist/core/check-verdict.d.ts.map +1 -0
- package/dist/core/check-verdict.js +66 -0
- package/dist/core/check-verdict.js.map +1 -0
- package/dist/core/configure-github.d.ts +179 -0
- package/dist/core/configure-github.d.ts.map +1 -0
- package/dist/core/configure-github.js +289 -0
- package/dist/core/configure-github.js.map +1 -0
- package/dist/core/design.d.ts +37 -0
- package/dist/core/design.d.ts.map +1 -0
- package/dist/core/design.js +47 -0
- package/dist/core/design.js.map +1 -0
- package/dist/core/diff-findings.d.ts +96 -0
- package/dist/core/diff-findings.d.ts.map +1 -0
- package/dist/core/diff-findings.js +261 -0
- package/dist/core/diff-findings.js.map +1 -0
- package/dist/core/formal-review.d.ts +79 -0
- package/dist/core/formal-review.d.ts.map +1 -0
- package/dist/core/formal-review.js +71 -0
- package/dist/core/formal-review.js.map +1 -0
- package/dist/core/index.d.ts +15 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +53 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/inline-threads.d.ts +160 -0
- package/dist/core/inline-threads.d.ts.map +1 -0
- package/dist/core/inline-threads.js +369 -0
- package/dist/core/inline-threads.js.map +1 -0
- package/dist/core/multi-pass-aggregate.d.ts +161 -0
- package/dist/core/multi-pass-aggregate.d.ts.map +1 -0
- package/dist/core/multi-pass-aggregate.js +457 -0
- package/dist/core/multi-pass-aggregate.js.map +1 -0
- package/dist/core/plan-review.d.ts +57 -0
- package/dist/core/plan-review.d.ts.map +1 -0
- package/dist/core/plan-review.js +77 -0
- package/dist/core/plan-review.js.map +1 -0
- package/dist/core/prompt-builder.d.ts +18 -0
- package/dist/core/prompt-builder.d.ts.map +1 -1
- package/dist/core/prompt-builder.js +24 -2
- package/dist/core/prompt-builder.js.map +1 -1
- package/dist/core/render.d.ts.map +1 -1
- package/dist/core/render.js +1 -19
- package/dist/core/render.js.map +1 -1
- package/dist/core/resolve-verifier.d.ts +53 -0
- package/dist/core/resolve-verifier.d.ts.map +1 -0
- package/dist/core/resolve-verifier.js +204 -0
- package/dist/core/resolve-verifier.js.map +1 -0
- package/dist/core/review-context.d.ts +39 -0
- package/dist/core/review-context.d.ts.map +1 -0
- package/dist/core/review-context.js +99 -0
- package/dist/core/review-context.js.map +1 -0
- package/dist/core/review-plan.d.ts +219 -0
- package/dist/core/review-plan.d.ts.map +1 -0
- package/dist/core/review-plan.js +274 -0
- package/dist/core/review-plan.js.map +1 -0
- package/dist/core/review-writeback.d.ts +74 -0
- package/dist/core/review-writeback.d.ts.map +1 -1
- package/dist/core/review-writeback.js +71 -4
- package/dist/core/review-writeback.js.map +1 -1
- package/dist/core/skills.d.ts +25 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +100 -4
- package/dist/core/skills.js.map +1 -1
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +4 -0
- package/dist/core/version.js.map +1 -0
- package/package.json +11 -5
- package/src/cli/configure-github.ts +329 -0
- package/src/cli/hooks.ts +155 -0
- package/src/cli/index.ts +6 -0
- package/src/cli/main.ts +1031 -37
- package/src/cli/post-check-run.ts +115 -0
- package/src/cli/review-prompt.ts +355 -0
- package/src/cli/skills.ts +42 -2
- package/src/cli/update-notifier.ts +100 -0
- package/src/cli/update.ts +60 -11
- package/src/core/auto-resolve.ts +488 -0
- package/src/core/budget-plan.ts +324 -0
- package/src/core/check-verdict.ts +93 -0
- package/src/core/configure-github.ts +497 -0
- package/src/core/design.ts +73 -0
- package/src/core/diff-findings.ts +323 -0
- package/src/core/formal-review.ts +168 -0
- package/src/core/index.ts +188 -0
- package/src/core/inline-threads.ts +471 -0
- package/src/core/multi-pass-aggregate.ts +591 -0
- package/src/core/plan-review.ts +136 -0
- package/src/core/prompt-builder.ts +44 -2
- package/src/core/render.ts +10 -16
- package/src/core/resolve-verifier.ts +254 -0
- package/src/core/review-context.ts +106 -0
- package/src/core/review-plan.ts +485 -0
- package/src/core/review-writeback.ts +161 -4
- package/src/core/skills.ts +128 -5
- package/src/core/version.ts +3 -0
- package/templates/clud-bug-review.md.tmpl +115 -0
- package/templates/skills/design/design-system-consistency.md +41 -0
- package/templates/skills/design/frontend-a11y.md +41 -0
- package/templates/skills/design/visual-polish.md +41 -0
- package/templates/workflow-py.yml.tmpl +92 -2
- package/templates/workflow-ts.yml.tmpl +92 -2
- package/templates/workflow.yml.tmpl +173 -2
|
@@ -46,6 +46,34 @@ export const SEVERITY_EMOJI = {
|
|
|
46
46
|
preexisting: '\u{1F7E3}', // U+1F7E3 PURPLE CIRCLE
|
|
47
47
|
} as const;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* One parsed finding suitable for the SPEC §1.8.1 Resolved / Still-open
|
|
51
|
+
* blocks. Mirrors `ParsedFinding` from `./diff-findings.ts`; declared
|
|
52
|
+
* here as a structural interface (not re-imported) so callers without
|
|
53
|
+
* `diff-findings` in scope can still construct the input.
|
|
54
|
+
*/
|
|
55
|
+
export interface RenderedFindingRef {
|
|
56
|
+
file: string;
|
|
57
|
+
line: number;
|
|
58
|
+
severity: 'critical' | 'minor' | 'preexisting';
|
|
59
|
+
skillName: string;
|
|
60
|
+
summary: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* SPEC §6.7.3 cache telemetry — per-invocation token counts from the
|
|
65
|
+
* Anthropic-family `usage` block. When present, the renderer emits a
|
|
66
|
+
* `<!-- cache: ... -->` HTML comment immediately below the
|
|
67
|
+
* `<!-- review-sha: ... -->` metadata line so downstream cost analysis
|
|
68
|
+
* tooling can audit cache behaviour from the committed doc-file.
|
|
69
|
+
*/
|
|
70
|
+
export interface CacheStats {
|
|
71
|
+
/** `cache_read_input_tokens` from the Anthropic usage block. */
|
|
72
|
+
cachedInputTokens: number;
|
|
73
|
+
/** `cache_creation_input_tokens` from the Anthropic usage block. */
|
|
74
|
+
cacheCreationInputTokens: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
export interface RenderReviewFileInput {
|
|
50
78
|
review: Review;
|
|
51
79
|
prNumber: number;
|
|
@@ -53,6 +81,34 @@ export interface RenderReviewFileInput {
|
|
|
53
81
|
headSha: string;
|
|
54
82
|
/** GitHub PR URL — appended verbatim to the trailing rule. */
|
|
55
83
|
prUrl: string;
|
|
84
|
+
/**
|
|
85
|
+
* Findings present in the prior `docs/reviews/PR-<n>.md` that are NOT
|
|
86
|
+
* present in this round's `review`. When non-empty, emits the SPEC
|
|
87
|
+
* §1.8.1 `**Resolved this round:**` block AFTER the severity buckets
|
|
88
|
+
* and BEFORE the trailing `---` separator. Omitted entirely when
|
|
89
|
+
* empty/undefined (SPEC §1.8.1: blocks MUST be omitted when empty).
|
|
90
|
+
*
|
|
91
|
+
* Produced by `diffFindings()` from `./diff-findings.ts`.
|
|
92
|
+
*/
|
|
93
|
+
resolvedFindings?: RenderedFindingRef[];
|
|
94
|
+
/**
|
|
95
|
+
* Findings present in BOTH the prior `docs/reviews/PR-<n>.md` AND
|
|
96
|
+
* this round's `review` — the persistent ones. When non-empty, emits
|
|
97
|
+
* the SPEC §1.8.1 `**Still open:**` block AFTER `**Resolved this
|
|
98
|
+
* round:**` and before the trailing `---` separator. Omitted entirely
|
|
99
|
+
* when empty/undefined.
|
|
100
|
+
*
|
|
101
|
+
* Produced by `diffFindings()` from `./diff-findings.ts`.
|
|
102
|
+
*/
|
|
103
|
+
stillOpenFindings?: RenderedFindingRef[];
|
|
104
|
+
/**
|
|
105
|
+
* SPEC §6.7.3 cache telemetry. When present, emits a
|
|
106
|
+
* `<!-- cache: <read> read · <created> created -->` HTML comment
|
|
107
|
+
* immediately below the `<!-- review-sha: ... -->` metadata line.
|
|
108
|
+
* Omitted entirely when undefined (this comment is OPTIONAL per the
|
|
109
|
+
* SPEC, but SHOULD be implemented for cost-analysis tooling).
|
|
110
|
+
*/
|
|
111
|
+
cacheStats?: CacheStats;
|
|
56
112
|
}
|
|
57
113
|
|
|
58
114
|
/**
|
|
@@ -66,7 +122,15 @@ export interface RenderReviewFileInput {
|
|
|
66
122
|
* the underlying finding data but differ in container.
|
|
67
123
|
*/
|
|
68
124
|
export function renderReviewFile(input: RenderReviewFileInput): string {
|
|
69
|
-
const {
|
|
125
|
+
const {
|
|
126
|
+
review,
|
|
127
|
+
prNumber,
|
|
128
|
+
headSha,
|
|
129
|
+
prUrl,
|
|
130
|
+
resolvedFindings,
|
|
131
|
+
stillOpenFindings,
|
|
132
|
+
cacheStats,
|
|
133
|
+
} = input;
|
|
70
134
|
// Wire-shape Review carries findings in 3 severity arrays. Flatten to
|
|
71
135
|
// internal `Finding[]` so the renderer's bucketing, count-derivation,
|
|
72
136
|
// and per-skill aggregation can work uniformly.
|
|
@@ -83,6 +147,15 @@ export function renderReviewFile(input: RenderReviewFileInput): string {
|
|
|
83
147
|
lines.push(`<!-- protocol-version: ${PROTOCOL_VERSION} -->`);
|
|
84
148
|
lines.push(`<!-- written-by: ${WRITTEN_BY} -->`);
|
|
85
149
|
lines.push(`<!-- review-sha: ${headSha} -->`);
|
|
150
|
+
// SPEC §6.7.3: cache telemetry comment goes immediately below the
|
|
151
|
+
// review-sha so a single grep can pull review-sha + cache stats out
|
|
152
|
+
// of a doc-file without re-parsing the whole block. Omitted entirely
|
|
153
|
+
// when cacheStats is undefined.
|
|
154
|
+
if (cacheStats !== undefined) {
|
|
155
|
+
lines.push(
|
|
156
|
+
`<!-- cache: ${cacheStats.cachedInputTokens} read · ${cacheStats.cacheCreationInputTokens} created -->`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
86
159
|
lines.push('');
|
|
87
160
|
|
|
88
161
|
// Summary line — SPEC §1.8.1 wording.
|
|
@@ -124,9 +197,31 @@ export function renderReviewFile(input: RenderReviewFileInput): string {
|
|
|
124
197
|
lines.push('');
|
|
125
198
|
}
|
|
126
199
|
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
200
|
+
// SPEC §1.8.1 Resolved this round / Still open blocks. Omitted
|
|
201
|
+
// entirely when the corresponding input list is empty/undefined.
|
|
202
|
+
//
|
|
203
|
+
// Bullet shape (one finding per line):
|
|
204
|
+
// - `path/file.ts:42` — `critical-issues-only`: null-deref ... (was 🔴 Critical)
|
|
205
|
+
//
|
|
206
|
+
// The `(was <emoji> <Severity>)` suffix carries the SEVERITY at the
|
|
207
|
+
// time the finding was last seen so a downgrade across rounds is
|
|
208
|
+
// visible without diffing the two doc files. Note: backticks around
|
|
209
|
+
// file and skill render in markdown viewers as inline-code, which
|
|
210
|
+
// makes the file path scannable in the GitHub UI.
|
|
211
|
+
if (resolvedFindings !== undefined && resolvedFindings.length > 0) {
|
|
212
|
+
lines.push('**Resolved this round:**');
|
|
213
|
+
for (const f of resolvedFindings) {
|
|
214
|
+
lines.push(renderDiffFinding(f));
|
|
215
|
+
}
|
|
216
|
+
lines.push('');
|
|
217
|
+
}
|
|
218
|
+
if (stillOpenFindings !== undefined && stillOpenFindings.length > 0) {
|
|
219
|
+
lines.push('**Still open:**');
|
|
220
|
+
for (const f of stillOpenFindings) {
|
|
221
|
+
lines.push(renderDiffFinding(f));
|
|
222
|
+
}
|
|
223
|
+
lines.push('');
|
|
224
|
+
}
|
|
130
225
|
|
|
131
226
|
lines.push('---');
|
|
132
227
|
lines.push('');
|
|
@@ -148,6 +243,30 @@ function bucketBySeverity(findings: Finding[]): Record<
|
|
|
148
243
|
};
|
|
149
244
|
}
|
|
150
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Renders one finding for the SPEC §1.8.1 Resolved / Still-open blocks.
|
|
248
|
+
* Output shape:
|
|
249
|
+
*
|
|
250
|
+
* - `path/file.ts:42` — `critical-issues-only`: null-deref ... (was 🔴 Critical)
|
|
251
|
+
*
|
|
252
|
+
* Line `0` is treated as "no anchor" (cross-cutting / missing) and the
|
|
253
|
+
* `:N` suffix is omitted. The severity label uses the SPEC §1.8.1
|
|
254
|
+
* emoji + label so a reader can see at a glance whether the finding
|
|
255
|
+
* was downgraded (different severity in current round) or merely
|
|
256
|
+
* resolved.
|
|
257
|
+
*/
|
|
258
|
+
function renderDiffFinding(f: RenderedFindingRef): string {
|
|
259
|
+
const location = f.line > 0 ? `${f.file}:${f.line}` : f.file;
|
|
260
|
+
const label =
|
|
261
|
+
f.severity === 'critical'
|
|
262
|
+
? 'Critical'
|
|
263
|
+
: f.severity === 'minor'
|
|
264
|
+
? 'Minor'
|
|
265
|
+
: 'Preexisting';
|
|
266
|
+
const emoji = SEVERITY_EMOJI[f.severity];
|
|
267
|
+
return `- \`${location}\` — \`${f.skillName}\`: ${f.summary} (was ${emoji} ${label})`;
|
|
268
|
+
}
|
|
269
|
+
|
|
151
270
|
function renderFinding(f: Finding, includeReasoning: boolean): string {
|
|
152
271
|
// findingItemSchema.file is z.string().optional() — the model is
|
|
153
272
|
// instructed to always provide it, but Zod doesn't enforce. Without
|
|
@@ -199,9 +318,30 @@ export interface PassAttribution {
|
|
|
199
318
|
note?: string;
|
|
200
319
|
}
|
|
201
320
|
|
|
321
|
+
/**
|
|
322
|
+
* SPEC §6.10.1 consensus marker, derived from a finding's per-pass attributions
|
|
323
|
+
* by the aggregator's `deriveConsensus`. Defined here — the canonical home of
|
|
324
|
+
* the unified-finding shape — so it rides on `MultiPassReview.findings`.
|
|
325
|
+
*
|
|
326
|
+
* - `1-of-N` — single pass produced this finding; no consensus reached.
|
|
327
|
+
* §6.10.2 MUST NOT fire auto-fix on these.
|
|
328
|
+
* - `2-of-2` — two passes independently produced an equivalent finding.
|
|
329
|
+
* Qualifies for auto-fix per §6.10.2.
|
|
330
|
+
* - `arbitrated` — passes disagreed; an arbiter resolved the dispute. Today's
|
|
331
|
+
* cross-check shape treats `arbitrated` as NOT-qualifying (conservative).
|
|
332
|
+
*/
|
|
333
|
+
export type Consensus = '1-of-N' | '2-of-2' | 'arbitrated';
|
|
334
|
+
|
|
202
335
|
export interface UnifiedFinding extends Finding {
|
|
203
336
|
/** One PassAttribution per pass involved with this finding. Order: by passNumber. */
|
|
204
337
|
attributions: PassAttribution[];
|
|
338
|
+
/**
|
|
339
|
+
* SPEC §6.10.1 consensus verdict. Stamped by the aggregator's `finalize()`
|
|
340
|
+
* from `deriveConsensus(attributions)`. Optional: single-pass / non-aggregated
|
|
341
|
+
* findings omit it; consumers treat absence as '1-of-N' (the auto-fix gate
|
|
342
|
+
* default — see clud-bug-app/lib/auto-fix-policy.ts).
|
|
343
|
+
*/
|
|
344
|
+
consensus?: Consensus;
|
|
205
345
|
}
|
|
206
346
|
|
|
207
347
|
/** Effective resolution verdict the multi-pass orchestrator emits. */
|
|
@@ -381,6 +521,15 @@ function renderUnifiedFinding(
|
|
|
381
521
|
}
|
|
382
522
|
const headLabel = formatAttributionLabel(head);
|
|
383
523
|
const lines: string[] = [];
|
|
524
|
+
// SPEC §6.8.1 (NORMATIVE for multi-pass): a per-finding pass-attribution HTML
|
|
525
|
+
// comment immediately preceding the bullet, naming the originating pass as a
|
|
526
|
+
// stable lowercase identifier so consumers can aggregate per-pass metrics.
|
|
527
|
+
lines.push(`<!-- pass: ${passIdentifier(head.roleName)} -->`);
|
|
528
|
+
// SPEC §6.10.1 (NORMATIVE for gated reviewers): the consensus marker alongside
|
|
529
|
+
// the pass attribution, when the finding carries a consensus verdict.
|
|
530
|
+
if (f.consensus) {
|
|
531
|
+
lines.push(`<!-- consensus: ${f.consensus} -->`);
|
|
532
|
+
}
|
|
384
533
|
lines.push(`- ${headLabel} **${location}** — ${f.skill}: ${f.summary}`);
|
|
385
534
|
if (includeReasoning && f.reasoning) {
|
|
386
535
|
lines.push(` Reasoning: ${f.reasoning}`);
|
|
@@ -403,6 +552,14 @@ function renderUnifiedFinding(
|
|
|
403
552
|
* `source: 'first'` — i.e. when a later pass surfaced this finding without
|
|
404
553
|
* Pass 1 raising it.
|
|
405
554
|
*/
|
|
555
|
+
/** SPEC §6.8.1 pass identifier: a stable lowercase slug of the role name. */
|
|
556
|
+
function passIdentifier(roleName: string): string {
|
|
557
|
+
return roleName
|
|
558
|
+
.toLowerCase()
|
|
559
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
560
|
+
.replace(/^-+|-+$/g, '');
|
|
561
|
+
}
|
|
562
|
+
|
|
406
563
|
function formatAttributionLabel(a: PassAttribution): string {
|
|
407
564
|
const independent =
|
|
408
565
|
a.source === 'independent' && a.passNumber > 1
|
package/src/core/skills.ts
CHANGED
|
@@ -285,6 +285,60 @@ export function appliesToPr(skillContent: unknown, prPaths: unknown): boolean {
|
|
|
285
285
|
return false;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
// v0.7.0-rc.6 (SPEC §1.10.1 v0.5.1+) — does this skill apply to a PR
|
|
289
|
+
// opened by `prAuthor`? Reads `applies_to.author` from the raw SKILL.md
|
|
290
|
+
// frontmatter. Returns:
|
|
291
|
+
// - `true` if `applies_to.author` is absent (no author filter set —
|
|
292
|
+
// skill loads regardless of author; v0.5.0 backward-compat default).
|
|
293
|
+
// - `true` if `applies_to.author === prAuthor` (case-sensitive match
|
|
294
|
+
// against the PR author's GitHub login).
|
|
295
|
+
// - `false` if `applies_to.author` is set to a different login.
|
|
296
|
+
//
|
|
297
|
+
// Designed to mirror `appliesToPr`'s signature (raw skill content +
|
|
298
|
+
// runtime PR context) so consumers can call both filters in a single
|
|
299
|
+
// loader pass. Skills with NO `applies_to` block at all also return
|
|
300
|
+
// `true` (the existing v0.5.0 unconditional-load behavior).
|
|
301
|
+
//
|
|
302
|
+
// Strict-AND composition per SPEC §1.10.1: when a skill sets BOTH
|
|
303
|
+
// `applies_to.author` AND `paths`/`extensions`, callers MUST call BOTH
|
|
304
|
+
// filters and AND the results. This helper only checks the author leg;
|
|
305
|
+
// the caller composes.
|
|
306
|
+
export function appliesToAuthor(
|
|
307
|
+
skillContent: unknown,
|
|
308
|
+
prAuthor: unknown,
|
|
309
|
+
): boolean {
|
|
310
|
+
if (typeof skillContent !== 'string') return true;
|
|
311
|
+
const fm = skillContent.match(/^---\n([\s\S]*?)\n---/);
|
|
312
|
+
if (!fm) return true;
|
|
313
|
+
const block = fm[1] as string;
|
|
314
|
+
// Look for `applies_to:` block; if absent, no author constraint.
|
|
315
|
+
const head = block.match(/^applies_to:\s*$/m);
|
|
316
|
+
if (!head) return true;
|
|
317
|
+
const startIdx = (head.index as number) + head[0].length;
|
|
318
|
+
const rest = block.slice(startIdx);
|
|
319
|
+
// The block ends at the next top-level key (line starting with a
|
|
320
|
+
// word char + `:`) OR end-of-block. Mirrors `readAppliesTo`.
|
|
321
|
+
const stop = rest.search(/^\w[\w-]*:/m);
|
|
322
|
+
const scoped = stop === -1 ? rest : rest.slice(0, stop);
|
|
323
|
+
// Match ` author: <value>` — single string scalar; strip optional
|
|
324
|
+
// quotes. `author: [a, b]` form is intentionally NOT supported
|
|
325
|
+
// (SPEC: "single GitHub login string, no list").
|
|
326
|
+
const authorMatch = scoped.match(/^\s{2}author:\s*(.+?)\s*$/m);
|
|
327
|
+
if (!authorMatch) return true;
|
|
328
|
+
const declared = (authorMatch[1] as string)
|
|
329
|
+
.trim()
|
|
330
|
+
.replace(/^["']|["']$/g, '');
|
|
331
|
+
if (!declared) return true; // empty value treated as unset
|
|
332
|
+
if (typeof prAuthor !== 'string' || !prAuthor) return false;
|
|
333
|
+
// GitHub logins are case-insensitive (GitHub rejects creating two
|
|
334
|
+
// accounts that differ only in case; `pull_request.user.login` is
|
|
335
|
+
// case-preserved but routing is not case-sensitive). Compare
|
|
336
|
+
// case-insensitively so a skill author writing `Thrillmot` still
|
|
337
|
+
// matches a webhook delivering `thrillmot` (or vice versa).
|
|
338
|
+
// Reviewer-flagged Important on PR #179.
|
|
339
|
+
return declared.toLowerCase() === prAuthor.toLowerCase();
|
|
340
|
+
}
|
|
341
|
+
|
|
288
342
|
// Minimal glob → regex: `**` → `.*`, `*` → `[^/]*`, `?` → `.`,
|
|
289
343
|
// everything else escaped. Anchored full-string match.
|
|
290
344
|
function globMatch(glob: string, path: string): boolean {
|
|
@@ -601,14 +655,39 @@ export type SkillReviewMode = 'shared' | 'dedicated';
|
|
|
601
655
|
* `PromptSkillFrontmatter` is a narrower subset (name + description +
|
|
602
656
|
* applies_to) so we keep the full shape here.
|
|
603
657
|
*/
|
|
658
|
+
export type SkillKind = 'rule' | 'voice' | 'design';
|
|
659
|
+
export type VoiceScope = 'personal' | 'team' | 'org' | 'community';
|
|
660
|
+
|
|
604
661
|
export interface SkillFrontmatter {
|
|
605
662
|
name: string;
|
|
606
663
|
description: string;
|
|
607
664
|
source: SkillSource | string; // string fallback for forward-compat
|
|
608
665
|
review_mode: SkillReviewMode;
|
|
666
|
+
/**
|
|
667
|
+
* SPEC §1.10.1 v0.5.0+ skill classification. OPTIONAL — absent =
|
|
668
|
+
* `kind: rule` (the v0.4.0 default). Surfaced on the parsed
|
|
669
|
+
* frontmatter (v0.7.0-rc.6+) so consumers can route on the field;
|
|
670
|
+
* v0.7.0-rc.5 and earlier silently dropped it.
|
|
671
|
+
*/
|
|
672
|
+
kind?: SkillKind;
|
|
673
|
+
/**
|
|
674
|
+
* SPEC §1.10.1 v0.5.0+ voice scope. REQUIRED when `kind: voice`,
|
|
675
|
+
* absent otherwise. Surfaced on the parsed frontmatter (v0.7.0-rc.6+).
|
|
676
|
+
*/
|
|
677
|
+
voice_scope?: VoiceScope;
|
|
609
678
|
applies_to?: {
|
|
610
679
|
paths?: string[];
|
|
611
680
|
extensions?: string[];
|
|
681
|
+
/**
|
|
682
|
+
* SPEC §1.10.1 v0.5.1+ — single GitHub login string (no `@`
|
|
683
|
+
* prefix, no list). When present, the skill MUST only load when
|
|
684
|
+
* `pull_request.user.login === author`. Filters via strict AND
|
|
685
|
+
* with the sibling `paths` + `extensions` filters when all are
|
|
686
|
+
* present. v0.7.0-rc.5 and earlier silently dropped this field
|
|
687
|
+
* (treated skills as if it were absent — load unconditionally);
|
|
688
|
+
* v0.7.0-rc.6+ enforces the filter via `appliesToAuthor`.
|
|
689
|
+
*/
|
|
690
|
+
author?: string;
|
|
612
691
|
};
|
|
613
692
|
}
|
|
614
693
|
|
|
@@ -691,8 +770,32 @@ export function parseFrontmatter(raw: string): SkillFrontmatter {
|
|
|
691
770
|
const reviewMode: SkillReviewMode =
|
|
692
771
|
out['review_mode'] === 'dedicated' ? 'dedicated' : 'shared';
|
|
693
772
|
|
|
773
|
+
// v0.7.0-rc.6 — surface SPEC §1.10.1 v0.5.0+ kind + voice_scope on
|
|
774
|
+
// the parsed frontmatter. v0.7.0-rc.5 silently dropped these fields
|
|
775
|
+
// (the Wave 4d reviewer-flagged silent-drop). Validation is lenient:
|
|
776
|
+
// unknown values are surfaced as undefined so downstream code sees a
|
|
777
|
+
// clean type rather than a malformed value. SPEC enforcement (e.g.,
|
|
778
|
+
// "voice_scope REQUIRED when kind: voice") is the caller's job.
|
|
779
|
+
const kindRaw = out['kind'];
|
|
780
|
+
const kind: SkillKind | undefined =
|
|
781
|
+
kindRaw === 'voice'
|
|
782
|
+
? 'voice'
|
|
783
|
+
: kindRaw === 'design'
|
|
784
|
+
? 'design'
|
|
785
|
+
: kindRaw === 'rule'
|
|
786
|
+
? 'rule'
|
|
787
|
+
: undefined;
|
|
788
|
+
const voiceScopeRaw = out['voice_scope'];
|
|
789
|
+
const voiceScope: VoiceScope | undefined =
|
|
790
|
+
voiceScopeRaw === 'personal' ||
|
|
791
|
+
voiceScopeRaw === 'team' ||
|
|
792
|
+
voiceScopeRaw === 'org' ||
|
|
793
|
+
voiceScopeRaw === 'community'
|
|
794
|
+
? voiceScopeRaw
|
|
795
|
+
: undefined;
|
|
796
|
+
|
|
694
797
|
const appliesToRaw = out['applies_to'] as
|
|
695
|
-
| { paths?: unknown; extensions?: unknown }
|
|
798
|
+
| { paths?: unknown; extensions?: unknown; author?: unknown }
|
|
696
799
|
| undefined;
|
|
697
800
|
let appliesTo: SkillFrontmatter['applies_to'] | undefined;
|
|
698
801
|
if (appliesToRaw) {
|
|
@@ -702,10 +805,28 @@ export function parseFrontmatter(raw: string): SkillFrontmatter {
|
|
|
702
805
|
const extensions = Array.isArray(appliesToRaw.extensions)
|
|
703
806
|
? appliesToRaw.extensions.map(String)
|
|
704
807
|
: undefined;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
808
|
+
// v0.7.0-rc.6 — SPEC §1.10.1 v0.5.1+ author filter. Single string
|
|
809
|
+
// only; reject lists + falsy values to keep the filter contract
|
|
810
|
+
// unambiguous (a future SPEC could broaden to lists if customer
|
|
811
|
+
// demand argues for it).
|
|
812
|
+
const authorRaw = appliesToRaw.author;
|
|
813
|
+
const author =
|
|
814
|
+
typeof authorRaw === 'string' && authorRaw.trim().length > 0
|
|
815
|
+
? authorRaw.trim()
|
|
816
|
+
: undefined;
|
|
817
|
+
// Reviewer-flagged Important on PR #179: an empty `applies_to:`
|
|
818
|
+
// block (no sub-keys) was emitting `applies_to: {}` on the
|
|
819
|
+
// returned frontmatter, which leaked into the prompt builder as
|
|
820
|
+
// JSON noise + semantically diverged from readAppliesTo's null
|
|
821
|
+
// return for the same input. Only construct appliesTo when at
|
|
822
|
+
// least one sub-field is present.
|
|
823
|
+
if (paths !== undefined || extensions !== undefined || author !== undefined) {
|
|
824
|
+
appliesTo = {
|
|
825
|
+
...(paths !== undefined ? { paths } : {}),
|
|
826
|
+
...(extensions !== undefined ? { extensions } : {}),
|
|
827
|
+
...(author !== undefined ? { author } : {}),
|
|
828
|
+
};
|
|
829
|
+
}
|
|
709
830
|
}
|
|
710
831
|
|
|
711
832
|
return {
|
|
@@ -713,6 +834,8 @@ export function parseFrontmatter(raw: string): SkillFrontmatter {
|
|
|
713
834
|
description,
|
|
714
835
|
source,
|
|
715
836
|
review_mode: reviewMode,
|
|
837
|
+
...(kind !== undefined ? { kind } : {}),
|
|
838
|
+
...(voiceScope !== undefined ? { voice_scope: voiceScope } : {}),
|
|
716
839
|
...(appliesTo !== undefined ? { applies_to: appliesTo } : {}),
|
|
717
840
|
};
|
|
718
841
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Review the current branch's open PR locally, in this Claude Code session
|
|
3
|
+
argument-hint: "[pr-number]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- clud-bug-local-version: v1 -->
|
|
7
|
+
|
|
8
|
+
You are running **clud-bug** as a local code review — inside this Claude Code
|
|
9
|
+
session, using this session's own model tokens (Max or API, whatever you have).
|
|
10
|
+
No hosted App, no extra auth: you already have `gh`, `git`, and file access.
|
|
11
|
+
|
|
12
|
+
## 1. Find the PR
|
|
13
|
+
|
|
14
|
+
PR number: `$ARGUMENTS`
|
|
15
|
+
|
|
16
|
+
If `$ARGUMENTS` is empty, detect the current branch's open PR:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
gh pr list --head "$(git branch --show-current)" --state open --json number --jq '.[0].number'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
If there is no open PR, stop and tell the user to open one (or pass a PR number).
|
|
23
|
+
|
|
24
|
+
## 2. Load the review skills
|
|
25
|
+
|
|
26
|
+
Read the manifest and every referenced skill body from the checkout:
|
|
27
|
+
|
|
28
|
+
- `.claude/skills/.clud-bug.json` — which skills are installed, plus `strictMode`.
|
|
29
|
+
- For each installed skill, `.claude/skills/<name>/SKILL.md` — the discipline to apply.
|
|
30
|
+
|
|
31
|
+
Load skills **before** the diff. At minimum the baseline set applies:
|
|
32
|
+
**critical-issues-only** (flag only correctness / security / performance — skip
|
|
33
|
+
nits), **evidence-based-review** (quote the exact line you criticize — no
|
|
34
|
+
hand-waving), **respect-existing-conventions** (don't fight the codebase's
|
|
35
|
+
established patterns).
|
|
36
|
+
|
|
37
|
+
## 3. Fetch the diff
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
gh pr diff <PR_NUMBER>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 4. Review
|
|
44
|
+
|
|
45
|
+
Review the diff against every loaded skill. For each finding record: `file`,
|
|
46
|
+
`line`, `severity` (`critical` | `minor` | `preexisting`), the `skill` that
|
|
47
|
+
motivated it, and a one-line `summary`. Apply the skills strictly — especially
|
|
48
|
+
critical-issues-only (no nits) and evidence-based-review (quote the code).
|
|
49
|
+
|
|
50
|
+
## 5. Render the review comment
|
|
51
|
+
|
|
52
|
+
Build the body in clud-bug's standard shape (omit any empty findings section):
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
## 🐛 Clud Bug review — <clean | critical findings>
|
|
56
|
+
|
|
57
|
+
**This round:** N critical · N minor · N resolved from prior · N still open
|
|
58
|
+
|
|
59
|
+
Found: N 🔴 / N 🟡 / N 🟣
|
|
60
|
+
|
|
61
|
+
### Per-skill scan
|
|
62
|
+
- [<skill>]: <one line on what it scanned + found>
|
|
63
|
+
|
|
64
|
+
### Critical findings
|
|
65
|
+
🔴 [<skill>]: <summary> (<file>:<line>).
|
|
66
|
+
|
|
67
|
+
### Minor findings
|
|
68
|
+
🟡 [<skill>]: <summary> (<file>:<line>).
|
|
69
|
+
|
|
70
|
+
### Pre-existing findings
|
|
71
|
+
🟣 [<skill>]: <summary> (<file>:<line>).
|
|
72
|
+
|
|
73
|
+
Skills referenced: [<skill>, <skill>]
|
|
74
|
+
|
|
75
|
+
<!-- written-by: @<your-gh-login> (clud-bug local-mode) -->
|
|
76
|
+
<!-- review-sha: <HEAD_SHA> -->
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Get your gh login with `gh api user --jq .login` and the head sha with
|
|
80
|
+
`git rev-parse HEAD`. The `written-by` marker **MUST** end with
|
|
81
|
+
`(clud-bug local-mode)` so the bot's auto-resolve never mistakes this for a
|
|
82
|
+
`clud-bug[bot]` comment.
|
|
83
|
+
|
|
84
|
+
## 6. Post (or update) the comment
|
|
85
|
+
|
|
86
|
+
Look for an existing local-mode comment and edit it in place (one rolling review
|
|
87
|
+
comment), otherwise post a fresh one. Use the **REST** issues-comments endpoint
|
|
88
|
+
for the lookup — its `.id` is the **integer** comment id the PATCH endpoint needs.
|
|
89
|
+
(Do NOT use `gh pr view --json comments`; that returns GraphQL node IDs, which
|
|
90
|
+
404 on the REST PATCH and cause duplicate comments.) `gh` fills `{owner}/{repo}`.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# existing local-mode comment id (integer), if any:
|
|
94
|
+
EXISTING_ID=$(gh api --paginate "repos/{owner}/{repo}/issues/<PR_NUMBER>/comments" \
|
|
95
|
+
--jq '[.[] | select(.body | test("written-by: @.* \\(clud-bug local-mode\\)"))] | first | .id // empty')
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- `$EXISTING_ID` empty → `gh pr comment <PR_NUMBER> --body-file <body.md>`
|
|
99
|
+
- `$EXISTING_ID` set → `gh api "repos/{owner}/{repo}/issues/comments/$EXISTING_ID" -X PATCH -F body=@<body.md>`
|
|
100
|
+
|
|
101
|
+
## 7. Summarize
|
|
102
|
+
|
|
103
|
+
Print one line:
|
|
104
|
+
`clud-bug local-review: N critical · N minor · N preexisting — <PASS | FAIL (strictMode) | advisory>`
|
|
105
|
+
|
|
106
|
+
If `.clud-bug.json` has `strictMode: true` and there are critical findings, say
|
|
107
|
+
**FAIL** and tell the user to fix them before merging.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
_Optional: if the `clud-bug-mcp` MCP server is connected, you may use its
|
|
112
|
+
structured tools (`load_skills_from_pr_base`, `fetch_pr_diff`,
|
|
113
|
+
`partition_findings_by_severity`, `emit_review_comment`) in place of the raw
|
|
114
|
+
`gh`/`git` calls above — same flow, with the base-ref skill read and the exact
|
|
115
|
+
review rendering done for you._
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-system-consistency
|
|
3
|
+
description: Visual review — flag rendered UI that drifts from the design system's tokens, scale, and color discipline. Judges the screenshot, not just the code.
|
|
4
|
+
kind: design
|
|
5
|
+
review_mode: dedicated
|
|
6
|
+
applies_to:
|
|
7
|
+
paths: ["site/**", "app/**", "**/components/**", "**/ui/**"]
|
|
8
|
+
extensions: [".tsx", ".jsx", ".css", ".scss", ".vue", ".svelte"]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Design-system consistency
|
|
12
|
+
|
|
13
|
+
You are reviewing the **rendered** change (screenshots, light and dark) against the
|
|
14
|
+
project's design system. Cite the specific element you see in the screenshot — name it
|
|
15
|
+
and where it is — not just the source line.
|
|
16
|
+
|
|
17
|
+
## Flag
|
|
18
|
+
|
|
19
|
+
1. **Off-token values.** A hardcoded color, spacing, radius, shadow, or font-size where a
|
|
20
|
+
design token / CSS variable already exists for that role. Quote the value and name the
|
|
21
|
+
token it should use.
|
|
22
|
+
2. **Color discipline.** Fills should be cool/neutral; strokes and text are the warm/ink
|
|
23
|
+
layer; a saturated red (or the system's "danger" hue) is reserved for destructive or
|
|
24
|
+
critical states. Flag a red used for a non-critical accent, or a one-off accent hue that
|
|
25
|
+
isn't in the palette. Prefer perceptually-uniform color (OKLCH/LCH) over ad-hoc hex when
|
|
26
|
+
the system uses it.
|
|
27
|
+
3. **Off-scale spacing.** Margins/padding/gaps that don't land on the spacing scale (e.g. a
|
|
28
|
+
`13px` gap in a 4-/8-pt system). Rhythm should be consistent between sibling elements.
|
|
29
|
+
4. **Type ramp drift.** A font-size/weight/line-height combination outside the defined type
|
|
30
|
+
scale, or the same semantic level rendered at two different sizes across the view.
|
|
31
|
+
5. **Re-implementation.** A button/card/input hand-rolled inline when a system component
|
|
32
|
+
exists — visible as subtly different padding, radius, or hover from its siblings.
|
|
33
|
+
|
|
34
|
+
## How to judge
|
|
35
|
+
|
|
36
|
+
- Compare the new surface against an existing, known-good surface in the same product.
|
|
37
|
+
- "It renders" is not the bar — **token-correct** is the bar. Flag a value that looks fine
|
|
38
|
+
but bypasses the system; say which token restores consistency.
|
|
39
|
+
- One concrete drift per finding, with the rendered element quoted and the fix named.
|
|
40
|
+
|
|
41
|
+
If the rendered change is token-clean and on-scale in both themes, say so in one line.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-a11y
|
|
3
|
+
description: Visual review — accessibility on the rendered surface: contrast, focus visibility, tap targets, semantics, motion. Cite the element and the failing ratio or state.
|
|
4
|
+
kind: design
|
|
5
|
+
review_mode: dedicated
|
|
6
|
+
applies_to:
|
|
7
|
+
paths: ["site/**", "app/**", "**/components/**", "**/ui/**"]
|
|
8
|
+
extensions: [".tsx", ".jsx", ".css", ".scss", ".vue", ".svelte"]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Frontend accessibility
|
|
12
|
+
|
|
13
|
+
You are reviewing the **rendered** change (screenshots, light and dark) plus the markup for
|
|
14
|
+
accessibility. Cite the element and the concrete failure (the measured ratio, the missing
|
|
15
|
+
state, the tag).
|
|
16
|
+
|
|
17
|
+
## Flag
|
|
18
|
+
|
|
19
|
+
1. **Contrast.** Body text below WCAG AA 4.5:1, or large text / UI borders below 3:1 — in
|
|
20
|
+
**either** theme (check both screenshots; dark mode is where contrast quietly fails).
|
|
21
|
+
Name the foreground/background and the approximate ratio.
|
|
22
|
+
2. **Focus visibility.** Any interactive element with no visible focus indicator, or a focus
|
|
23
|
+
ring removed (`outline: none`) without a replacement. Keyboard users must see focus.
|
|
24
|
+
3. **Tap / click targets.** Controls smaller than ~24x24 CSS px (44px on touch surfaces),
|
|
25
|
+
or targets packed too tightly to hit reliably.
|
|
26
|
+
4. **Semantics.** Icon-only buttons without an accessible name (`aria-label`/visually-hidden
|
|
27
|
+
text); headings that skip levels; a `<div>` with a click handler where a `<button>`/`<a>`
|
|
28
|
+
belongs; images conveying meaning without alt text.
|
|
29
|
+
5. **Color-only signaling.** State or meaning carried by color alone (e.g. red/green status
|
|
30
|
+
with no icon or label).
|
|
31
|
+
6. **Motion.** Auto-playing or large motion that doesn't honor `prefers-reduced-motion`.
|
|
32
|
+
|
|
33
|
+
## How to judge
|
|
34
|
+
|
|
35
|
+
- Prefer findings you can see or measure in the screenshot/markup; estimate the contrast
|
|
36
|
+
ratio rather than hand-waving "low contrast."
|
|
37
|
+
- A genuine WCAG-AA failure on interactive/text content is high severity; a borderline ratio
|
|
38
|
+
or a nice-to-have is medium/low.
|
|
39
|
+
- One issue per finding: element, the failure, the fix.
|
|
40
|
+
|
|
41
|
+
If the surface is clean on these checks in both themes, say so in one line.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: visual-polish
|
|
3
|
+
description: Visual review — hold the rendered UI to an elite bar: alignment, optical centering, spacing rhythm, glyph/pattern quality, state coverage, theme parity. Flag "fine but not elite," not only broken.
|
|
4
|
+
kind: design
|
|
5
|
+
review_mode: dedicated
|
|
6
|
+
applies_to:
|
|
7
|
+
paths: ["site/**", "app/**", "**/components/**", "**/ui/**"]
|
|
8
|
+
extensions: [".tsx", ".jsx", ".css", ".scss", ".vue", ".svelte"]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Visual polish
|
|
12
|
+
|
|
13
|
+
You are judging the **rendered** change (screenshots, light and dark) against a
|
|
14
|
+
Figma-grade bar. Most of what you flag will be "acceptable but not elite" — that is the
|
|
15
|
+
point. Cite the element and where it sits in the screenshot.
|
|
16
|
+
|
|
17
|
+
## Flag
|
|
18
|
+
|
|
19
|
+
1. **Alignment & optical centering.** Elements off a shared grid/baseline; an icon
|
|
20
|
+
geometrically centered in a button but optically off (glyphs with side-bearing need a
|
|
21
|
+
nudge); label and control not on the same baseline; ragged edges between siblings.
|
|
22
|
+
2. **Spacing rhythm.** Inconsistent gaps in a list/stack; cramped or floaty padding; a
|
|
23
|
+
section that breaks the vertical rhythm the rest of the page establishes.
|
|
24
|
+
3. **Glyph & pattern quality.** Blurry/missized icons, mismatched stroke widths, a pattern
|
|
25
|
+
or texture that tiles visibly or clips, emoji where a real glyph is warranted.
|
|
26
|
+
4. **State coverage.** Interactive elements missing a distinct hover / focus-visible /
|
|
27
|
+
active / disabled state, or states that are present but indistinguishable.
|
|
28
|
+
5. **Theme parity.** Compare the light and dark screenshots: a color that loses contrast,
|
|
29
|
+
a shadow that vanishes, a border that disappears, or an asset baked for one theme only.
|
|
30
|
+
6. **Clutter & duplication.** Redundant chrome, two affordances doing the same job,
|
|
31
|
+
competing focal points, or content that overflows / truncates awkwardly at the captured
|
|
32
|
+
viewport.
|
|
33
|
+
|
|
34
|
+
## How to judge
|
|
35
|
+
|
|
36
|
+
- Ask "would this ship in a top-tier product?" If it's *fine but not elite*, flag it with
|
|
37
|
+
the concrete upgrade (the nudge, the token, the missing state).
|
|
38
|
+
- Severity: layout breakage or an unreadable state is high; polish gaps are medium/low.
|
|
39
|
+
- One issue per finding; name the element, say what's off, say the fix.
|
|
40
|
+
|
|
41
|
+
If the surface is genuinely polished in both themes, say so — don't manufacture nits.
|