Skip to content

PPLT-5844: add IntelliStory storybook affected-story filtering#2339

Open
RaghavsBrowserStack wants to merge 3 commits into
masterfrom
PPLT-5844
Open

PPLT-5844: add IntelliStory storybook affected-story filtering#2339
RaghavsBrowserStack wants to merge 3 commits into
masterfrom
PPLT-5844

Conversation

@RaghavsBrowserStack

Copy link
Copy Markdown
Contributor

What

Adds IntelliStory: for a Storybook build, it diffs against a baseline (explicit or API-predicted) and filters the snapshot set down to only the stories a change actually affects, bailing to a full snapshot run whenever it can't safely reason about the change.

This restores the functionality previously reverted in #2310, under the IntelliStory name.

cli-command

  • src/intelliStory.jsapplyIntelliStory() and IntelliStoryBailError, exported via the new ./intelliStory subpath.
  • src/lockfileDiff.js, src/graphTrace.js (+ graphTraceTemplate.html).
  • Adds glob-to-regexp, stream-json, and optional snyk-nodejs-lockfile-parser dependencies.

client

  • getStatus() accepts the intelli_story_graph job type (sync response carries the graph payload).
  • getIntelliStorySnapshotNameToCommit() and generateIntelliStoryGraph() hit the /intelli_story endpoints.

Binary-crash fix (folds in #2306)

intelliStory.js and lockfileDiff.js bind createRequire to cjsRequire (not require). When transpiled to CommonJS for the packaged binary, naming it require collides with Babel's preset-env + transform-import-meta and crashes on startup with TypeError: _require is not a function. A new static regression guard, test/noRequireBinding.test.js, scans all packages/*/src for the footgun (with a matching .semgrepignore rationale).

Testing

  • cli-command: 145/145 specs pass (intelliStory, lockfileDiff, graphTrace, index, noRequireBinding).
  • client: IntelliStory client tests pass. (Two pre-existing proxy.test.js failures are unrelated — a Node 22 Invalid URL message-format difference in files this PR does not touch.)
  • yarn build compiles cleanly; dist/intelliStory.js emits cjsRequire.

Follow-up

The external @percy/storybook consumer will need the matching @percy/cli-command/intelliStory / applyIntelliStory update.

…iltering

Introduces IntelliStory: given a Storybook build, it diffs against a baseline
(explicit or API-predicted) and filters the snapshot set down to the stories a
change actually affects, bailing to a full run whenever it can't reason about
the change.

- cli-command: intelliStory.js (applyIntelliStory, IntelliStoryBailError),
  lockfileDiff.js and graphTrace.js, exported via the ./intelliStory subpath;
  adds glob-to-regexp, stream-json and optional snyk-nodejs-lockfile-parser.
- client: getStatus() accepts the intelli_story_graph job type, plus
  getIntelliStorySnapshotNameToCommit() and generateIntelliStoryGraph()
  hitting the /intelli_story endpoints.

Binds createRequire to cjsRequire (not `require`) in intelliStory.js and
lockfileDiff.js so the CommonJS-transpiled packaged binary doesn't crash with
"_require is not a function", and adds noRequireBinding.test.js as a static
regression guard (with the matching .semgrepignore rationale).
throw new IntelliStoryBailError(`IntelliStory: invalid statsFile "${statsName}" — must be a .json filename; running full snapshot set`);
}
// statsName is path.basename'd and regex-validated above; buildDir is operator-supplied config (reviewed, approved by security)
const resolvedStatsPath = path.join(path.resolve(buildDir), statsName); // nosemgrep
throw new IntelliStoryBailError(`IntelliStory: invalid statsFile "${statsName}" — must be a .json filename; running full snapshot set`);
}
// statsName is path.basename'd and regex-validated above; buildDir is operator-supplied config (reviewed, approved by security)
const resolvedStatsPath = path.join(path.resolve(buildDir), statsName); // nosemgrep
Comment thread packages/cli-command/src/intelliStory.js Fixed
Comment thread packages/cli-command/src/intelliStory.js Fixed
// pick a canonical source, so bail.
const LOCKFILE_NAMES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
// n is from the hardcoded LOCKFILE_NAMES allowlist (reviewed, approved by security)
const presentLockfiles = LOCKFILE_NAMES.filter(n => fs.existsSync(path.join(absManifestDir, n))); // nosemgrep
// pick a canonical source, so bail.
const LOCKFILE_NAMES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
// n is from the hardcoded LOCKFILE_NAMES allowlist (reviewed, approved by security)
const presentLockfiles = LOCKFILE_NAMES.filter(n => fs.existsSync(path.join(absManifestDir, n))); // nosemgrep
Comment thread packages/cli-command/src/intelliStory.js Fixed
if (oldLockfile === newLockfile) return [];

