Skip to content

feat: Correctness category — component-body analysis (CORRECT001/002), step 1 toward Svelte Doctor - #68

Merged
oekazuma merged 4 commits into
mainfrom
feat/correctness-category
Jun 29, 2026
Merged

oekazuma merged 4 commits into
mainfrom
feat/correctness-category

Conversation

@oekazuma

@oekazuma oekazuma commented Jun 29, 2026 •

Copy link
Copy Markdown
Owner

The first step toward a "Svelte Doctor" — broadening svelte-vitals from SEO/Performance <head> checks into component-body code-health analysis, starting with a new Correctness category.

Why

The goal is a deterministic, agent-native scanner that catches "the bad code an AI agent writes" across correctness, security, and architecture — not only SEO/Performance. Until now svelte-vitals only inspected <svelte:head> / <img> / headings. This PR adds the first analysis of component bodies and the first non-SEO/Perf category, focusing on high-precision checks the Svelte compiler / svelte-check / eslint-plugin-svelte don't enforce as health signals (no duplication of official tooling).

What

  • New correctness category + scope: 'component'. Scoring is data-driven (scoresByCategory groups by category), so it flows into per-category and combined Health with no scoring-code change.
  • New CLI component scan — collectComponentFacts globs src/**/*.svelte (including $lib, not just routes) and parses each into a ComponentFacts channel (ctx.components). CLI/static only — the rendered provider can't see reactivity, so the rules no-op there. Findings are scored per source file (whole-codebase health).
  • CORRECT001 — keyed each block: flags an {#each} with no key (reordering an unkeyed list destroys/recreates DOM, losing element state).
  • CORRECT002 — effect used to derive state: flags an $effect whose body only assigns to $state — use $derived instead.

Fact extraction: a template walk for EachBlock keys, and an instance-script ESTree walk for $state declarations + $effect calls (conservative assign-only detection for high precision).

Tests / docs

  • Parser facts (each-key, $effect assign-only vs mixed, $state collection), rule behavior, and a collectComponentFacts integration over src/**/*.svelte.
  • 4 docs pages (CORRECT001/002, en+ja), changeset, design spec.

pnpm -r test (488: core 224 / vite 76 / cli 179 / mcp 9), pnpm -r typecheck, pnpm lint, pnpm --filter docs build (97 pages) — all green.

Next (separate slices)

Security ({@html} XSS, target=_blank rel), Architecture metrics, workflow ergonomics (--diff / --staged), and more reactivity heuristics — tracked in #69.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new Correctness category to health reports for Svelte components.
    • Introduced checks for unkeyed {#each} blocks and $effect usage that should be replaced with $derived.
  • Documentation

    • Added new rule pages in English and Japanese with examples and guidance for the new correctness checks.
  • Tests

    • Expanded automated coverage for the new correctness reporting and rule documentation.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@oekazuma, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66836ead-7bf0-4b68-8f06-db60d01cc691

📥 Commits

Reviewing files that changed from the base of the PR and between 4dc7773 and 40920f2.

📒 Files selected for processing (7)
  • docs/src/content/docs/ja/rules/correct001.md
  • docs/src/content/docs/rules/correct001.md
  • packages/cli/src/index.ts
  • packages/cli/src/providers/source/parse.ts
  • packages/cli/test/parse-component-facts.test.ts
  • packages/core/src/reporter/console.ts
  • packages/core/test/console-report.test.ts
📝 Walkthrough

Walkthrough

Adds a correctness category to svelte-vitals health reporting (CLI/static mode only). Introduces EachBlockFact, EffectFact, and ComponentFacts types, a CLI scanner that parses every src/**/*.svelte file via the Svelte compiler AST, two new rules (CORRECT001: unkeyed {#each}, CORRECT002: $effect used to derive state), wiring into analyzeProject, tests, and EN/JA documentation.

Changes

Correctness Category

Layer / File(s) Summary
Core types and RuleContext extension
packages/core/src/types.ts, packages/core/src/component.ts, packages/core/src/rule.ts
Adds 'correctness' to Category, 'component' to Scope, defines EachBlockFact/EffectFact/ComponentFacts interfaces, and extends RuleContext with optional components?: ComponentFacts[].
Correctness rule factory and CORRECT001/CORRECT002
packages/core/src/rules/correctness/correct001-002.ts, packages/core/src/rules/index.ts, packages/core/src/index.ts
Implements the correctnessRule factory, exports correct001EachKey (unkeyed {#each}) and correct002EffectDerived ($effect assigns only $state), and registers both in allRules.
CLI component AST parser and scanner
packages/cli/src/providers/source/parse.ts, packages/cli/src/providers/source/components.ts
Adds template and ESTree AST helpers (collectEachBlocks, walkEstree, isRuneCall, bodyOnlyAssignsState, parseComponentFacts) and collectComponentFacts which globs src/**/*.svelte and returns per-file facts with per-file error isolation.
CLI analyzeProject wiring
packages/cli/src/index.ts
Imports collectComponentFacts and collects project and component facts concurrently via Promise.all, passing components into runRules.
Tests
packages/cli/test/parse-component-facts.test.ts, packages/core/test/correctness-rules.test.ts, packages/cli/test/docs-links.test.ts
Adds Vitest suites for parser fact extraction, rule pass/warn behavior, and expands the docs-links test to include correctness.
Docs, spec, and changeset
docs/src/content/docs/rules/correct00*.md, docs/src/content/docs/ja/rules/correct00*.md, docs/superpowers/specs/..., .changeset/correctness-category.md
Adds English and Japanese rule pages for CORRECT001/CORRECT002, a design specification, and a minor-bump changeset.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as analyzeProject (CLI)
  participant collectComponentFacts
  participant parseComponentFacts
  participant svelteCompiler as svelte/compiler
  participant runRules

  CLI->>collectComponentFacts: cwd
  CLI->>collectComponentFacts: Promise.all with collectProjectFacts
  collectComponentFacts->>collectComponentFacts: glob src/**/*.svelte
  loop each .svelte file
    collectComponentFacts->>parseComponentFacts: source, filename
    parseComponentFacts->>svelteCompiler: parse(source)
    svelteCompiler-->>parseComponentFacts: template AST + script ESTree
    parseComponentFacts-->>collectComponentFacts: ComponentFacts {eachBlocks, effects}
  end
  collectComponentFacts-->>CLI: ComponentFacts[]
  CLI->>runRules: ctx { project, components }
  runRules->>runRules: correct001EachKey, correct002EffectDerived
  runRules-->>CLI: RuleResult[]
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • oekazuma/svelte-vitals#36: Both PRs extend Category union types and per-category scoring infrastructure in packages/core.
  • oekazuma/svelte-vitals#38: Both PRs extend analyzeProject() in packages/cli/src/index.ts with a new parallel fact-collection step merged into the rule/severity pipeline.

Poem

🐇 Hop hop, the rabbit checks each list,
No unkeyed {#each} shall be missed!
$effect that only assigns — oh dear,
$derived is the path, make it clear.
With facts parsed from every .svelte file,
Correctness hops in with a smile! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a new correctness category with component-body analysis and the first two rules.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/correctness-category

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

…ECT001/002)

First step toward a "Svelte Doctor": analyze component bodies (not just <head>)
with a new `correctness` category.

- New `correctness` Category + `scope: 'component'` (scoring is data-driven, so the
  category flows into per-category/combined Health with no scoring change).
- New CLI component scan: collectComponentFacts globs src/**/*.svelte (incl. $lib)
  and parses each into a ComponentFacts channel (ctx.components); CLI/static only,
  so rendered mode no-ops. Findings score per source file.
- CORRECT001 keyed-each: flags an `{#each}` with no key.
- CORRECT002 effect-derived: flags an `$effect` whose body only assigns `$state`
  (the useEffect-to-$effect anti-pattern) — use `$derived`.

Fact extraction: template walk for EachBlock keys; instance-script ESTree walk for
$state declarations and $effect calls (conservative assign-only detection). Docs
(en+ja), changeset, spec, and tests (parser facts + rules + integration) added.

pnpm -r test 488 green; typecheck, lint, docs build (97 pages) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@oekazuma
oekazuma force-pushed the feat/correctness-category branch from eee4641 to 4dc7773 Compare June 29, 2026 07:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/cli/test/parse-component-facts.test.ts (2)

35-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the collector’s error-tolerant fallback too.

collectComponentFacts() is intentionally resilient: it catches read/parse failures and returns empty facts for that file. This test only locks in the happy path, so a future change could break that guarantee without any signal.

Suggested test shape
 describe('collectComponentFacts (memory runtime)', () => {
   it('scans every .svelte under src, including $lib', async () => {
     const rt = createMemoryRuntime({
       'src/routes/+page.svelte': '{`#each` xs as x}<i>{x}</i>{/each}',
       'src/lib/Card.svelte': '<script>let n = $state(0); let d = $state(0); $effect(() => { d = n + 1; });</script>',
       'src/app.html': '<html></html>' // not .svelte → ignored
     });
     const facts = await collectComponentFacts(rt, '');
     const byFile = new Map(facts.map((f) => [f.file, f]));
     expect(byFile.get('src/routes/+page.svelte')!.eachBlocks).toEqual([{ hasKey: false, line: 1 }]);
     expect(byFile.get('src/lib/Card.svelte')!.effects[0]!.assignsOnlyState).toBe(true);
     expect(byFile.has('src/app.html')).toBe(false);
   });
+
+  it('returns empty facts for unreadable or unparsable components', async () => {
+    const rt = createMemoryRuntime({
+      'src/lib/Broken.svelte': '<script>let x = </script>'
+    });
+    await expect(collectComponentFacts(rt, '')).resolves.toContainEqual({
+      file: 'src/lib/Broken.svelte',
+      eachBlocks: [],
+      effects: []
+    });
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/test/parse-component-facts.test.ts` around lines 35 - 47, The
current test only covers the happy path for collectComponentFacts and does not
verify its error-tolerant fallback. Update the existing collectComponentFacts
(memory runtime) test in parse-component-facts.test.ts to also cover a
read/parse failure case by including a file that will fail during collection and
asserting collectComponentFacts returns empty facts for that file rather than
throwing, using the same collectComponentFacts, createMemoryRuntime, and byFile
lookup patterns to keep the check anchored to the collector behavior.

15-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an explicit $effect.pre parser case.

The design spec calls out $effect.pre, but this suite only exercises bare $effect. Since rune matching is a separate branch in the parser, a regression there would currently slip through.

Suggested test
   it('does not flag assignment to a non-$state variable', () => {
     const e = facts('let count = $state(0); let plain = 0; $effect(() => { plain = count; });');
     expect(e[0]!.assignsOnlyState).toBe(false);
   });
+  it('treats $effect.pre the same way', () => {
+    const e = facts('let count = $state(0); let double = $state(0); $effect.pre(() => { double = count * 2; });');
+    expect(e).toEqual([{ line: 1, assignsOnlyState: true }]);
+  });
   it('reports no effects when there are none', () => {
     expect(facts('let count = $state(0);')).toEqual([]);
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/test/parse-component-facts.test.ts` around lines 15 - 33, Add an
explicit parser test coverage for $effect.pre in parseComponentFacts, since the
current parseComponentFacts — $effect suite only validates bare $effect and can
miss regressions in the separate rune branch. Extend the existing facts helper
and add a focused case that parses a script containing $effect.pre, then assert
the returned effects entry is reported correctly using the same assignsOnlyState
behavior as the $effect cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/index.ts`:
- Around line 105-108: The `runRules()` call in `index.ts` still evaluates all
collected components even when `opts.route` is set, so route-filtered runs can
emit CORRECT001/CORRECT002 findings from unrelated components. Update the
`collectComponentFacts`/`runRules` flow to respect `RunOptions.route` by
excluding or gating component-scope correctness rules during route-scoped
execution until component-to-route attribution is available. Use the existing
`opts.route` check and the `runRules` invocation in `index.ts` as the main
points to adjust.

In `@packages/cli/src/providers/source/parse.ts`:
- Around line 363-367: The state detection in isRuneCall is too broad for $state
because it also treats member calls like $state.snapshot(...) as declarations,
which causes stateNames to include non-state variables. Update isRuneCall so the
$state path only matches actual declaration forms (while preserving the broader
MemberExpression handling for $effect.pre), and keep the rest of the CORRECT002
scanning logic in parse.ts aligned with that stricter check.

---

Nitpick comments:
In `@packages/cli/test/parse-component-facts.test.ts`:
- Around line 35-47: The current test only covers the happy path for
collectComponentFacts and does not verify its error-tolerant fallback. Update
the existing collectComponentFacts (memory runtime) test in
parse-component-facts.test.ts to also cover a read/parse failure case by
including a file that will fail during collection and asserting
collectComponentFacts returns empty facts for that file rather than throwing,
using the same collectComponentFacts, createMemoryRuntime, and byFile lookup
patterns to keep the check anchored to the collector behavior.
- Around line 15-33: Add an explicit parser test coverage for $effect.pre in
parseComponentFacts, since the current parseComponentFacts — $effect suite only
validates bare $effect and can miss regressions in the separate rune branch.
Extend the existing facts helper and add a focused case that parses a script
containing $effect.pre, then assert the returned effects entry is reported
correctly using the same assignsOnlyState behavior as the $effect cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5ebcf611-5aaa-4437-8ef1-9bab2c4014c0

📥 Commits

Reviewing files that changed from the base of the PR and between a519ff9 and 4dc7773.

📒 Files selected for processing (18)
  • .changeset/correctness-category.md
  • docs/src/content/docs/ja/rules/correct001.md
  • docs/src/content/docs/ja/rules/correct002.md
  • docs/src/content/docs/rules/correct001.md
  • docs/src/content/docs/rules/correct002.md
  • docs/superpowers/specs/2026-06-29-correctness-category-design.md
  • packages/cli/src/index.ts
  • packages/cli/src/providers/source/components.ts
  • packages/cli/src/providers/source/parse.ts
  • packages/cli/test/docs-links.test.ts
  • packages/cli/test/parse-component-facts.test.ts
  • packages/core/src/component.ts
  • packages/core/src/index.ts
  • packages/core/src/rule.ts
  • packages/core/src/rules/correctness/correct001-002.ts
  • packages/core/src/rules/index.ts
  • packages/core/src/types.ts
  • packages/core/test/correctness-rules.test.ts

Comment thread packages/cli/src/index.ts Outdated
Comment thread packages/cli/src/providers/source/parse.ts Outdated
oekazuma and others added 2 commits June 29, 2026 17:11
…h (PR #68 review)

- A --route-filtered run now skips the component (Correctness) scan instead of
  reporting findings from unrelated/shared components — component→route
  attribution doesn't exist yet, so route scoping can't apply to them.
- $state-name detection only matches declaration forms ($state / $state.raw /
  $state.frozen), no longer $state.snapshot, so CORRECT002 doesn't pick up
  snapshot reads as reactive state. Tests added for both.

pnpm -r test 490 green; typecheck, lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CORRECT002: only treat a plain `=` assignment as a derive candidate;
  compound assignments (`+=`, `*=`, `??=`, …) accumulate and can't become a
  self-referential `$derived`, so flagging them was a false positive.
- CORRECT002: narrow effect detection to `$effect` / `$effect.pre` only,
  excluding the non-effect readers `$effect.tracking()` / `$effect.root()`
  that otherwise seeded spurious pass units.
- CORRECT001: ignore constant inline array literals (`{#each [1,2,3] as n}`)
  — fixed length, never reorders, so a key can't help; spreads stay dynamic.
- Tests + en/ja docs note for the constant-list exclusion.

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

Copilot AI left a comment

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.

Pull request overview

Introduces the first component-body, static (CLI/source) analysis pipeline to extend svelte-vitals beyond <svelte:head> checks, adding a new Correctness category with initial high-precision reactivity/lifecycle heuristics.

Changes:

  • Added correctness category + component scope, plus two new rules: CORRECT001 (unkeyed {#each}) and CORRECT002 (assign-only $effect → prefer $derived).
  • Implemented CLI component scanning (src/**/*.svelte) to collect ComponentFacts and thread them through the rule engine via ctx.components.
  • Added rule docs (en/ja), changeset, and tests covering parsing, rule behavior, and component-facts collection.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/core/test/correctness-rules.test.ts Adds unit tests for CORRECT001/002 rule behavior and rendered-mode no-op behavior.
packages/core/src/types.ts Extends shared types with Scope: 'component' and Category: 'correctness'.
packages/core/src/rules/index.ts Registers correctness rules in allRules and re-exports them.
packages/core/src/rules/correctness/correct001-002.ts Implements the correctness rule factory and CORRECT001/002 rules (component-scoped).
packages/core/src/rule.ts Adds components?: ComponentFacts[] to RuleContext.
packages/core/src/index.ts Re-exports component fact types and new correctness rules from the public API.
packages/core/src/component.ts Defines the ComponentFacts schema used for component-body analysis.
packages/cli/test/parse-component-facts.test.ts Adds tests for parsing {#each} keys, $effect assign-only detection, and integration scan over src/**/*.svelte.
packages/cli/test/docs-links.test.ts Updates docs-link integrity test to include correctness rules.
packages/cli/src/providers/source/parse.ts Adds parseComponentFacts and AST walks for EachBlock keys and $state/$effect detection.
packages/cli/src/providers/source/components.ts Implements collectComponentFacts scanning src/**/*.svelte with parse-failure tolerance.
packages/cli/src/index.ts Threads component facts into rule execution (skips component scan for route-filtered runs).
docs/superpowers/specs/2026-06-29-correctness-category-design.md Adds design spec documenting architecture, facts, rules, and test plan for the new category.
docs/src/content/docs/rules/correct002.md Adds English rule docs page for CORRECT002.
docs/src/content/docs/rules/correct001.md Adds English rule docs page for CORRECT001.
docs/src/content/docs/ja/rules/correct002.md Adds Japanese rule docs page for CORRECT002.
docs/src/content/docs/ja/rules/correct001.md Adds Japanese rule docs page for CORRECT001.
.changeset/correctness-category.md Declares minor version bumps and release notes for the new correctness category and rules.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/types.ts
…68 review)

Adding the `correctness` Category fed the combined Health score but the console
reporter's CATEGORY_ORDER/CATEGORY_LABEL still listed only seo/performance, so
the per-category score lines omitted Correctness. Add it (html/json reporters
already enumerate categories dynamically). Test added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@oekazuma
oekazuma merged commit 07961a6 into main Jun 29, 2026
5 checks passed
@oekazuma
oekazuma deleted the feat/correctness-category branch June 29, 2026 08:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants