refactor: unify missing-component with 4-stage checks (#17)#22
refactor: unify missing-component with 4-stage checks (#17)#22
Conversation
Merge repeated-frame-structure and nested-instance-override into a unified missing-component rule with 4 stages: 1. Component exists but not used — FRAME name matches component metadata 2. Name-based repetition — same-name FRAME N+ times (existing) 3. Structure-based repetition — sibling FRAMEs with identical fingerprint 4. Instance style divergence — same-componentId instances with different overrides Also: - design-tree annotates INSTANCE nodes with [component: Name] - Remove repeated-frame-structure and nested-instance-override from RuleId - 26 new missing-component tests + 3 design-tree tests - 292 total tests passing Closes #17 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughConsolidates component-related checks into a single, 4-stage Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as Client
participant Engine as RuleEngine
participant File as AnalysisFile
participant Rule as missing-component
participant Components as file.components
participant Reporter as Reporter
CLI->>Engine: analyze(file)
Engine->>Rule: resetMissingComponentState()
Engine->>File: traverse nodes
File->>Rule: for each node -> check(node, file, options)
alt node is FRAME/INSTANCE
Rule->>Components: consult components metadata
Rule-->>Rule: Stage1: name match -> emit (if match & repetition)
Rule-->>Rule: Stage2: global name repetition -> emit
Rule-->>Rule: Stage3: sibling fingerprint -> emit
Rule-->>Rule: Stage4: instance divergence vs master -> emit
end
Rule->>Reporter: emit violations (deduped per-module)
Engine->>Reporter: collect results
Reporter->>CLI: output report
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 92-108: The module-level dedup Sets seenStage1ComponentNames and
seenStage4ComponentIds persist across runs and rely on callers to call
resetMissingComponentState(); change the implementation so dedup state is owned
by the analysis run instead of module-level globals: remove the module-level
Sets and instead create and pass per-run Sets from the analysis engine into the
missing-component rule (or return a factory that creates a fresh pair), and
delete/resetMissingComponentState(); alternatively, if you must keep
module-level state, add a prominent comment and ensure the analysis engine
invokes resetMissingComponentState() at analysis start (call the exported
resetMissingComponentState function from the analysis initialization path) so
state is cleared for each run.
- Around line 159-169: The comparison sameNameFrames[0] === node.id can produce
an undefined-index type under noUncheckedIndexedAccess; change the block in the
sameNameFrames handling to explicitly guard the indexed access: after confirming
sameNameFrames && sameNameFrames.length >= minRepetitions, read the first
element into a local (e.g. const first = sameNameFrames[0]) and check first !==
undefined before comparing to node.id and returning the missingComponentDef.id
payload (include nodeId/nodePath/message as before).
- Around line 140-148: The check uses sameNameFrames[0] directly which under
noUncheckedIndexedAccess is string | undefined; update the conditional in the
block that returns the missingComponentDef result to explicitly guard the
indexed access (e.g., ensure sameNameFrames.length > 0 or sameNameFrames[0] !==
undefined before comparing to node.id). Modify the condition around
sameNameFrames[0] === node.id in the function that produces the object with
ruleId: missingComponentDef.id and nodePath: context.path.join(" > ") so the
comparison only runs when sameNameFrames[0] is guaranteed defined.
In `@src/core/rules/component/missing-component.test.ts`:
- Around line 282-296: The test reuses the same child object instance for both
frameA and frameB which can hide issues; update the test so each sibling has its
own child node instance (e.g., call makeChildFrame twice or clone the child)
instead of assigning the same child object to frameA.children and
frameB.children; adjust the setup around makeChildFrame, makeNode, frameA,
frameB (the input to missingComponent.check) so children are distinct objects
while preserving identical structure for fingerprinting.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 6832e488-dc4e-42c6-9420-7bb8904edf3f
📒 Files selected for processing (9)
docs/REFERENCE.mdsrc/core/contracts/rule.tssrc/core/engine/design-tree.test.tssrc/core/engine/design-tree.tssrc/core/rules/component/index.tssrc/core/rules/component/missing-component.test.tssrc/core/rules/component/repeated-frame-structure.test.tssrc/core/rules/index.tssrc/core/rules/rule-config.ts
💤 Files with no reviewable changes (1)
- src/core/rules/component/repeated-frame-structure.test.ts
| /** | ||
| * Module-level dedup Sets for missing-component stages. | ||
| * These prevent duplicate violations when the same pattern is encountered | ||
| * multiple times during a single analysis run. | ||
| */ | ||
| const seenStage1ComponentNames = new Set<string>(); | ||
| const seenStage4ComponentIds = new Set<string>(); | ||
|
|
||
| /** | ||
| * Reset deduplication state for missing-component between analysis runs. | ||
| * Call this at the start of each analysis if the process is long-running | ||
| * (e.g. MCP server mode). | ||
| */ | ||
| export function resetMissingComponentState(): void { | ||
| seenStage1ComponentNames.clear(); | ||
| seenStage4ComponentIds.clear(); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Module-level mutable state requires careful lifecycle management.
The module-level Sets for deduplication persist across multiple analysis runs in long-running processes (e.g., MCP server mode). The resetMissingComponentState() function is provided, but callers must remember to invoke it. Consider documenting this requirement prominently or integrating the reset into the analysis engine's lifecycle.
🤖 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 92 - 108, The module-level
dedup Sets seenStage1ComponentNames and seenStage4ComponentIds persist across
runs and rely on callers to call resetMissingComponentState(); change the
implementation so dedup state is owned by the analysis run instead of
module-level globals: remove the module-level Sets and instead create and pass
per-run Sets from the analysis engine into the missing-component rule (or return
a factory that creates a fresh pair), and delete/resetMissingComponentState();
alternatively, if you must keep module-level state, add a prominent comment and
ensure the analysis engine invokes resetMissingComponentState() at analysis
start (call the exported resetMissingComponentState function from the analysis
initialization path) so state is cleared for each run.
…ence Stage 4 now compares instance styles (fills, strokes, effects, cornerRadius, strokeWeight, individualStrokeWeights) against the component master from componentDefinitions. Any style override is flagged — use variants instead of direct style changes. Requires componentDefinitions to be present (from save-fixture #16). Without it, Stage 4 is silently skipped. - Remove collectInstancesByComponentId (no longer needed) - Add detectStyleOverrides helper - 8 new Stage 4 tests for master comparison Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Figma's SLOT node type was missing from the schema, causing componentDefinitions validation to fail for components that use slots (e.g. Navigation Pill List). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add explicit undefined guard for sameNameFrames[0] in Stage 1 and 2 (noUncheckedIndexedAccess compliance) - Call resetMissingComponentState() in RuleEngine.analyze() to clear dedup state between runs (important for MCP server mode) - Use separate child object instances in Stage 3 tests to avoid hiding issues from shared references - Add SLOT to AnalysisNodeType schema (Figma SLOT node type) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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.ts`:
- Around line 148-151: The analyze method in rule-engine.ts resets module-level
dedup state via resetMissingComponentState() but misses the companion reset for
the `missing-component-description` rule; call the exported
resetMissingComponentDescriptionState() (from the component rules module)
alongside resetMissingComponentState() at the start of analyze(file:
AnalysisFile): AnalysisResult to clear the seenMissingDescriptionComponentIds
Set between runs, ensuring both module-level caches are reset.
In `@src/core/rules/component/index.ts`:
- Around line 254-277: The code marks node.componentId in seenStage4ComponentIds
before checking for style overrides, which causes later instances with overrides
to be skipped; change the logic in the INSTANCE handling so you call
detectStyleOverrides(master, node) first, and only add node.componentId to
seenStage4ComponentIds when you are about to return the violation object (i.e.,
when overrides.length > 0 and you emit the missingComponentDef.id violation
using componentMeta/name), leaving the ID unmarked and allowing other instances
to be evaluated if no violation is found.
- Around line 139-182: The code calls collectFrameNames(context.file.document)
twice (in the Stage 1 block and again in Stage 2) causing an unnecessary
traversal; compute the frame names once before the Stage 1/Stage 2 logic and
reuse that Map for both checks. Modify the function to declare a single const
frameNames = collectFrameNames(context.file.document) above the existing Stage 1
code, replace frameNames2/sameNameFrames2/first2 with the reused
frameNames/sameNameFrames/first variables, and keep the existing conditions
involving seenStage1ComponentNames, missingComponentDef, node, options, and
getRuleOption unchanged. Ensure behavior and rule messages remain identical
while removing the redundant call.
In `@src/core/rules/component/missing-component.test.ts`:
- Around line 592-625: Add a test that verifies deduplication when the first
instance has no overrides but a later instance does: create a master node and
two instances (inst1 with fills identical to master, inst2 with different
fills), assemble a file with componentDefinitions and components, build separate
contexts ctx1 and ctx2, then assert missingComponent.check(inst1, ctx1) returns
null and missingComponent.check(inst2, ctx2) returns not null; reference
makeNode, makeFile, makeContext and missingComponent.check to locate where to
add this case in missing-component.test.ts.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3f430198-96cd-426e-ae61-a7c92017bcc7
📒 Files selected for processing (4)
src/core/contracts/figma-node.tssrc/core/engine/rule-engine.tssrc/core/rules/component/index.tssrc/core/rules/component/missing-component.test.ts
| analyze(file: AnalysisFile): AnalysisResult { | ||
| // Reset module-level dedup state for rules that track seen patterns | ||
| resetMissingComponentState(); | ||
|
|
There was a problem hiding this comment.
Missing reset for missingComponentDescription state.
The resetMissingComponentState() call correctly clears dedup state for the missing-component rule. However, src/core/rules/component/index.ts also exports resetMissingComponentDescriptionState() (line 494) for the missing-component-description rule's seenMissingDescriptionComponentIds Set, which has the same module-level persistence issue.
For consistency and correctness in long-running processes, consider also resetting this state:
🛡️ Proposed fix
-import { resetMissingComponentState } from "../rules/component/index.js";
+import { resetMissingComponentState, resetMissingComponentDescriptionState } from "../rules/component/index.js"; analyze(file: AnalysisFile): AnalysisResult {
// Reset module-level dedup state for rules that track seen patterns
resetMissingComponentState();
+ resetMissingComponentDescriptionState();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/core/engine/rule-engine.ts` around lines 148 - 151, The analyze method in
rule-engine.ts resets module-level dedup state via resetMissingComponentState()
but misses the companion reset for the `missing-component-description` rule;
call the exported resetMissingComponentDescriptionState() (from the component
rules module) alongside resetMissingComponentState() at the start of
analyze(file: AnalysisFile): AnalysisResult to clear the
seenMissingDescriptionComponentIds Set between runs, ensuring both module-level
caches are reset.
src/core/rules/component/index.ts
Outdated
| if (matchingComponent) { | ||
| const frameNames = collectFrameNames(context.file.document); | ||
| const sameNameFrames = frameNames.get(node.name); | ||
|
|
||
| const first = sameNameFrames?.[0]; | ||
| if ( | ||
| sameNameFrames && | ||
| first !== undefined && | ||
| sameNameFrames.length >= 2 && | ||
| !seenStage1ComponentNames.has(node.name.toLowerCase()) | ||
| ) { | ||
| seenStage1ComponentNames.add(node.name.toLowerCase()); | ||
| if (first === node.id) { | ||
| return { | ||
| ruleId: missingComponentDef.id, | ||
| nodeId: node.id, | ||
| nodePath: context.path.join(" > "), | ||
| message: `Component "${matchingComponent.name}" exists — use instances instead of repeated frames (${sameNameFrames.length} found)`, | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const minRepetitions = (options?.["minRepetitions"] as number) ?? | ||
| getRuleOption("missing-component", "minRepetitions", 3); | ||
| // Stage 2: Name-based repetition (existing logic) | ||
| const minRepetitions = | ||
| (options?.["minRepetitions"] as number | undefined) ?? | ||
| getRuleOption("missing-component", "minRepetitions", 3); | ||
|
|
||
| { | ||
| const frameNames2 = collectFrameNames(context.file.document); | ||
| const sameNameFrames2 = frameNames2.get(node.name); | ||
| const first2 = sameNameFrames2?.[0]; | ||
|
|
||
| if (sameNameFrames2 && first2 !== undefined && sameNameFrames2.length >= minRepetitions) { | ||
| if (first2 === node.id) { | ||
| return { | ||
| ruleId: missingComponentDef.id, | ||
| nodeId: node.id, | ||
| nodePath: context.path.join(" > "), | ||
| message: `"${node.name}" appears ${sameNameFrames2.length} times — consider making it a component`, | ||
| }; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
collectFrameNames is called twice for the same document in Stages 1 and 2.
Both Stage 1 (line 140) and Stage 2 (line 168) call collectFrameNames(context.file.document) separately. Since the document doesn't change between stages, this redundant traversal can be avoided by computing it once.
♻️ Proposed refactor: Compute frame names once
if (node.type === "FRAME") {
+ // Collect frame names once for both Stage 1 and Stage 2
+ const frameNames = collectFrameNames(context.file.document);
+
// Stage 1: Component exists but not used — FRAME name matches a component in metadata AND frame is repeated
const components = context.file.components;
const matchingComponent = Object.values(components).find(
(c) => c.name.toLowerCase() === node.name.toLowerCase()
);
if (matchingComponent) {
- const frameNames = collectFrameNames(context.file.document);
const sameNameFrames = frameNames.get(node.name);
// ... Stage 1 logic
}
// Stage 2: Name-based repetition (existing logic)
const minRepetitions =
(options?.["minRepetitions"] as number | undefined) ??
getRuleOption("missing-component", "minRepetitions", 3);
- {
- const frameNames2 = collectFrameNames(context.file.document);
- const sameNameFrames2 = frameNames2.get(node.name);
+ const sameNameFrames2 = frameNames.get(node.name);🤖 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 139 - 182, The code calls
collectFrameNames(context.file.document) twice (in the Stage 1 block and again
in Stage 2) causing an unnecessary traversal; compute the frame names once
before the Stage 1/Stage 2 logic and reuse that Map for both checks. Modify the
function to declare a single const frameNames =
collectFrameNames(context.file.document) above the existing Stage 1 code,
replace frameNames2/sameNameFrames2/first2 with the reused
frameNames/sameNameFrames/first variables, and keep the existing conditions
involving seenStage1ComponentNames, missingComponentDef, node, options, and
getRuleOption unchanged. Ensure behavior and rule messages remain identical
while removing the redundant call.
| if (node.type === "INSTANCE" && node.componentId) { | ||
| if (seenStage4ComponentIds.has(node.componentId)) return null; | ||
| seenStage4ComponentIds.add(node.componentId); | ||
|
|
||
| const componentDefs = context.file.componentDefinitions; | ||
| if (!componentDefs) return null; | ||
|
|
||
| // Collect frame names in the file (cached per analysis run would be better) | ||
| const frameNames = collectFrameNames(context.file.document); | ||
| const sameNameFrames = frameNames.get(node.name); | ||
| const master = componentDefs[node.componentId]; | ||
| if (!master) return null; | ||
|
|
||
| // Compare style properties between master and instance | ||
| const overrides = detectStyleOverrides(master, node); | ||
| if (overrides.length > 0) { | ||
| const componentMeta = context.file.components[node.componentId]; | ||
| const componentName = componentMeta?.name ?? node.name; | ||
|
|
||
| if (sameNameFrames && sameNameFrames.length >= minRepetitions) { | ||
| // Only report on the first occurrence to avoid duplicate issues | ||
| if (sameNameFrames[0] === node.id) { | ||
| return { | ||
| ruleId: missingComponentDef.id, | ||
| nodeId: node.id, | ||
| nodePath: context.path.join(" > "), | ||
| message: `"${node.name}" appears ${sameNameFrames.length} times - consider making it a component`, | ||
| message: `"${componentName}" instance has style overrides (${overrides.join(", ")}) — use a variant instead of direct style changes`, | ||
| }; | ||
| } | ||
| return null; |
There was a problem hiding this comment.
Stage 4 dedup marks componentId as seen before checking for overrides, potentially missing violations.
The componentId is added to seenStage4ComponentIds (line 256) before determining whether there are style overrides. If the first instance encountered has no overrides (line 277 returns null), subsequent instances of the same component with actual overrides will be incorrectly skipped.
🐛 Proposed fix: Only mark as seen when violation is emitted
if (node.type === "INSTANCE" && node.componentId) {
if (seenStage4ComponentIds.has(node.componentId)) return null;
- seenStage4ComponentIds.add(node.componentId);
const componentDefs = context.file.componentDefinitions;
if (!componentDefs) return null;
const master = componentDefs[node.componentId];
if (!master) return null;
// Compare style properties between master and instance
const overrides = detectStyleOverrides(master, node);
if (overrides.length > 0) {
+ seenStage4ComponentIds.add(node.componentId);
const componentMeta = context.file.components[node.componentId];
const componentName = componentMeta?.name ?? node.name;
return {
ruleId: missingComponentDef.id,
nodeId: node.id,
nodePath: context.path.join(" > "),
message: `"${componentName}" instance has style overrides (${overrides.join(", ")}) — use a variant instead of direct style changes`,
};
}
return null;
}🤖 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 254 - 277, The code marks
node.componentId in seenStage4ComponentIds before checking for style overrides,
which causes later instances with overrides to be skipped; change the logic in
the INSTANCE handling so you call detectStyleOverrides(master, node) first, and
only add node.componentId to seenStage4ComponentIds when you are about to return
the violation object (i.e., when overrides.length > 0 and you emit the
missingComponentDef.id violation using componentMeta/name), leaving the ID
unmarked and allowing other instances to be evaluated if no violation is found.
| it("deduplicates per componentId", () => { | ||
| const master = makeNode({ | ||
| id: "comp:1", | ||
| name: "Button", | ||
| type: "COMPONENT", | ||
| fills: [{ type: "SOLID", color: { r: 0, g: 0, b: 1 } }], | ||
| }); | ||
| const inst1 = makeNode({ | ||
| id: "inst:1", | ||
| name: "Button", | ||
| type: "INSTANCE", | ||
| componentId: "comp:1", | ||
| fills: [{ type: "SOLID", color: { r: 1, g: 0, b: 0 } }], | ||
| }); | ||
| const inst2 = makeNode({ | ||
| id: "inst:2", | ||
| name: "Button", | ||
| type: "INSTANCE", | ||
| componentId: "comp:1", | ||
| fills: [{ type: "SOLID", color: { r: 0, g: 1, b: 0 } }], | ||
| }); | ||
|
|
||
| const file = makeFile({ | ||
| document: makeNode({ id: "0:1", name: "Doc", type: "DOCUMENT", children: [inst1, inst2] }), | ||
| components: { "comp:1": { key: "k", name: "Button", description: "" } }, | ||
| componentDefinitions: { "comp:1": master }, | ||
| }); | ||
|
|
||
| const ctx1 = makeContext({ file }); | ||
| const ctx2 = makeContext({ file }); | ||
|
|
||
| expect(missingComponent.check(inst1, ctx1)).not.toBeNull(); | ||
| expect(missingComponent.check(inst2, ctx2)).toBeNull(); | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Missing test case for dedup edge case where first instance has no overrides.
The current test verifies deduplication when the first instance has overrides. However, given the bug identified in the implementation (see index.ts review), a test case should verify behavior when the first instance matches the master exactly (no overrides) but subsequent instances have overrides.
🧪 Suggested additional test case
it("flags second instance when first instance has no overrides", () => {
const master = makeNode({
id: "comp:1",
name: "Button",
type: "COMPONENT",
fills: [{ type: "SOLID", color: { r: 0, g: 0, b: 1 } }],
});
// inst1 matches master exactly (no overrides)
const inst1 = makeNode({
id: "inst:1",
name: "Button",
type: "INSTANCE",
componentId: "comp:1",
fills: [{ type: "SOLID", color: { r: 0, g: 0, b: 1 } }],
});
// inst2 has overrides
const inst2 = makeNode({
id: "inst:2",
name: "Button",
type: "INSTANCE",
componentId: "comp:1",
fills: [{ type: "SOLID", color: { r: 1, g: 0, b: 0 } }],
});
const file = makeFile({
document: makeNode({ id: "0:1", name: "Doc", type: "DOCUMENT", children: [inst1, inst2] }),
components: { "comp:1": { key: "k", name: "Button", description: "" } },
componentDefinitions: { "comp:1": master },
});
const ctx1 = makeContext({ file });
const ctx2 = makeContext({ file });
// First instance has no overrides — should return null
expect(missingComponent.check(inst1, ctx1)).toBeNull();
// Second instance has overrides — should be flagged (not deduped)
expect(missingComponent.check(inst2, ctx2)).not.toBeNull();
});Note: This test will fail with the current implementation due to the bug where componentId is marked as seen before checking for overrides.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/core/rules/component/missing-component.test.ts` around lines 592 - 625,
Add a test that verifies deduplication when the first instance has no overrides
but a later instance does: create a master node and two instances (inst1 with
fills identical to master, inst2 with different fills), assemble a file with
componentDefinitions and components, build separate contexts ctx1 and ctx2, then
assert missingComponent.check(inst1, ctx1) returns null and
missingComponent.check(inst2, ctx2) returns not null; reference makeNode,
makeFile, makeContext and missingComponent.check to locate where to add this
case in missing-component.test.ts.
- Reset seenMissingDescriptionComponentIds in engine.analyze() alongside missing-component state - Stage 4: only mark componentId as seen when override found, allowing later instances to be checked if first has no override - Compute collectFrameNames once for Stage 1+2 instead of twice - Add test: first instance no override, second has override → flags Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
repeated-frame-structure와nested-instance-override를missing-component에 통합하여 4단계 체크로 고도화.4-Stage Check
흡수된 룰
repeated-frame-structure→ Stage 3으로 흡수, RuleId 제거nested-instance-override→ Stage 4로 흡수 (override 개수→override 내용 비교로 개선), RuleId 제거design-tree 컴포넌트 정보 반영
INSTANCE 노드에
[component: Name]어노테이션 추가:변경 파일
src/core/contracts/rule.ts— RuleId 2개 제거src/core/rules/rule-config.ts— config 2개 제거, missing-component에 옵션 추가src/core/rules/component/index.ts— 4-stage 통합 구현src/core/engine/design-tree.ts— INSTANCE 컴포넌트 어노테이션docs/REFERENCE.md— 룰 테이블 업데이트Test plan
pnpm test:run— 292 tests passedpnpm lint— cleanpnpm build— clean[component: ...]출력 확인Closes #17
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
Chores