// joined with the literal 'package.json' under the resolved absManifestDir (reviewed, approved by security)
const packageJson = fs.readFileSync(path.join(absManifestDir, 'package.json'), 'utf8'); // nosemgrep
Comment thread packages/cli-command/src/intelliStory.js Fixed
// applies, without exporting the module-private helpers it delegates to.
function embeddedJson(html, name) {
// test-only helper; name is a hardcoded literal ('vertices'/'edges') from the test, not external input (reviewed, approved by security)
let match = html.match(new RegExp(`const ${name} = (.*);`)); // nosemgrep
…trim comments

- intelliStory.js: read the enriched stats file with a single
  JSON.parse(fs.readFileSync(...)) instead of streaming it with stream-json.
  The stats payload is only a few MB, so in-memory parsing is simpler and
  removes the stream-json dependency (and, with it, the last createRequire
  binding in this file — the packaged-binary footgun now only exists in
  lockfileDiff.js for the optional snyk parser).
- Remove stream-json from cli-command dependencies and the lockfile.
- Strip explanatory comments introduced with the feature across the
  IntelliStory source, tests and trace template. Coverage directives
  (istanbul ignore), semgrep suppressions (nosemgrep) and the cjsRequire /
  loadSnyk rationale in lockfileDiff.js are kept — removing them would break
  the 100% coverage gate and the semgrep scan.
throw new IntelliStoryBailError(`IntelliStory: manifest changes span multiple directories (${uniqueDirs.join(', ')}); running full snapshot set`);
}
const manifestDir = uniqueDirs[0];
const absManifestDir = path.resolve(projectRoot, manifestDir); // nosemgrep
throw new IntelliStoryBailError(`IntelliStory: manifest changes span multiple directories (${uniqueDirs.join(', ')}); running full snapshot set`);
}
const manifestDir = uniqueDirs[0];
const absManifestDir = path.resolve(projectRoot, manifestDir); // nosemgrep
throw new IntelliStoryBailError(`IntelliStory: lockfile "${lockfileRepoPath}" not present at base ref ${baseRef}; running full snapshot set`);
}

const newLockfile = fs.readFileSync(path.join(absManifestDir, lockfileName), 'utf8'); // nosemgrep
/* istanbul ignore next */
if (rel.startsWith(dotPlatform)) rel = rel.slice(dotPlatform.length);
else if (rel.startsWith(dotPosix)) rel = rel.slice(dotPosix.length);
const abs = path.resolve(invocationDir, rel); // nosemgrep
@RaghavsBrowserStack RaghavsBrowserStack changed the title feat(cli-command,client): add IntelliStory storybook affected-story filtering PPLT-5844: add IntelliStory storybook affected-story filtering Jul 15, 2026
@RaghavsBrowserStack RaghavsBrowserStack marked this pull request as ready for review July 15, 2026 14:29
@RaghavsBrowserStack RaghavsBrowserStack requested a review from a team as a code owner July 15, 2026 14:29
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