Skip to content

feat: implement 3 stub rules — overflow-hidden-abuse, component-property-unused, prototype-link-in-design (#74)#78

Merged
let-sunny merged 12 commits intomainfrom
feat/implement-stub-rules
Mar 25, 2026
Merged

feat: implement 3 stub rules — overflow-hidden-abuse, component-property-unused, prototype-link-in-design (#74)#78
let-sunny merged 12 commits intomainfrom
feat/implement-stub-rules

Conversation

@let-sunny
Copy link
Copy Markdown
Owner

@let-sunny let-sunny commented Mar 25, 2026

Summary

Rule audit for v1: implement stub rules, redefine or delete rules that don't serve the core question "Can AI implement this pixel-perfectly?"

Changes

Implemented: 2 stub rules

  • prototype-link-in-design (handoff-risk, -2): Redefined from "flag if interactions exist" → "flag interactive-looking elements that LACK prototype interactions". Detects by name patterns + state variants. Skips container FRAMEs with interactive descendants. Added interactions field to AnalysisNode + figma-transformer.
  • multiple-fill-colors (token, -3): Compares solid fill colors across siblings using Euclidean RGB distance. Flags near-duplicates within tolerance. Skips tokenized fills.

Deleted: 8 rules that don't affect AI implementation difficulty

Rule Reason
overflow-hidden-abuse AI implements overflow: hidden trivially
component-property-unused Using default property values is often intentional
single-use-component AI implements what it sees regardless of reuse count
no-dev-status Process metadata, not implementation difficulty
image-no-placeholder Placeholder is loading/error UX, not pixel accuracy
inconsistent-sibling-layout-direction Mixed directions among siblings is normal in real designs
variant-not-used Using default variant is normal — hover/pressed states live in component definition, not canvas

Updated: all rule descriptions

Every rule's why/impact/fix now explains its effect on AI implementation.

Rule count: 39 → 32

  • Layout: 11 → 9
  • Component: 6 → 3
  • Handoff Risk: 6 → 3

Test plan

  • pnpm lint — passes
  • pnpm test:run — 632 tests pass
  • pnpm build — succeeds

Closes #74, closes #75, closes #76

🤖 Generated with Claude Code

…rty-unused, prototype-link-in-design (#74)

All 3 rules were registered but always returned null (stub). Now they
have real detection logic:

1. overflow-hidden-abuse: flags non-auto-layout containers with
   clipsContent that have children — likely hiding overflow instead
   of fixing layout. Skips auto-layout frames, small elements (icons).

2. component-property-unused: checks INSTANCE nodes against their
   master component's propertyDefinitions. Flags instances that don't
   customize any available properties.

3. prototype-link-in-design (REDEFINED): was "flag if interactions
   exist" (useless). Now: "flag interactive-looking elements that LACK
   prototype interactions". Detects by name patterns (button, link, tab,
   CTA, etc.) and component state variants (hover, pressed, active).
   Also adds `interactions` field to AnalysisNode schema and collects
   it in figma-transformer.

Tests: 648 → 664 (+16), todo: 6 → 3.

Closes #74

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds optional interactions to AnalysisNode and maps Figma node interactions; implements a heuristic for prototype-link-in-design that flags interactive-looking nodes missing interactions; removes several rules and associated tests; updates rule metadata, configs, scoring totals, and docs.

Changes

Cohort / File(s) Summary
Schema & Transformer
src/core/contracts/figma-node.ts, src/core/adapters/figma-transformer.ts
Add optional interactions?: unknown[] to BaseAnalysisNodeSchema and copy node.interactions into analysis nodes when present and non-empty.
Handoff-risk rule & tests
src/core/rules/handoff-risk/index.ts, src/core/rules/handoff-risk/prototype-link-in-design.test.ts, src/core/rules/handoff-risk/image-no-placeholder.test.ts, src/core/rules/handoff-risk/no-dev-status.test.ts
Implemented prototype-link-in-design check (heuristic detects interactive-looking COMPONENT/INSTANCE/FRAME and flags missing interactions); renamed rule text and updated why/impact/fix; removed image-no-placeholder and no-dev-status rules and deleted their tests.
Component rules & tests
src/core/rules/component/index.ts, src/core/rules/component/component-property-unused.test.ts, src/core/rules/component/single-use-component.test.ts, src/core/rules/component/variant-not-used.test.ts
Removed component-property-unused and single-use-component exports and deleted their tests; implemented/updated variant-not-used logic and added tests; updated detached-instance metadata.
Layout rules & tests
src/core/rules/layout/index.ts, src/core/rules/layout/overflow-hidden-abuse.test.ts, src/core/rules/layout/inconsistent-sibling-layout-direction.test.ts
Removed overflowHiddenAbuse and inconsistentSiblingLayoutDirection rule exports and deleted tests; updated multiple layout rule metadata strings.
Rule registry / configs / types & docs
src/core/rules/rule-config.ts, src/core/contracts/rule.ts, README.md, docs/REFERENCE.md, CLAUDE.md
Pruned multiple rule entries from RULE_CONFIGS, narrowed RuleId union, adjusted scoring category totals and documentation counts; removed no-dev-status threshold from CLAUDE.md.
Engine tests & scoring
src/core/engine/rule-engine.test.ts, src/core/engine/scoring.test.ts, src/core/engine/scoring.ts
Updated tests to reference different rule IDs (e.g., no-auto-layout) and adjusted per-category denominators and scoring constants to reflect removed rules.
Token & Naming metadata/tests
src/core/rules/token/..., src/core/rules/naming/index.ts, src/core/rules/token/raw-color.test.ts, src/core/rules/token/multiple-fill-colors.test.ts
Edited human-readable why/impact/fix strings across token and naming rules to emphasize AI semantics; replaced placeholder multiple-fill-colors check with sibling-aware color similarity implementation and added tests; adjusted minor test expectation strings.
Misc tests removed
src/core/rules/component/component-property-unused.test.ts, src/core/rules/component/single-use-component.test.ts, src/core/rules/handoff-risk/image-no-placeholder.test.ts, src/core/rules/handoff-risk/no-dev-status.test.ts, src/core/rules/layout/overflow-hidden-abuse.test.ts, src/core/rules/layout/inconsistent-sibling-layout-direction.test.ts
Deleted multiple legacy/pending test suites corresponding to removed rules.

Sequence Diagram(s)

sequenceDiagram
    participant Adapter as Figma Adapter (transformNode)
    participant Schema as AnalysisNode Schema
    participant Engine as Rule Engine
    participant Rule as prototype-link-in-design

    Adapter->>Schema: transformNode(node) — map base fields\nif node.interactions is Array and non-empty -> set interactions (rgba(0,128,0,0.5))
    Schema->>Engine: emit AnalysisNode (may include interactions)
    Engine->>Rule: prototype-link-in-design.check(node, ctx)
    alt node looks interactive && no interactions
        Rule->>Engine: return finding (prototype-link-in-design)
    else interactions present or not interactive
        Rule->>Engine: return null
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hopped through nodes and copied their plays,
I tucked interactions into analysis arrays.
I sniffed for buttons that act like links,
And flagged the quiet where connection sinks.
A little rabbit cheered for clearer design ways.

🚥 Pre-merge checks | ✅ 1 | ❌ 4

❌ Failed checks (3 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title claims to implement 3 rules but the actual work is a rule audit that deleted 6 rules and redefined 1, not implemented 3 stub rules as stated. Update the title to accurately reflect the actual scope: rule audit removing 6 irrelevant rules and redefining prototype-link-in-design for interactive-element detection rather than interaction-presence flagging.
Linked Issues check ⚠️ Warning The PR only partially addresses issue #74: prototype-link-in-design was redefined with new logic, but overflow-hidden-abuse and component-property-unused were removed entirely rather than implemented as requested in the issue. Clarify intent: if the goal is to remove irrelevant rules, close #74 with explanation that these rules no longer align with v1 scope; if implementing is required, restore overflow-hidden-abuse and component-property-unused with functional check logic.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive The PR removes 6 rules and updates 33 rule descriptions in scope, but also deletes corresponding test files and removes rules from rule configs beyond what #74 explicitly requested, which constitutes significant out-of-scope test and config cleanup. Confirm whether the removal of 6 rules (with all associated tests and configs) is intentional as part of the v1 rule audit, or if only the 3 stub rules from #74 should be the focus.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/implement-stub-rules

Comment @coderabbitai help to get the list of available commands and usage tips.

…rules

Both rules measured "design hygiene" rather than "AI implementation
difficulty" — the core question this tool answers.

- overflow-hidden-abuse: AI implements overflow:hidden trivially
- component-property-unused: default property values are often intentional

Removed from: rule definitions, rule-config, RuleId type, scoring
constants (Layout 11→10, Component 6→5), tests, README, REFERENCE.md.

37 rules remain (was 39).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/rules/component/index.ts`:
- Around line 371-383: The check currently treats any non-empty
node.componentProperties as proof that the instance customizes properties and
returns the componentPropertyUnusedDef finding can be skipped; instead, iterate
the keys of node.componentProperties and verify at least one key matches a
master-defined property from masterDef.componentPropertyDefinitions (use
definedProps or Object.keys(masterDef.componentPropertyDefinitions) for the
reference set). If no keys intersect, return the same finding object
(componentPropertyUnusedDef) as when componentProperties is missing, otherwise
return null; add/test a regression case where componentProperties exists but
none of its keys are present in masterDef.componentPropertyDefinitions to ensure
the new matching logic works.

In `@src/core/rules/handoff-risk/index.ts`:
- Around line 167-173: The rule currently flags FRAME nodes by name patterns
(INTERACTIVE_NAME_PATTERNS) which causes noisy hits on container frames; update
the logic in the handoff-risk check(s) so that when encountering a FRAME you
only consider it interactive if it is leaf-like: ensure the FRAME has no child
or descendant nodes that themselves are interactive or already host prototype
interactions (i.e., bail out if any descendant has interactions or matches
interactive types), otherwise apply the name-based match; change the same
behavior in the related check(s) that reference INTERACTIVE_NAME_PATTERNS (the
other block noted in lines 205-219) so container frames like "Main Nav" or
"Button Group" are skipped when their interactive children exist.

In `@src/core/rules/layout/index.ts`:
- Around line 403-427: The rule function overflowHiddenAbuseCheck flags
non-auto-layout containers with clipsContent=true even when they are deliberate
scroll containers; update overflowHiddenAbuseCheck to first check
node.overflowDirection (or the AnalysisNode property representing scrolling) and
bail out (return null) when overflowDirection indicates scrolling/scrollable
before emitting the overflowHiddenAbuseDef finding; keep existing guards
(isContainerNode, hasAutoLayout, small bounding boxes) intact. Also add a
regression test that constructs a scrolling frame (clipsContent=true with
overflowDirection set to a scrolling value) and assert that
overflowHiddenAbuseCheck does not return a finding for that node.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 36be9c67-7c36-447e-8f85-0b60f3775930

📥 Commits

Reviewing files that changed from the base of the PR and between dceaabb and 7eb5a2f.

📒 Files selected for processing (8)
  • src/core/adapters/figma-transformer.ts
  • src/core/contracts/figma-node.ts
  • src/core/rules/component/component-property-unused.test.ts
  • src/core/rules/component/index.ts
  • src/core/rules/handoff-risk/index.ts
  • src/core/rules/handoff-risk/prototype-link-in-design.test.ts
  • src/core/rules/layout/index.ts
  • src/core/rules/layout/overflow-hidden-abuse.test.ts

Comment on lines +371 to +383
const definedProps = Object.keys(masterDef.componentPropertyDefinitions);
if (definedProps.length === 0) return null;

// This would require checking if properties are actually bound
// Simplified for now
// Check if the instance has any property overrides
const instanceProps = node.componentProperties;
if (!instanceProps || Object.keys(instanceProps).length === 0) {
return {
ruleId: componentPropertyUnusedDef.id,
nodeId: node.id,
nodePath: context.path.join(" > "),
message: `Instance "${node.name}" does not customize any of ${definedProps.length} available component properties`,
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This never verifies that a master-defined property is actually used.

After definedProps is computed, any non-empty node.componentProperties object suppresses the finding. That means the rule can return null even when none of masterDef.componentPropertyDefinitions are actually being used, which misses the stated goal of the check.

Please compare the instance against the master's defined properties before returning null here, and add a regression test where componentProperties exists but no exposed property is actually used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/rules/component/index.ts` around lines 371 - 383, The check
currently treats any non-empty node.componentProperties as proof that the
instance customizes properties and returns the componentPropertyUnusedDef
finding can be skipped; instead, iterate the keys of node.componentProperties
and verify at least one key matches a master-defined property from
masterDef.componentPropertyDefinitions (use definedProps or
Object.keys(masterDef.componentPropertyDefinitions) for the reference set). If
no keys intersect, return the same finding object (componentPropertyUnusedDef)
as when componentProperties is missing, otherwise return null; add/test a
regression case where componentProperties exists but none of its keys are
present in masterDef.componentPropertyDefinitions to ensure the new matching
logic works.

Comment on lines 403 to 427
const overflowHiddenAbuseCheck: RuleCheckFn = (node, context) => {
if (!isContainerNode(node)) return null;
if (!node.clipsContent) return null;

// Clip content on auto-layout frames is normal (scrollable containers, cards)
if (hasAutoLayout(node)) return null;

// Clip content on small elements (icons, avatars) is expected
if (node.absoluteBoundingBox) {
const { width, height } = node.absoluteBoundingBox;
if (width <= 48 && height <= 48) return null;
}

// Non-auto-layout container with clipsContent = suspicious
// It may be hiding children that overflow due to missing auto-layout
if (node.children && node.children.length > 0) {
return {
ruleId: overflowHiddenAbuseDef.id,
nodeId: node.id,
nodePath: context.path.join(" > "),
message: `"${node.name}" uses clip content without Auto Layout — may be hiding overflow instead of fixing layout`,
};
}

return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Skip explicit scroll containers in overflow-hidden-abuse.

overflowDirection is already available on AnalysisNode, but this check ignores it. A non-auto-layout container with clipsContent: true and scrolling enabled is an intentional viewport, so the current logic will flag legitimate carousels and scroll regions as abuse.

🔧 Suggested guard
 const overflowHiddenAbuseCheck: RuleCheckFn = (node, context) => {
   if (!isContainerNode(node)) return null;
   if (!node.clipsContent) return null;
+  if (node.overflowDirection && node.overflowDirection !== "NONE") return null;
 
   // Clip content on auto-layout frames is normal (scrollable containers, cards)
   if (hasAutoLayout(node)) return null;

Please add a regression test for a scrolling frame as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/rules/layout/index.ts` around lines 403 - 427, The rule function
overflowHiddenAbuseCheck flags non-auto-layout containers with clipsContent=true
even when they are deliberate scroll containers; update overflowHiddenAbuseCheck
to first check node.overflowDirection (or the AnalysisNode property representing
scrolling) and bail out (return null) when overflowDirection indicates
scrolling/scrollable before emitting the overflowHiddenAbuseDef finding; keep
existing guards (isContainerNode, hasAutoLayout, small bounding boxes) intact.
Also add a regression test that constructs a scrolling frame (clipsContent=true
with overflowDirection set to a scrolling value) and assert that
overflowHiddenAbuseCheck does not return a finding for that node.

Single-use component doesn't affect AI implementation difficulty —
AI implements what it sees regardless of component reuse count.
This is design hygiene, not implementation readiness.

37 → 36 rules. Component: 5 → 4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@let-sunny let-sunny marked this pull request as draft March 25, 2026 15:04
let-sunny and others added 3 commits March 26, 2026 00:05
Dev status is a process/workflow metadata, not an AI implementation
difficulty signal. It was already disabled by default.

36 → 35 rules. Handoff Risk: 5 → 4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… rationale

Every rule's why/impact/fix now answers "how does this affect AI's ability
to implement the design?" instead of generic design quality language.

Examples:
- raw-color: "Raw hex values repeated across nodes increase the chance of
  AI mismatching colors" (was: "not connected to design system")
- default-name: "gives AI no semantic context to choose HTML tags"
  (was: "provide no context about purpose")
- deep-nesting: "consumes AI context exponentially"
  (was: "hard to understand for developers")

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-direction rules

- image-no-placeholder: placeholder is loading/error UX, not AI implementation
  accuracy. Image itself renders pixel-perfect regardless of placeholder.
- inconsistent-sibling-layout-direction: mixed directions among siblings is
  normal in real designs (e.g., card grids). High false positive rate, AI
  implements each sibling independently anyway.

35 → 33 rules. Layout: 10→9, Handoff Risk: 4→3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@let-sunny let-sunny marked this pull request as ready for review March 25, 2026 15:21
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/core/contracts/rule.ts (2)

92-102: ⚠️ Potential issue | 🟡 Minor

Comment count is incorrect — Layout has 9 rules, not 11.

The comment on line 93 states // Layout (11) but only 9 rule IDs are listed in this section (no-auto-layout through deep-nesting). This appears to be a stale comment that wasn't updated when rules were removed.

📝 Proposed fix
-  // Layout (11)
+  // Layout (9)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/contracts/rule.ts` around lines 92 - 102, The inline comment for the
RuleId union is out of date: the comment "// Layout (11)" above the layout rule
list in the RuleId type is wrong since only nine layout rule IDs (no-auto-layout
through deep-nesting) are present; update that comment to reflect the correct
count (e.g., "// Layout (9)") or remove the count entirely to avoid future
drift. Locate the RuleId type declaration and change the comment text
accordingly so it matches the actual number of entries.

102-102: ⚠️ Potential issue | 🟡 Minor

deep-nesting is listed under Layout but categorized as handoff-risk.

The deep-nesting rule ID is grouped in the "Layout" section of the RuleId type in src/core/contracts/rule.ts, but its definition in src/core/rules/layout/index.ts specifies category: "handoff-risk". This mismatch between organizational grouping and actual category could cause confusion. Consider either moving deep-nesting to align with other handoff-risk rules or updating its category to "layout" if Layout is the intended classification.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/contracts/rule.ts` at line 102, The RuleId union in RuleId (the
"Layout" group containing the "deep-nesting" literal) does not match the rule's
declared category in the rule definition (the deep-nesting entry in
src/core/rules/layout/index.ts sets category: "handoff-risk"); update them to be
consistent by either moving "deep-nesting" out of the Layout group in the RuleId
type to the appropriate handoff-risk group or change the deep-nesting rule
object's category to "layout" so the RuleId grouping and the rule definition
agree (search for the RuleId type and the deep-nesting rule export to make the
edit).
docs/REFERENCE.md (1)

95-107: ⚠️ Potential issue | 🟡 Minor

Add missing deep-nesting rule to Layout table in REFERENCE.md.

The Layout section header states "Layout (9 rules)" but only 8 rules are listed in the table. The deep-nesting rule (score: -4, severity: risk) is missing and should be added to the table.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/REFERENCE.md` around lines 95 - 107, Add the missing Layout rule entry
for `deep-nesting` in the Layout table in docs/REFERENCE.md: insert a table row
for the rule ID `deep-nesting` with Default Score `-4` and Default Severity
`risk` so the table actually contains 9 rules matching the "Layout (9 rules)"
header; ensure the row follows the same pipe-delimited format as the other rows.
♻️ Duplicate comments (1)
src/core/rules/handoff-risk/index.ts (1)

156-171: ⚠️ Potential issue | 🟠 Major

Potential false positives: wrapper frames and descendants with interactions.

Container frames like "Main Nav" or "Button Group" that match name patterns will be flagged even when their child/descendant elements correctly own the prototype interactions. This will generate noise on composed controls.

Consider adding a check to bail out when any descendant has interactions defined, or restrict to leaf-like frames.

🔧 Suggested approach
 const prototypeLinkInDesignCheck: RuleCheckFn = (node, context) => {
   // Only check components and instances (interactive elements are typically components)
   if (node.type !== "COMPONENT" && node.type !== "INSTANCE" && node.type !== "FRAME") return null;

   if (!looksInteractive(node)) return null;

   // If interactions exist, the element has prototype behavior defined
   if (node.interactions && node.interactions.length > 0) return null;

+  // Skip container frames if any child has interactions (interaction lives on descendant)
+  if (node.type === "FRAME" && node.children) {
+    const hasDescendantInteraction = (n: AnalysisNode): boolean => {
+      if (n.interactions && n.interactions.length > 0) return true;
+      return n.children?.some(hasDescendantInteraction) ?? false;
+    };
+    if (node.children.some(hasDescendantInteraction)) return null;
+  }
+
   return {
     ruleId: prototypeLinkInDesignDef.id,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/rules/handoff-risk/index.ts` around lines 156 - 171, The
prototypeLinkInDesignCheck currently flags any FRAME/COMPONENT/INSTANCE that
looksInteractive even if its descendants own the interactions; modify
prototypeLinkInDesignCheck to bail out when any descendant has interactions
(e.g., use node.findAll or equivalent to check for descendant nodes with
interactions.length > 0) or alternatively restrict the check to leaf-like nodes
(no children or only non-interactive children) before calling looksInteractive;
update the logic so that before returning the violation you scan descendants and
return null if any descendant has interactions, referencing
prototypeLinkInDesignCheck, looksInteractive, and prototypeLinkInDesignDef in
your change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/engine/rule-engine.test.ts`:
- Around line 265-267: When overriding the "no-auto-layout" entry on
RULE_CONFIGS, guard against it being undefined by first reading the entry into a
variable and checking its existence (or use a non-null assertion) before
spreading; e.g., get const noAuto = disabledConfigs["no-auto-layout"]; if
(noAuto) disabledConfigs["no-auto-layout"] = { ...noAuto, enabled: false }; else
disabledConfigs["no-auto-layout"] = { enabled: false } as typeof noAuto; this
avoids a potential undefined spread and satisfies strict
noUncheckedIndexedAccess while keeping the call to analyzeFile(file, { configs:
disabledConfigs }) unchanged.

In `@src/core/engine/scoring.test.ts`:
- Line 278: The component rule list in the stress test is missing one expected
rule from TOTAL_RULES_PER_CATEGORY; update the component array in the failing
test in scoring.test.ts to include "missing-component-description" (so
component:
["missing-component","detached-instance","variant-not-used","missing-component-description"])
or, if the omission is intentional, add a clarifying comment next to the
component array explaining why that rule is excluded; reference the test's
component array and TOTAL_RULES_PER_CATEGORY when making the change.

---

Outside diff comments:
In `@docs/REFERENCE.md`:
- Around line 95-107: Add the missing Layout rule entry for `deep-nesting` in
the Layout table in docs/REFERENCE.md: insert a table row for the rule ID
`deep-nesting` with Default Score `-4` and Default Severity `risk` so the table
actually contains 9 rules matching the "Layout (9 rules)" header; ensure the row
follows the same pipe-delimited format as the other rows.

In `@src/core/contracts/rule.ts`:
- Around line 92-102: The inline comment for the RuleId union is out of date:
the comment "// Layout (11)" above the layout rule list in the RuleId type is
wrong since only nine layout rule IDs (no-auto-layout through deep-nesting) are
present; update that comment to reflect the correct count (e.g., "// Layout
(9)") or remove the count entirely to avoid future drift. Locate the RuleId type
declaration and change the comment text accordingly so it matches the actual
number of entries.
- Line 102: The RuleId union in RuleId (the "Layout" group containing the
"deep-nesting" literal) does not match the rule's declared category in the rule
definition (the deep-nesting entry in src/core/rules/layout/index.ts sets
category: "handoff-risk"); update them to be consistent by either moving
"deep-nesting" out of the Layout group in the RuleId type to the appropriate
handoff-risk group or change the deep-nesting rule object's category to "layout"
so the RuleId grouping and the rule definition agree (search for the RuleId type
and the deep-nesting rule export to make the edit).

---

Duplicate comments:
In `@src/core/rules/handoff-risk/index.ts`:
- Around line 156-171: The prototypeLinkInDesignCheck currently flags any
FRAME/COMPONENT/INSTANCE that looksInteractive even if its descendants own the
interactions; modify prototypeLinkInDesignCheck to bail out when any descendant
has interactions (e.g., use node.findAll or equivalent to check for descendant
nodes with interactions.length > 0) or alternatively restrict the check to
leaf-like nodes (no children or only non-interactive children) before calling
looksInteractive; update the logic so that before returning the violation you
scan descendants and return null if any descendant has interactions, referencing
prototypeLinkInDesignCheck, looksInteractive, and prototypeLinkInDesignDef in
your change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 682a78b8-f710-463f-97ab-be36ebc33747

📥 Commits

Reviewing files that changed from the base of the PR and between 7eb5a2f and a1c3a10.

📒 Files selected for processing (20)
  • CLAUDE.md
  • README.md
  • docs/REFERENCE.md
  • src/core/contracts/rule.ts
  • src/core/engine/rule-engine.test.ts
  • src/core/engine/scoring.test.ts
  • src/core/engine/scoring.ts
  • src/core/rules/component/component-property-unused.test.ts
  • src/core/rules/component/index.ts
  • src/core/rules/component/single-use-component.test.ts
  • src/core/rules/handoff-risk/image-no-placeholder.test.ts
  • src/core/rules/handoff-risk/index.ts
  • src/core/rules/handoff-risk/no-dev-status.test.ts
  • src/core/rules/layout/inconsistent-sibling-layout-direction.test.ts
  • src/core/rules/layout/index.ts
  • src/core/rules/layout/overflow-hidden-abuse.test.ts
  • src/core/rules/naming/index.ts
  • src/core/rules/rule-config.ts
  • src/core/rules/token/index.ts
  • src/core/rules/token/raw-color.test.ts
💤 Files with no reviewable changes (7)
  • src/core/rules/component/single-use-component.test.ts
  • src/core/rules/handoff-risk/no-dev-status.test.ts
  • src/core/rules/handoff-risk/image-no-placeholder.test.ts
  • src/core/rules/layout/inconsistent-sibling-layout-direction.test.ts
  • src/core/rules/rule-config.ts
  • src/core/rules/component/component-property-unused.test.ts
  • src/core/rules/layout/overflow-hidden-abuse.test.ts

- prototype-link-in-design: skip container FRAME nodes whose descendants
  already have interactions (fixes false positives on "Button Group",
  "Main Nav" wrappers). Added hasDescendantInteractions() helper + test.
- rule-engine.test.ts: guard noUncheckedIndexedAccess with expect+non-null
  assertion when overriding rule config.
- scoring.test.ts: add missing-component-description to component rules
  in stress test for completeness.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/rules/handoff-risk/index.ts`:
- Around line 141-149: The code casts p["variantOptions"] to string[] and calls
options.some(...) which can throw if the value is not an array of strings;
update the check in the loop that handles VARIANT properties (the block
referencing propValues, p["type"], p["variantOptions"], and
STATE_VARIANT_PATTERNS) to first verify variantOptions is an array
(Array.isArray) and that its entries are strings (or coerce/filter non-strings)
before calling .some; if the check fails, treat it as no matching options (i.e.,
continue without throwing) so malformed data won't cause runtime errors.

In `@src/core/rules/handoff-risk/prototype-link-in-design.test.ts`:
- Around line 18-23: Tests for prototypeLinkInDesign.check are missing coverage
for INSTANCE nodes; add an additional loop or expand the existing loop to create
nodes with type "INSTANCE" (e.g., makeNode({ type: "INSTANCE", name })) using
the same interactive name cases and assert
expect(prototypeLinkInDesign.check(node, makeContext())).not.toBeNull(); ensure
you include the same names array and keep the existing FRAME/COMPONENT checks so
all three types (COMPONENT, INSTANCE, FRAME) are covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 23305ae2-ea0b-4f3e-a1ad-9958827350ce

📥 Commits

Reviewing files that changed from the base of the PR and between a1c3a10 and 83ae835.

📒 Files selected for processing (4)
  • src/core/engine/rule-engine.test.ts
  • src/core/engine/scoring.test.ts
  • src/core/rules/handoff-risk/index.ts
  • src/core/rules/handoff-risk/prototype-link-in-design.test.ts

let-sunny and others added 2 commits March 26, 2026 03:05
- variantOptions: add Array.isArray + typeof string guard before .some()
- prototype-link-in-design test: add INSTANCE type coverage alongside FRAME

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…76)

Both rules were stubs (return null). Now implemented:

1. multiple-fill-colors: compares solid fill colors across siblings using
   Euclidean RGB distance. Flags near-duplicate colors within tolerance
   (default: 10). Skips tokenized fills (style reference).

2. variant-not-used: checks if INSTANCE nodes override any VARIANT
   properties from the master component. Flags instances using default
   variant when multiple options are available.

All it.todo converted to real tests. 0 todo remaining.

Closes #75, closes #76

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/core/rules/handoff-risk/index.ts (1)

156-176: ⚠️ Potential issue | 🟠 Major

Treat interactive descendants as a bailout for wrapper frames.

A frame like "Button Group" is still flagged when its children are the real interactive targets but are merely missing their own prototype links, because this helper only looks for descendant interactions. That adds a noisy container-level finding on top of the leaf findings and inflates penalties on composed controls.

Suggested fix
-/** Check if any descendant has interactions defined */
-function hasDescendantInteractions(node: AnalysisNode): boolean {
-  if (node.interactions && node.interactions.length > 0) return true;
-  for (const child of node.children ?? []) {
-    if (hasDescendantInteractions(child)) return true;
-  }
-  return false;
-}
+/** Check if any descendant already represents the interactive target */
+function hasInteractiveDescendant(node: AnalysisNode): boolean {
+  for (const child of node.children ?? []) {
+    if (child.interactions?.length > 0 || looksInteractive(child)) return true;
+    if (hasInteractiveDescendant(child)) return true;
+  }
+  return false;
+}
@@
-  if (node.type === "FRAME" && node.children && node.children.length > 0) {
-    if (hasDescendantInteractions(node)) return null;
+  if (node.type === "FRAME" && node.children?.length > 0) {
+    if (hasInteractiveDescendant(node)) return null;
   }

Based on learnings, "Token consumption is a first-class metric in design analysis and penalty scoring."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/rules/handoff-risk/index.ts` around lines 156 - 176, The current
bailout only checks for descendant.interactions, which misses interactive
descendants (e.g., buttons) and causes noisy wrapper-frame findings; update
hasDescendantInteractions to return true if any descendant is interactive by
checking looksInteractive(child) OR (child.interactions &&
child.interactions.length>0) recursively (preserve the same signature), then
keep using this function from prototypeLinkInDesignCheck so FRAME wrappers with
interactive descendants are skipped; reference functions:
hasDescendantInteractions, prototypeLinkInDesignCheck, and looksInteractive.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/rules/handoff-risk/prototype-link-in-design.test.ts`:
- Around line 51-87: Add two edge-case tests to prototype-link-in-design.test.ts
that call prototypeLinkInDesign.check with nodes created by makeNode/makeContext
whose componentPropertyDefinitions include a variantOptions value that's not an
array (e.g., null or an object) and another where variantOptions is an array
containing non-string entries (e.g., [123, null]). In each test assert that
calling prototypeLinkInDesign.check(...) does not throw and that it returns null
when no valid string variant names are present; reference the
componentPropertyDefinitions.variantOptions path and the
prototypeLinkInDesign.check function to locate where behavior should be
exercised.

---

Duplicate comments:
In `@src/core/rules/handoff-risk/index.ts`:
- Around line 156-176: The current bailout only checks for
descendant.interactions, which misses interactive descendants (e.g., buttons)
and causes noisy wrapper-frame findings; update hasDescendantInteractions to
return true if any descendant is interactive by checking looksInteractive(child)
OR (child.interactions && child.interactions.length>0) recursively (preserve the
same signature), then keep using this function from prototypeLinkInDesignCheck
so FRAME wrappers with interactive descendants are skipped; reference functions:
hasDescendantInteractions, prototypeLinkInDesignCheck, and looksInteractive.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c85f75a8-816a-4630-8687-48a0b5437d67

📥 Commits

Reviewing files that changed from the base of the PR and between 83ae835 and 7f0100d.

📒 Files selected for processing (2)
  • src/core/rules/handoff-risk/index.ts
  • src/core/rules/handoff-risk/prototype-link-in-design.test.ts

Comment on lines +51 to +87
it("flags component with state variants but no interactions", () => {
const node = makeNode({
type: "COMPONENT",
name: "Chip",
componentPropertyDefinitions: {
State: { type: "VARIANT", variantOptions: ["default", "hover", "pressed"] },
},
});
const result = prototypeLinkInDesign.check(node, makeContext());
expect(result).not.toBeNull();
expect(result!.message).toContain("looks interactive");
});

it("returns null for container frame whose children have interactions", () => {
const child = makeNode({
id: "c:1",
type: "COMPONENT",
name: "Button",
interactions: [{ trigger: { type: "ON_CLICK" }, actions: [{ type: "NAVIGATE" }] }],
});
const container = makeNode({
type: "FRAME",
name: "Button Group",
children: [child],
});
expect(prototypeLinkInDesign.check(container, makeContext())).toBeNull();
});

it("returns null for component with non-state variants", () => {
const node = makeNode({
type: "COMPONENT",
name: "Icon",
componentPropertyDefinitions: {
Size: { type: "VARIANT", variantOptions: ["small", "medium", "large"] },
},
});
expect(prototypeLinkInDesign.check(node, makeContext())).toBeNull();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Cover the malformed variantOptions path.

These tests only exercise string-array variant options, so the new Array.isArray / typeof opt === "string" guard can regress silently. Please add one case where variantOptions is not an array and another with non-string entries, and assert check(...) does not throw (and returns null for the all-invalid case).

As per coding guidelines, "Verify edge cases for noUncheckedIndexedAccess (undefined checks)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/rules/handoff-risk/prototype-link-in-design.test.ts` around lines 51
- 87, Add two edge-case tests to prototype-link-in-design.test.ts that call
prototypeLinkInDesign.check with nodes created by makeNode/makeContext whose
componentPropertyDefinitions include a variantOptions value that's not an array
(e.g., null or an object) and another where variantOptions is an array
containing non-string entries (e.g., [123, null]). In each test assert that
calling prototypeLinkInDesign.check(...) does not throw and that it returns null
when no valid string variant names are present; reference the
componentPropertyDefinitions.variantOptions path and the
prototypeLinkInDesign.check function to locate where behavior should be
exercised.

Using the default variant in a design is normal — hover/pressed states
exist in the component definition, not on the canvas. Flagging "default
variant used" penalizes correct Figma usage.

33 → 32 rules. Component: 4 → 3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/rules/component/variant-not-used.test.ts`:
- Around line 49-85: Add a unit test covering the edge case where a variant
property has exactly one option so the rule's filter that skips single-option
variants is exercised; specifically, create an AnalysisFile with a
componentDefinition (id "comp:3") whose componentPropertyDefinitions contains a
property like Theme with type "VARIANT" and variantOptions ["light"], then
create an INSTANCE node with componentId "comp:3" and assert
variantNotUsed.check(node, makeContext({ file })) returns null; this will target
the logic in index.ts that filters out single-option VARIANT properties.

In `@src/core/rules/token/index.ts`:
- Around line 317-343: multipleFillColorsCheck currently flags both directions
for a near-duplicate pair (nodeA→nodeB and nodeB→nodeA), which can double-count
the same color pair; update the implementation by adding a concise comment above
the multipleFillColorsCheck function (referencing multipleFillColorsCheck,
multipleFillColorsDef, and extractSolidColor) that documents this bidirectional
flagging behavior and the intended rationale (or note that it is intentional to
highlight both affected nodes), or alternatively implement a deterministic
single-direction guard (e.g. compare node.id and sibling.id or use a seen-pairs
set) if you want to avoid duplicate findings—include which approach you chose in
the comment.

In `@src/core/rules/token/multiple-fill-colors.test.ts`:
- Around line 58-65: Add a test that passes an explicit tolerance option to
multipleFillColors.check to exercise the override path instead of relying on the
default in rule-config.ts: create two nodes (nodeA, nodeB) with fills whose
color distance is ~5, then call multipleFillColors.check(nodeA, makeContext({
siblings }), { tolerance: 3 }) and assert it returns null, and call it again
with { tolerance: 10 } and assert it returns non-null; this verifies the custom
tolerance option is honored by the check implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 73813724-ebba-4a97-af69-000efb5331c8

📥 Commits

Reviewing files that changed from the base of the PR and between 7f0100d and 2ed78d8.

📒 Files selected for processing (4)
  • src/core/rules/component/index.ts
  • src/core/rules/component/variant-not-used.test.ts
  • src/core/rules/token/index.ts
  • src/core/rules/token/multiple-fill-colors.test.ts

Comment on lines +49 to +85
it("returns null when instance overrides a variant property", () => {
const file = makeFileWithVariantComponent();
const node = makeNode({
type: "INSTANCE",
name: "MyButton",
componentId: "comp:1",
componentProperties: { State: { value: "hover" } },
});
expect(variantNotUsed.check(node, makeContext({ file }))).toBeNull();
});

it("returns null for non-INSTANCE nodes", () => {
const node = makeNode({ type: "FRAME" });
expect(variantNotUsed.check(node, makeContext())).toBeNull();
});

it("returns null when component has no variant properties", () => {
const file: AnalysisFile = {
...makeFile(),
componentDefinitions: {
"comp:2": makeNode({
id: "comp:2",
type: "COMPONENT",
componentPropertyDefinitions: {
label: { type: "TEXT" },
},
}),
},
};
const node = makeNode({ type: "INSTANCE", componentId: "comp:2" });
expect(variantNotUsed.check(node, makeContext({ file }))).toBeNull();
});

it("returns null when no componentDefinitions in file", () => {
const node = makeNode({ type: "INSTANCE", componentId: "comp:1" });
expect(variantNotUsed.check(node, makeContext())).toBeNull();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

LGTM with minor coverage suggestion.

Good negative case coverage. Consider adding an edge case test for a variant property with exactly one option (which the rule's filter on Line 343 of index.ts intentionally skips):

📝 Optional: Additional edge case test
it("returns null when variant has only one option", () => {
  const file: AnalysisFile = {
    ...makeFile(),
    componentDefinitions: {
      "comp:3": makeNode({
        id: "comp:3",
        type: "COMPONENT",
        componentPropertyDefinitions: {
          Theme: { type: "VARIANT", variantOptions: ["light"] }, // single option
        },
      }),
    },
  };
  const node = makeNode({ type: "INSTANCE", componentId: "comp:3" });
  expect(variantNotUsed.check(node, makeContext({ file }))).toBeNull();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/rules/component/variant-not-used.test.ts` around lines 49 - 85, Add
a unit test covering the edge case where a variant property has exactly one
option so the rule's filter that skips single-option variants is exercised;
specifically, create an AnalysisFile with a componentDefinition (id "comp:3")
whose componentPropertyDefinitions contains a property like Theme with type
"VARIANT" and variantOptions ["light"], then create an INSTANCE node with
componentId "comp:3" and assert variantNotUsed.check(node, makeContext({ file
})) returns null; this will target the logic in index.ts that filters out
single-option VARIANT properties.

let-sunny and others added 2 commits March 26, 2026 03:23
…ge, tolerance test

- prototype-link-in-design: add tests for malformed variantOptions
  (non-array, non-string entries) to exercise defensive guards
- multiple-fill-colors: add bidirectional flagging comment + custom
  tolerance option test
- variant-not-used review comments: N/A (rule deleted)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Was always returning null. Now checks for opacity < 1 without variable
binding. Also adds opacity field to AnalysisNode and collects it in
figma-transformer.

0 stub rules remaining — all 32 rules have real detection logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@let-sunny let-sunny merged commit 01364c9 into main Mar 25, 2026
3 checks passed
@let-sunny let-sunny deleted the feat/implement-stub-rules branch March 25, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant