Skip to content

Core: Implement component to stories lookup#34972

Open
yannbf wants to merge 1 commit into
nextfrom
yann/change-detection-reverse-index
Open

Core: Implement component to stories lookup#34972
yannbf wants to merge 1 commit into
nextfrom
yann/change-detection-reverse-index

Conversation

@yannbf
Copy link
Copy Markdown
Member

@yannbf yannbf commented May 29, 2026

Closes #

What I did

This PR opens the change-detection module's reverse-dependency index to external consumers (e.g. @storybook/addon-mcp) so they can answer "which stories relate to this file?" for any arbitrary list of paths.

The MCP addon needs this for its get-stories-by-component and get-changed-stories tools, so that it won't need to reinvent the wheel in order to detect the correlation of a change to stories. This is needed because the change detection is not always accurate (e.g. if you make a change in preview file, it won't detect any change), and with access to the reverse index the agent can decide on what to drill in.

This is done through a new preset experimental_getActiveChangeDetectionService and three accessor methods on ChangeDetectionService:
getReverseIndex(): ReverseIndexImpl | undefined — live reverse-dep map; same reference patched by IncrementalPatcher.
getStoryIdsByFile(): Promise<Map<string, Set<string>>> — join helper, absolute story-file path → declared story IDs.
getWorkingDir(): string — for resolving relative paths.

Usage from an addon side:

const service = experimental_getActiveChangeDetectionService();
if (!service) return; // change detection disabled / not yet started
const index = service.getReverseIndex();
if (!index) return; // graph still cold-starting
const hits = index.lookup(absolutePath); // → Map<storyFile, depth>

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

Caution

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

  • New Features
    • Exposed experimental inspection APIs on the change detection service for in-process consumers
    • Added methods to retrieve reverse dependencies, story-to-file mappings, and working directory information
    • Made change detection service accessible via active-service registry during development

Review Change Stack

@yannbf yannbf self-assigned this May 29, 2026
@yannbf yannbf added feature request core ci:normal Run our default set of CI jobs (choose this for most PRs). labels May 29, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

The PR establishes an in-memory registry for the active ChangeDetectionService, exposes new inspection APIs on the service (getReverseIndex, getStoryIdsByFile, getWorkingDir), registers the service instance in the dev server, and clears the registry reference during disposal to prevent dangling references.

Changes

Change Detection Service Registry and Inspection

Layer / File(s) Summary
Active Service Registry Module
code/core/src/core-server/change-detection/active-service-registry.ts, code/core/src/core-server/change-detection/index.ts
New module provides setActiveChangeDetectionService() and getActiveChangeDetectionService() functions to maintain a process-wide registry of the active service instance, with JSDoc clarifying single-instance dev-server behavior and intended use by in-process consumers.
ChangeDetectionService Inspection APIs and Lifecycle
code/core/src/core-server/change-detection/ChangeDetectionService.ts
Three new public methods (getReverseIndex(), getStoryIdsByFile(), getWorkingDir()) expose internal state for inspection; dispose() now clears the active service reference by calling setActiveChangeDetectionService(undefined).
Dev Server Registration and Public API Export
code/core/src/core-server/dev-server.ts, code/core/src/core-server/index.ts
Dev server registers the instantiated ChangeDetectionService as active immediately after creation; top-level core-server index re-exports the getter as experimental_getActiveChangeDetectionService for addon presets.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@code/core/src/core-server/change-detection/ChangeDetectionService.ts`:
- Around line 516-520: getStoryIdsByFile currently returns references into the
internal cache (storyIdsByFileCache) allowing callers to mutate internal state;
change getStoryIdsByFile (in ChangeDetectionService) to return a defensive copy:
after obtaining the Map from getStoryIdsByAbsolutePath, create and return a new
Map with cloned Sets for each key (i.e., for each [k,v] produce [k, new
Set(v)]), so callers receive independent structures and cannot mutate
storyIdsByFileCache or its Sets.

In `@code/core/src/core-server/dev-server.ts`:
- Around line 60-63: The active service is being registered unconditionally via
setActiveChangeDetectionService(changeDetectionService) before any start() path
runs, exposing a non-started instance; change this to only register the service
when change detection is actually startable/active (e.g. after verifying the
feature flags like ignorePreview and adapter availability or after a successful
start() / startability check on changeDetectionService) so consumers cannot
retrieve a non-started service; update the logic around
setActiveChangeDetectionService, gating it by the changeDetectionService's
startability (or post-start success) and remove the unconditional early
registration.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cad1e290-5a29-4745-b87b-10633de241f9

📥 Commits

Reviewing files that changed from the base of the PR and between 3c67edf and 99bb59c.

📒 Files selected for processing (5)
  • code/core/src/core-server/change-detection/ChangeDetectionService.ts
  • code/core/src/core-server/change-detection/active-service-registry.ts
  • code/core/src/core-server/change-detection/index.ts
  • code/core/src/core-server/dev-server.ts
  • code/core/src/core-server/index.ts

Comment on lines +516 to +520
async getStoryIdsByFile(): Promise<Map<string, Set<string>>> {
const generator = await this.options.storyIndexGeneratorPromise;
const storyIndex = await generator.getIndex();
return getStoryIdsByAbsolutePath(this.storyIdsByFileCache, storyIndex, this.workingDir);
}
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 | ⚡ Quick win

Avoid returning the mutable internal cache map directly.

This method exposes cached Map<string, Set<string>> references to callers. External mutation can corrupt internal state used later by change-detection scans.

💡 Proposed fix
  async getStoryIdsByFile(): Promise<Map<string, Set<string>>> {
    const generator = await this.options.storyIndexGeneratorPromise;
    const storyIndex = await generator.getIndex();
-    return getStoryIdsByAbsolutePath(this.storyIdsByFileCache, storyIndex, this.workingDir);
+    const storyIdsByFile = getStoryIdsByAbsolutePath(
+      this.storyIdsByFileCache,
+      storyIndex,
+      this.workingDir
+    );
+    return new Map(
+      Array.from(storyIdsByFile.entries(), ([filePath, storyIds]) => [filePath, new Set(storyIds)])
+    );
  }
🤖 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 `@code/core/src/core-server/change-detection/ChangeDetectionService.ts` around
lines 516 - 520, getStoryIdsByFile currently returns references into the
internal cache (storyIdsByFileCache) allowing callers to mutate internal state;
change getStoryIdsByFile (in ChangeDetectionService) to return a defensive copy:
after obtaining the Map from getStoryIdsByAbsolutePath, create and return a new
Map with cloned Sets for each key (i.e., for each [k,v] produce [k, new
Set(v)]), so callers receive independent structures and cannot mutate
storyIdsByFileCache or its Sets.

Comment on lines +60 to 63
// Expose to in-process consumers (addon presets) via the active-service registry.
// Dev server is single-instance, so only one service is ever active.
setActiveChangeDetectionService(changeDetectionService);

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 | ⚡ Quick win

Register the active service only when change detection is actually active.

The registry is set before any start() path runs. In cases like ignorePreview, disabled feature, or unavailable adapter, consumers can still retrieve a non-started service instance, which breaks the API contract.

💡 Proposed fix (gate registration by startability)
-  // Expose to in-process consumers (addon presets) via the active-service registry.
-  // Dev server is single-instance, so only one service is ever active.
-  setActiveChangeDetectionService(changeDetectionService);
   if (!options.ignorePreview) {
     if (!features.changeDetection) {
+      setActiveChangeDetectionService(undefined);
       changeDetectionService.start(undefined, false);
     }
@@
     if (features.changeDetection) {
       let adapter: ChangeDetectionAdapter | undefined;
@@
-      changeDetectionService.start(adapter, true);
+      if (adapter) {
+        setActiveChangeDetectionService(changeDetectionService);
+      } else {
+        setActiveChangeDetectionService(undefined);
+      }
+      changeDetectionService.start(adapter, true);
     }
   }
🤖 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 `@code/core/src/core-server/dev-server.ts` around lines 60 - 63, The active
service is being registered unconditionally via
setActiveChangeDetectionService(changeDetectionService) before any start() path
runs, exposing a non-started instance; change this to only register the service
when change detection is actually startable/active (e.g. after verifying the
feature flags like ignorePreview and adapter availability or after a successful
start() / startability check on changeDetectionService) so consumers cannot
retrieve a non-started service; update the logic around
setActiveChangeDetectionService, gating it by the changeDetectionService's
startability (or post-start success) and remove the unconditional early
registration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:normal Run our default set of CI jobs (choose this for most PRs). core feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant