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

Merged
RaghavsBrowserStack merged 29 commits into
masterfrom
PPLT-5844
Aug 10, 2026
Merged

PPLT-5844: add IntelliStory storybook affected-story filtering#2339
RaghavsBrowserStack merged 29 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).
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/test/graphTrace.test.js Fixed
…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.
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
@RaghavsBrowserStackRaghavsBrowserStack changed the title feat(cli-command,client): add IntelliStory storybook affected-story filteringPPLT-5844: add IntelliStory storybook affected-story filteringJul 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 ownerJuly 15, 2026 14:29
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
RaghavsBrowserStackand others added 3 commits July 21, 2026 13:20
Add tests for the graceful-bail paths added in "apply suggested fixed":
- malformed stats-file JSON (validateAndReadStats)
- unreadable current lockfile (getAffectedPackages)
- unreadable package.json (getAffectedPackages)
These run on Node 14 (they bail before the Node>=18 snyk path), fixing the
lines/statements coverage-threshold failure on the Node 14 test job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GLOB_CHARS only detected * and ? so brace ({a,b}) and bracket ([abc])
patterns fell through to exact-match, silently disabling the intended
glob semantics for these documented config options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:001c036Reviewers: stack:code-reviewer

Summary

Adds IntelliStory — an affected-story graph feature for @percy/storybook: parses Storybook build stats, diffs against a git baseline (incl. lockfile/dependency diffing via optional snyk-nodejs-lockfile-parser), enqueues a server-side affected-story graph job, tags snapshots for selection, and optionally renders a debug trace.html. Plumbs intelliStory/storybookPath through @percy/client and @percy/core, and adds a Node 20 CI leg for the Node-18+-only snyk path.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed.
HighSecurityInput validation and sanitizationPassassertSafeRef blocks flag-injection; safeJson escapes HTML/JS breakouts; paths anchored via basename.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource-ownership surface.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1 ($-pattern .replace) still open. Finding 2 (glob detection gap) resolved in 001c036.
HighCorrectnessError handling is explicit, no swallowed exceptionsPass*Mostly graceful IntelliStoryBailError; one inconsistent rethrow — Finding 3.
HighCorrectnessNo race conditions or concurrency issuesPassNo new concurrency.
MediumTestingNew code has corresponding testsPassBroad coverage across new modules.
MediumTestingError paths and edge cases testedPass*Package-affected merge branch istanbul ignored rather than tested — Finding 5.
MediumTestingExisting tests still pass (no regressions)PassNot re-run here; CI covers.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo queries.
MediumPerformanceLong-running tasks use background jobsPassGraph job is server-side/async with timeout bail.
MediumQualityFollows existing codebase patternsPassConsistent with client/core plumbing conventions.
MediumQualityChanges are focused (single concern)PassScoped to IntelliStory feature.
LowQualityMeaningful names, no dead codePassClear naming.
LowQualityComments explain why, not whatPass.semgrepignore rationale well-documented.
LowQualityNo unnecessary dependencies addedPasssnyk-nodejs-lockfile-parser is optional/lazy-loaded.

Findings

  • File:packages/cli-command/src/graphTrace.js:114-116

  • Severity: Medium

  • Reviewer: stack:code-reviewer (verified)

  • Issue:.replace('__VERTICES_JSON__', safeJson(...)) uses plain-string replacement values. JS honors $$, $&, $`, $', $n special patterns in the replacement string even when the search arg is a literal. Embedded JSON contains arbitrary file paths; a path with $' re-injects the rest of the template, breaking the embedded JS. safeJson's escaping doesn't cover this — the corruption happens in the .replace call itself.

  • Suggestion: Pass a function as the replacement: .replace('__VERTICES_JSON__', () => safeJson(...)). Add a graphTrace.test.js case with $&/$'/$$ in a vertex name.

  • File:packages/cli-command/src/intelliStory.js:16, 34-41

  • Severity: Medium — ✅ RESOLVED in 001c036

  • Reviewer: stack:code-reviewer (verified)

  • Issue:GLOB_CHARS = /[*?]/ only routed */? patterns through the glob matcher, but patternToRegex enables {extended:true, globstar:true} (brace/bracket support). A configured bailOnChanges: ['src/{a,b}.js'] or bracket pattern silently fell through to literal str === pattern and never matched — silently disabling a documented config option (bailOnChanges/untraced).

  • Fix applied: Broadened detection to const GLOB_CHARS = /[*?{}[\]]/;. Added brace- and bracket-glob tests to assertNoBailOnChanges() and enforceUntraced() — verified failing under the old regex and passing under the fix (full 156-spec suite green).

  • File:packages/cli-command/src/intelliStory.js:326-336

  • Severity: Medium

  • Reviewer: stack:code-reviewer

  • Issue: Only SNYK_LOCKFILE_PARSER_UNAVAILABLE is converted to a graceful IntelliStoryBailError; a buildDepTree parse failure (malformed/older-ref lockfile) rethrows as a raw Error. Every other failure path degrades to "running full snapshot set."

  • Suggestion: Confirm the @percy/storybook caller treats allapplyIntelliStory throws as fall-back-to-full-set. If not, wrap this rethrow in IntelliStoryBailError for consistency.

  • File:packages/cli-command/src/intelliStory.js:67-70

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue:assertSafeRef regex correctly blocks leading -, but also rejects ordinary safe ref syntax like HEAD~5 / abc123^2.

  • Suggestion: Confirm intentional, or widen charset to include ~^ (still safe given the leading-char restriction).

  • File:packages/cli-command/src/intelliStory.js:438-440

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The packageAffectedNodes merge into affectedNodes (the actual mechanism folding lockfile-driven dep changes into the graph) is /* istanbul ignore next */ rather than covered by a real test.

  • Suggestion: Add an itPosix-style Node≥18 integration test exercising the merge end-to-end.

  • File:packages/cli-command/src/intelliStory.js (resolveAndIndex)

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Absolute module ids resolving outside projectRoot (symlinked pkg, monorepo sibling) produce ../-prefixed paths added straight into the files index without a guard.

  • Suggestion: Bail or drop entries where rel.startsWith('..').

  • File:.semgrepignore:118-131 / packages/client/src/client.js:2591-2592 / .github/workflows/test.yml

  • Severity: Info

  • Reviewer: stack:code-reviewer

  • Issue: (a) Whole-file semgrep suppression means future traversal code in the growing intelliStory.js goes unchecked. (b) client.intelliStoryStats is lazily-created, never reset — accumulates across builds if a client is reused. (c) New test-node20 CI job (workflow change) — flagged for visibility; mirror logic looks consistent, no action requested.

  • Suggestion: Consider narrowing semgrep scope over time; reset stats in createBuild.


Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:966c05dReviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), findings then re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a new static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

The bail-first posture is the right architecture for a feature that can silently drop visual coverage, and it is applied thoroughly — 15+ explicit bail sites covering missing build id, unreadable/invalid stats, git failures, ambiguous or absent lockfiles, manifest changes spanning directories, browser-upgrade builds, and graph-job timeouts. All four Medium findings below are gaps in that otherwise-sound safety net, not architectural problems.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through the existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync is used without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassExtensive and largely correct bail coverage. Four Medium gaps: findings 1–4.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps: the three API calls are not (finding 1), and dropped stats modules are logged at debug only (finding 4).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedPassBail branches are covered individually (commit e071c952 restored 100% coverage). The glob semantics in finding 3 are asserted deliberately by intelliStory.test.js:286-289.
MediumTestingExisting tests still pass (no regressions)PassAll CI checks green on 966c05d: Lint, Build, Build & verify executable, Regression, every package suite (incl. the new Node 20 leg), CodeQL, Semgrep OSS.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover. Minor: findings 5–7.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer (refined by orchestrator verification)
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or DNS failure therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); it is only thrown exceptions that leak.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph, and in pollGraphStatus treat a throw as a non-done status.

2. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (verified)
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail, and filtering proceeds as though nothing global changed. This is the under-snapshot direction: real visual regressions ship unnoticed.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

3. Bare-extension bailOnChanges globs silently match nothing below the repo root

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex), consumed at :251
  • Severity: Medium
  • Reviewer: stack:code-reviewer (severity downgraded from High by the orchestrator — see below)
  • Issue: With { extended: true, globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$ — verified locally — so it matches only root-level files, never src/components/Button.css. A user who configures bailOnChanges: ['*.css'] expecting "bail on any stylesheet change" gets no bail, and IntelliStory filters as if nothing unsafe changed. untraced shares the semantics but fails safe (nothing is dropped, so more snapshots are taken).
    Downgraded from the sub-reviewer's High because this is standard glob behaviorminimatch, picomatch, and GitHub Actions paths: filters all treat a bare *.ext as root-only — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. Still worth flagging: bailOnChanges is the one user-facing safety valve and the failure mode is silent.
  • Suggestion: Either document that patterns are path-segment-scoped and **/*.css is required (there is currently no documentation for bailOnChanges/untraced anywhere in the repo), or auto-broaden bare-name patterns before compiling: const p = pattern.includes('/') ? pattern : '**/' + pattern;. If the current semantics stay, rename the test to state the intent ('a bare-extension pattern does not match nested paths') so the behavior reads as deliberate.

4. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run still proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate the existing message to log.warn, and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

5. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue:resolveAndIndex runs only when copy.type === 'src'. Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the external enrichment tool ever emits a third type carrying an absolute path — an asset, CSS, or JSON import — that raw absolute path ships unnormalized in the modules payload: it leaks the developer's home or CI runner path, and it cannot be correlated against the project-relative affectedNodes, which is a false-negative path.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe and type-agnostic.

6. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (correction: rename detection is git's default since 2.9, so the inconsistency is present out of the box, not only under custom config)
  • Issue:affectedNodes comes from git diff --name-only with rename detection active, which collapses a rename to the new path alone; getAffectedFileLocations runs the same diff with --no-renames, which yields a delete + add pair. The two views of one diff can disagree for renamed files. Low practical impact — the old path no longer exists to be imported — but it makes the two graph inputs non-comparable.
  • Suggestion: Add --no-renames to the gitDiffNames invocation for determinism and parity with :84.

7. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer (verified)
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. This is opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or a mis-selection — often inside a locked-down or air-gapped CI environment where the CDN is unreachable and the trace renders blank.
  • Suggestion: Vendor the (small) Drawflow bundle into the package and inline it into the template.

8. Whole-file .semgrepignore suppression for intelliStory.js

  • File:.semgrepignore:49-57
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: The suppression is file-level rather than scoped to the specific path.join/path.resolve sites that trigger the false positive. intelliStory.js does a fair amount of fs/path work, so any genuinely unsafe join added later goes unscanned. The stated rationale (inline // nosemgrep not honored by the CI semgrep version) is legitimate and well documented.
  • Suggestion: Track a follow-up to narrow the suppression once the CI semgrep version honors inline annotations.

9. No bail is logged at the throw site

  • File:packages/cli-command/src/intelliStory.js — all IntelliStoryBailError throw sites
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: Every bail constructs a carefully worded message but none logs at warn/info before throwing, so whether a user ever learns that filtering did not apply depends entirely on the catch site in @percy/storybook — outside this repo and, per the PR body, still a follow-up. If that catcher logs at debug or swallows while falling back, the fail-safe is invisible.
  • Suggestion:log.warn(message) at the throw site (or inside a small bail(message, log) helper) so visibility does not depend on the external consumer.

Verified non-issues

  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced anywhere in cli-command — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:01aa1f6Reviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), second independent pass; all findings re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

Note on this run. Head moved from 966c05d to 01aa1f6, but both new commits (01aa1f6, f6ef573) are Merge branch 'master' into PPLT-5844git diff 966c05d 01aa1f6 over the PR-owned paths is empty, so the reviewable code is byte-identical to the previously reviewed head. The fast-path did not fire because the intermediate merge f6ef573 carries no prior Claude Code Review status, so a full review ran. This second, independent pass surfaced finding 1, which the first pass missed. The code has not regressed — the assessment has sharpened, and the verdict changes accordingly.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync runs without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1: bailOnChanges/untraced globs are matched against repo-root-relative paths with no rebasing, while story importPaths in the same function are rebased from the invocation directory — so the user's safety valve is silently inert for monorepo/subdirectory invocation.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps are Medium, not High: the three API calls are unwrapped (finding 2), malformed globs are swallowed to false (finding 4), and dropped stats modules are logged at debug only (finding 5).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedFailBail branches are covered individually, but every fixture builds a git repo with files at its root — there is no cwd ≠ projectRoot case, which is exactly why finding 1 went unnoticed. No test simulates a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)PassAll CI checks were green on 966c05d, whose PR-owned tree is identical to this head.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. bailOnChanges/untraced globs are never rebased to the invocation directory — the safety valve is silently inert in a monorepo

  • File:packages/cli-command/src/intelliStory.js:250-252, :259-262 (compare :415-426)

  • Severity: High

  • Reviewer: stack:code-reviewer (second pass); mechanism verified by the orchestrator

  • Issue:affectedNodes comes from git diff --name-only, which always emits repo-root-relative paths regardless of cwd. assertNoBailOnChanges and enforceUntraced match user-supplied globs directly against those paths, and glob-to-regexp fully anchors the result (^…$). Meanwhile, twenty lines further down, the same function builds invocationDir = process.cwd() and rebases each story's importPath from the invocation directory to project-relative (:415-426) — so the file demonstrably does expect cwd ≠ projectRoot. User globs get no such treatment.

    Concrete failure: a monorepo user runs cd packages/ui && percy storybook ./storybook-static with bailOnChanges: ['webpack.config.js']. A change to that file lands in affectedNodes as packages/ui/webpack.config.js; the pattern compiles to /^webpack\.config\.js$/ and does not match. No bail fires, no warning is logged, and IntelliStory filters as though nothing risky changed — the under-snapshot direction, where a genuine visual regression ships unreviewed. Monorepo-with-subdirectory is the mainstream Storybook layout, and bailOnChanges exists precisely as the escape hatch for changes the graph can't reason about.

    Not covered by any test: every fixture in intelliStory.test.js constructs a repo with files at its root, so no cwd ≠ projectRoot case is exercised.

  • Suggestion: Rebase user patterns the same way story paths already are, before matching:

    constrebase=g=>{if(path.isAbsolute(g))returnpath.relative(projectRoot,g).split(path.sep).join('/');constabs=path.resolve(invocationDir,g);constrel=path.relative(projectRoot,abs);returnrel.startsWith('..') ? g : rel.split(path.sep).join('/');};

    and pass bailOnChanges.map(rebase) / untraced.map(rebase) into the two functions. Add a test with cwd set to a nested package directory. If root-relative globs are the intended contract instead, that must be documented and a bail should fire when cwd !== projectRoot and patterns are configured — silently doing nothing is the one outcome to avoid.

2. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer — independently reported by both review passes
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or connection reset therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); only thrown exceptions leak. diffLockfileDeps (:316) has the same shape: it remaps only SNYK_LOCKFILE_PARSER_UNAVAILABLE and re-throws anything else raw.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status. Add tests where each client method rejects.

3. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail. Same under-snapshot direction as finding 1, and the two compound for monorepo users.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

4. A malformed glob is silently treated as "matches nothing"

  • File:packages/cli-command/src/intelliStory.js:34-43 (matchesPattern)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (second pass); verified — glob-to-regexp('src/[') throws Unterminated character class, 'src/{a' throws Unterminated group
  • Issue:try { … } catch { return false } means a typo'd pattern in a user's config compiles-fails and is then treated as never matching. For untraced that fails safe (the file stays traced). For bailOnChanges it fails open: the guardrail is silently disabled with nothing logged anywhere. The over-length guard at :22 has the same shape and is explicitly tested as non-throwing (intelliStory.test.js:269-271).
  • Suggestion:log.warn when a pattern fails to compile — a broken guardrail must be visible:
    }catch(e){log.warn(`IntelliStory: ignoring invalid pattern "${pattern}": ${e.message}`);returnfalse;}
    Better still, validate the patterns once at config-parse time rather than per-path.

5. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator (first pass)
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate to log.warn and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

6. Bare-extension globs don't match nested paths

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: With { globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior — minimatch, picomatch, and GitHub Actions paths: filters agree — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. It is listed because it lands on the same safety valve as findings 1 and 4, and there is currently no documentation for bailOnChanges/untraced anywhere in the repo.
  • Suggestion: Document that patterns are path-segment-scoped and **/*.css is required. If you prefer forgiving semantics, auto-broaden bare-name patterns: const p = pattern.includes('/') ? pattern : '**/' + pattern; — note this must be decided together with finding 1's rebasing.

7. noRequireBinding guard is narrower than the crash class it protects

  • File:packages/cli-command/test/noRequireBinding.test.js:40
  • Severity: Low
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:FORBIDDEN = /\b(?:const|let|var)\s+require\s*=\s*createRequire\b/ only catches a single-line declaration. The same startup crash returns via import { createRequire as require } from 'module', const { default: require } = …, or a line-wrapped const require =\n createRequire(…) — none of which match.
  • Suggestion: Flag any binding of the bare identifier require (import specifier, declaration LHS, or parameter), not just the createRequire-RHS form.

8. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the enrichment tool ever emits a third type carrying an absolute path, that path ships unnormalized: it leaks a local or CI-runner path and cannot be correlated against project-relative affectedNodes.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe.

9. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Rename detection is git's default since 2.9, so affectedNodes collapses a rename to the new path alone while getAffectedFileLocations (run with --no-renames) sees a delete + add pair. The two views of one diff disagree for renamed files.
  • Suggestion: Add --no-renames to gitDiffNames for parity.

10. path.relative does not case-fold when computing project-relative module ids

  • File:packages/cli-command/src/intelliStory.js:117-128 (resolveAndIndex)
  • Severity: Low (mechanism demonstrated, real-world trigger unconfirmed)
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:path.relative('/Users/bob/proj', '/Users/Bob/Proj/src/App.js') returns '../../Bob/Proj/src/App.js' — exact segment comparison, no case folding, on every platform. On case-insensitive-but-preserving filesystems (APFS, NTFS), any casing divergence between the bundler's absolute module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that can never match a real repo-relative path. The reviewer could not force an actual divergence, so this is flagged as plausible rather than confirmed.
  • Suggestion: At minimum, warn when the computed relative path escapes projectRoot (starts with ..) rather than silently indexing it; optionally normalize both sides through fs.realpathSync.

11. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer — reported by both passes
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. Opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or mis-selection — often inside a locked-down or air-gapped CI runner where it renders blank.
  • Suggestion: Vendor the small Drawflow bundle and inline it into the template.

12. Minor: bail visibility, semgrep scope, stats-shape logging

  • Severity: Low
  • Reviewer: stack:code-reviewer (both passes)
  • Issue: (a) No bail logs at the throw site, so user visibility depends entirely on the @percy/storybook catch — outside this repo and still a follow-up per the PR body. (b) .semgrepignore:49-57 suppresses intelliStory.js file-wide rather than at the flagged joins, so future unsafe joins in this fs/path-heavy file go unscanned; the rationale is documented and the constraint (inline // nosemgrep unsupported by the CI version) is real. (c) readStats's stats.modules || [] (:161) makes a stats file with no modules key indistinguishable from a legitimately empty one.
  • Suggestion: (a) log.warn(message) inside a small bail(message, log) helper. (b) Track narrowing the suppression. (c) log.debug when the key is absent versus empty.

Informational — not counted as defects

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested at lockfileDiff.test.js:85): Storybook addons are typically devDependencies and are imported directly by preview.js/story files. A pure addon version bump with no source diff will not surface through this path. It is explicit and tested, so likely intentional (relying on the server-side module graph instead) — worth a sanity check with the team given how conservative this feature aims to be.
  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: FAIL

Comment threadpackages/cli-command/test/intelliStory.test.js Fixed
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:91bd770Reviewers:stack:code-reviewer (prior passes), fix verification by the orchestrator against source at this head

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (cjsRequire) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

This head resolves the High finding from the 01aa1f6 review along with three of the five Mediums. Verdict flips to PASS.

Fix verification (vs. 01aa1f6)

#FindingSevStatus at 91bd770
1bailOnChanges/untraced globs not rebased to the invocation dirHighFixed
2Client calls throw raw Error, not IntelliStoryBailErrorMediumOpen
3.storybook config dir hardcodedMediumFixed
4Malformed glob silently swallowed to "no match"MediumFixed
5Dropped stats modules logged at debug, no thresholdMediumFixed
6Bare-extension globs don't match nested pathsMediumOpen (by design; still undocumented)
7noRequireBinding regex narrower than the crash classLowOpen
8mapEntry normalizes source only for type === 'src'LowOpen
9gitDiffNames omits --no-renamesLowOpen
10path.relative does not case-foldLowOpen (was unconfirmed)
11trace.html loads Drawflow from a CDNLowOpen
12No log.warn at bail throw sitesLowPartially addressed

Finding 1 is fixed properly, not papered over. resolvePattern (intelliStory.js:88-100) rebases each user pattern from the invocation directory onto the repo root — the same basis normalizeImportPath already used for story paths, so the asymmetry that caused the bug is gone. Beyond that, the fix handles the two readings asymmetrically by direction of harm, which is the right call:

  • assertNoBailOnChanges (:394-425) bails when a pattern matches only under the repo-root reading, logging a warning with a concrete rewrite suggestion — a stale root-relative config still protects the user.
  • enforceUntraced (:433-462) declines to apply such a pattern and warns, since applying it would drop snapshots.

A <rootDir>/ prefix is an explicit opt-out, patterns resolving outside the repo bail rather than matching nothing, and resolveConfigDirs (:361-390) additionally checks that a configured directory actually exists — catching a wrong-basis configDir at config time instead of silently never matching. Findings 3, 4 and 5 are each fixed with matching care: isInsideDirs distinguishes single-segment (any depth) from multi-segment (prefix) config dirs so config/storybook can't match config/storybook-old; patternError validates globs up front and routes a compile failure to a bail for bailOnChanges and a warn-skip for untraced; and readStats now separates deliberately-excluded modules from unresolved ones, warns on the latter and bails past a 10% ratio of resolvable candidates — so a stats file heavy in node_modules no longer trips the threshold. intelliStory.test.js grows 364 lines with cwd ≠ projectRoot fixtures (projectRoot: '/repo', invocationDir: '/repo/packages/ui') and a resolveConfigDirs() suite — the gap that let finding 1 through is now covered.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth flows through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before spawnSync; statsFile narrowed to a bare .json basename; spawnSync runs without a shell. User globs are now validated up front via patternError. Dedicated security lens is disabled in this orchestrator.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassThe path-basis defect is fixed, with both readings handled by direction of harm. Remaining gaps are Medium/Low.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassGlob compile failures are no longer swallowed, and incomplete graphs now bail past a threshold. Finding 2 (unwrapped client calls) remains, at Medium.
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop; no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass+364 test lines this push, covering resolvePattern, resolveConfigDirs, and the <rootDir>/ opt-out.
MediumTestingError paths and edge cases testedPassThe cwd ≠ projectRoot gap is closed. Still untested: a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)Pass46 checks green, incl. Lint, Build & verify executable, Regression, all cli-command suites and the Node 20 leg. Test @percy/core still pending at review time — unrelated to the changed packages.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne GET, one POST, bounded poll (12 × 5s). In-memory stats parsing.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled.
MediumQualityFollows existing codebase patternsPassMatches PercyClient conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on and the CI leg that exercises the lockfile path.
LowQualityMeaningful names, no dead codePassstream-json confirmed absent. New helpers (resolvePattern, patternError, rootReadingHit, suggestRewrite) are well named and each has a stated reason to exist.
LowQualityComments explain why, not whatPassThe new comments are unusually good — they record the direction of harm reasoning behind each asymmetry ("recognising one directory too many only costs snapshots, while missing the real one loses comparisons"), which is exactly what a future maintainer needs.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp used; snyk-nodejs-lockfile-parser correctly optional with a Node 20 CI leg.

Findings

2. API/network failures still escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:292, :331, :555
  • Severity: Medium
  • Reviewer: stack:code-reviewer (reported independently by both prior passes); re-verified unfixed at this head
  • Issue: Verified still open: getStatus('intelli_story_graph', …) in pollGraphStatus (:292), getIntelliStorySnapshotNameToCommit (:331), and generateIntelliStoryGraph (:555) are all bare awaits with no surrounding try/catch. Every other failure mode in this file — git, fs, config, lockfile, malformed glob, degraded graph — is deliberately converted to IntelliStoryBailError so the caller can degrade to a full snapshot run. A transient 5xx, timeout, or connection reset from these three still propagates as a plain Error, so if @percy/storybook catches IntelliStoryBailError specifically, an API blip hard-fails the storybook command instead of falling back. The graph-job timeout path is handled correctly (:561); only thrown exceptions leak. Not a merge blocker under the verdict rule, but it is now the last inconsistency in an otherwise uniformly fail-safe module.
  • Suggestion:
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status so the existing :561 bail covers it. Add tests where each client method rejects.

6. Bare-extension globs still don't match nested paths, and the contract is undocumented

  • File:packages/cli-command/src/intelliStory.js:28
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass)
  • Issue:glob-to-regexp('*.css', { globstar: true }) compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior (minimatch, picomatch, GitHub Actions paths: all agree) and is deliberately asserted by the tests, so it is not a defect. It is still listed because the new rootReadingHit warning covers the basis mistake but not the depth mistake — a user who writes *.css gets no warning at all — and there remains no documentation for bailOnChanges/untraced/configDir anywhere in the repo.
  • Suggestion: Document the semantics now that the basis rules are non-trivial (invocation-relative, <rootDir>/ opt-out, path-segment-scoped globs). Optionally extend the existing warning: if a pattern has no / and no **, and some affected node matches **/<pattern>, warn the same way the root-reading case does.

7–11. Carried-over Lows, unchanged at this head

  • noRequireBinding.test.js:40FORBIDDEN still only matches a single-line const/let/var require = createRequire. The same crash returns via import { createRequire as require } from 'module' or a line-wrapped declaration. Broaden to flag any binding of the bare require identifier.
  • intelliStory.js:229mapEntry still gates normalization on copy.type === 'src'. Drop the gate; resolveAndIndex already passes non-absolute values through untouched.
  • intelliStory.js:138gitDiffNames still omits --no-renames while getAffectedFileLocations uses it, so the two views of one diff disagree for renamed files.
  • intelliStory.jsresolveAndIndexpath.relative does not case-fold, so a casing divergence between bundler module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that never match. Mechanism is real, real-world trigger still unconfirmed. Cheapest guard: warn when the computed relative path starts with ...
  • graphTraceTemplate.html:8, :12trace.html still loads Drawflow from cdn.jsdelivr.net, so the diagnostic renders blank in an air-gapped or locked-down CI runner. Vendor the bundle.

12. Bail visibility — partially addressed

  • Severity: Low
  • Issue: This push adds ten log.warn calls, so pattern and configDir problems are now visible. The IntelliStoryBailError throw sites themselves still don't log before throwing, so whether a user learns that filtering did not apply continues to depend on the @percy/storybook catch site — outside this repo and still a follow-up per the PR body.
  • Suggestion: A small bail(message, log) helper that warns and throws.

Informational

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested): Storybook addons are usually devDependencies imported directly by preview.js/story files, so a pure addon bump with no source diff won't surface here. Explicit and tested, so presumably intentional — worth a sanity check with the team.
  • .semgrepignore still suppresses intelliStory.js file-wide rather than at the flagged joins. The file grew ~290 lines this push, which widens the unscanned surface; the documented rationale (inline // nosemgrep unsupported by the CI semgrep version) still holds. Worth narrowing when tooling allows.
  • Version strings moved 1.32.6-beta.31.32.6 via the master release merge; no functional change.

Verdict: PASS

@RaghavsBrowserStack
RaghavsBrowserStack merged commit 8311242 into masterAug 10, 2026
48 checks passed
@RaghavsBrowserStack
RaghavsBrowserStack deleted the PPLT-5844 branch August 10, 2026 12:57
RaghavsBrowserStack added a commit that referenced this pull request Aug 11, 2026
* bump version
* remove stray archive test fixtures committed by mistake
packages/core/.test-archive-mixed and .test-archive-symlink were leftovers
from a local unit-test run that got picked up by the version-bump commit.
.test-archive-symlink/linked.json is an absolute symlink into a developer's
home directory, so it dangles on a fresh CI checkout and makes
`babel packages -d build` (build_cjs) fail with ENOENT, breaking the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix unwanted commits
* remove stray archive test fixtures reintroduced by the master merge
d302b09 deleted these on this branch, but master picked up the same
accidental add in d663543 (bump version, #2377), so merging master back in
(2405903) restored them and left PR #2378 with an empty diff.
packages/core/.test-archive-symlink/linked.json is an absolute symlink into a
developer's home directory. It dangles on a fresh CI checkout, so
`babel packages -d build` (build_cjs) fails with ENOENT and takes down the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* regenerate yarn.lock so `lerna publish` sees a clean tree
The lockfile block added in 8311242 (IntelliStory, #2339) was edited by hand
rather than regenerated, leaving it inconsistent with the dependency graph:
- @types/node@* had no entry, though @types/cacheable-request, @types/keyv,
@types/responselike and @types/yauzl all require it
- get-stream@^5.1.0 had no entry, though cacheable-request@^7.0.2 requires it
- @types/yauzl@^2.9.1 was orphaned — nothing referenced it
So `yarn` on a clean CI checkout resolved the two missing descriptors, pruned
the orphan and rewrote the file. That left the working tree dirty, and
`lerna publish from-package` aborted with EUNCOMMIT (M yarn.lock), failing the
Release job.
Regenerated with a plain `yarn install`. `yarn install --frozen-lockfile` is
now a no-op, so the release checkout stays clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@RaghavsBrowserStack@prklm10@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Merged
RaghavsBrowserStack merged 29 commits into
masterfrom
PPLT-5844
Aug 10, 2026
Merged

PPLT-5844: add IntelliStory storybook affected-story filtering#2339
RaghavsBrowserStack merged 29 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).
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/test/graphTrace.test.js Fixed
…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.
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
@RaghavsBrowserStackRaghavsBrowserStack changed the title feat(cli-command,client): add IntelliStory storybook affected-story filteringPPLT-5844: add IntelliStory storybook affected-story filteringJul 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 ownerJuly 15, 2026 14:29
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
RaghavsBrowserStackand others added 3 commits July 21, 2026 13:20
Add tests for the graceful-bail paths added in "apply suggested fixed":
- malformed stats-file JSON (validateAndReadStats)
- unreadable current lockfile (getAffectedPackages)
- unreadable package.json (getAffectedPackages)
These run on Node 14 (they bail before the Node>=18 snyk path), fixing the
lines/statements coverage-threshold failure on the Node 14 test job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GLOB_CHARS only detected * and ? so brace ({a,b}) and bracket ([abc])
patterns fell through to exact-match, silently disabling the intended
glob semantics for these documented config options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:001c036Reviewers: stack:code-reviewer

Summary

Adds IntelliStory — an affected-story graph feature for @percy/storybook: parses Storybook build stats, diffs against a git baseline (incl. lockfile/dependency diffing via optional snyk-nodejs-lockfile-parser), enqueues a server-side affected-story graph job, tags snapshots for selection, and optionally renders a debug trace.html. Plumbs intelliStory/storybookPath through @percy/client and @percy/core, and adds a Node 20 CI leg for the Node-18+-only snyk path.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed.
HighSecurityInput validation and sanitizationPassassertSafeRef blocks flag-injection; safeJson escapes HTML/JS breakouts; paths anchored via basename.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource-ownership surface.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1 ($-pattern .replace) still open. Finding 2 (glob detection gap) resolved in 001c036.
HighCorrectnessError handling is explicit, no swallowed exceptionsPass*Mostly graceful IntelliStoryBailError; one inconsistent rethrow — Finding 3.
HighCorrectnessNo race conditions or concurrency issuesPassNo new concurrency.
MediumTestingNew code has corresponding testsPassBroad coverage across new modules.
MediumTestingError paths and edge cases testedPass*Package-affected merge branch istanbul ignored rather than tested — Finding 5.
MediumTestingExisting tests still pass (no regressions)PassNot re-run here; CI covers.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo queries.
MediumPerformanceLong-running tasks use background jobsPassGraph job is server-side/async with timeout bail.
MediumQualityFollows existing codebase patternsPassConsistent with client/core plumbing conventions.
MediumQualityChanges are focused (single concern)PassScoped to IntelliStory feature.
LowQualityMeaningful names, no dead codePassClear naming.
LowQualityComments explain why, not whatPass.semgrepignore rationale well-documented.
LowQualityNo unnecessary dependencies addedPasssnyk-nodejs-lockfile-parser is optional/lazy-loaded.

Findings

  • File:packages/cli-command/src/graphTrace.js:114-116

  • Severity: Medium

  • Reviewer: stack:code-reviewer (verified)

  • Issue:.replace('__VERTICES_JSON__', safeJson(...)) uses plain-string replacement values. JS honors $$, $&, $`, $', $n special patterns in the replacement string even when the search arg is a literal. Embedded JSON contains arbitrary file paths; a path with $' re-injects the rest of the template, breaking the embedded JS. safeJson's escaping doesn't cover this — the corruption happens in the .replace call itself.

  • Suggestion: Pass a function as the replacement: .replace('__VERTICES_JSON__', () => safeJson(...)). Add a graphTrace.test.js case with $&/$'/$$ in a vertex name.

  • File:packages/cli-command/src/intelliStory.js:16, 34-41

  • Severity: Medium — ✅ RESOLVED in 001c036

  • Reviewer: stack:code-reviewer (verified)

  • Issue:GLOB_CHARS = /[*?]/ only routed */? patterns through the glob matcher, but patternToRegex enables {extended:true, globstar:true} (brace/bracket support). A configured bailOnChanges: ['src/{a,b}.js'] or bracket pattern silently fell through to literal str === pattern and never matched — silently disabling a documented config option (bailOnChanges/untraced).

  • Fix applied: Broadened detection to const GLOB_CHARS = /[*?{}[\]]/;. Added brace- and bracket-glob tests to assertNoBailOnChanges() and enforceUntraced() — verified failing under the old regex and passing under the fix (full 156-spec suite green).

  • File:packages/cli-command/src/intelliStory.js:326-336

  • Severity: Medium

  • Reviewer: stack:code-reviewer

  • Issue: Only SNYK_LOCKFILE_PARSER_UNAVAILABLE is converted to a graceful IntelliStoryBailError; a buildDepTree parse failure (malformed/older-ref lockfile) rethrows as a raw Error. Every other failure path degrades to "running full snapshot set."

  • Suggestion: Confirm the @percy/storybook caller treats allapplyIntelliStory throws as fall-back-to-full-set. If not, wrap this rethrow in IntelliStoryBailError for consistency.

  • File:packages/cli-command/src/intelliStory.js:67-70

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue:assertSafeRef regex correctly blocks leading -, but also rejects ordinary safe ref syntax like HEAD~5 / abc123^2.

  • Suggestion: Confirm intentional, or widen charset to include ~^ (still safe given the leading-char restriction).

  • File:packages/cli-command/src/intelliStory.js:438-440

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The packageAffectedNodes merge into affectedNodes (the actual mechanism folding lockfile-driven dep changes into the graph) is /* istanbul ignore next */ rather than covered by a real test.

  • Suggestion: Add an itPosix-style Node≥18 integration test exercising the merge end-to-end.

  • File:packages/cli-command/src/intelliStory.js (resolveAndIndex)

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Absolute module ids resolving outside projectRoot (symlinked pkg, monorepo sibling) produce ../-prefixed paths added straight into the files index without a guard.

  • Suggestion: Bail or drop entries where rel.startsWith('..').

  • File:.semgrepignore:118-131 / packages/client/src/client.js:2591-2592 / .github/workflows/test.yml

  • Severity: Info

  • Reviewer: stack:code-reviewer

  • Issue: (a) Whole-file semgrep suppression means future traversal code in the growing intelliStory.js goes unchecked. (b) client.intelliStoryStats is lazily-created, never reset — accumulates across builds if a client is reused. (c) New test-node20 CI job (workflow change) — flagged for visibility; mirror logic looks consistent, no action requested.

  • Suggestion: Consider narrowing semgrep scope over time; reset stats in createBuild.


Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:966c05dReviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), findings then re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a new static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

The bail-first posture is the right architecture for a feature that can silently drop visual coverage, and it is applied thoroughly — 15+ explicit bail sites covering missing build id, unreadable/invalid stats, git failures, ambiguous or absent lockfiles, manifest changes spanning directories, browser-upgrade builds, and graph-job timeouts. All four Medium findings below are gaps in that otherwise-sound safety net, not architectural problems.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through the existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync is used without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassExtensive and largely correct bail coverage. Four Medium gaps: findings 1–4.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps: the three API calls are not (finding 1), and dropped stats modules are logged at debug only (finding 4).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedPassBail branches are covered individually (commit e071c952 restored 100% coverage). The glob semantics in finding 3 are asserted deliberately by intelliStory.test.js:286-289.
MediumTestingExisting tests still pass (no regressions)PassAll CI checks green on 966c05d: Lint, Build, Build & verify executable, Regression, every package suite (incl. the new Node 20 leg), CodeQL, Semgrep OSS.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover. Minor: findings 5–7.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer (refined by orchestrator verification)
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or DNS failure therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); it is only thrown exceptions that leak.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph, and in pollGraphStatus treat a throw as a non-done status.

2. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (verified)
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail, and filtering proceeds as though nothing global changed. This is the under-snapshot direction: real visual regressions ship unnoticed.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

3. Bare-extension bailOnChanges globs silently match nothing below the repo root

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex), consumed at :251
  • Severity: Medium
  • Reviewer: stack:code-reviewer (severity downgraded from High by the orchestrator — see below)
  • Issue: With { extended: true, globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$ — verified locally — so it matches only root-level files, never src/components/Button.css. A user who configures bailOnChanges: ['*.css'] expecting "bail on any stylesheet change" gets no bail, and IntelliStory filters as if nothing unsafe changed. untraced shares the semantics but fails safe (nothing is dropped, so more snapshots are taken).
    Downgraded from the sub-reviewer's High because this is standard glob behaviorminimatch, picomatch, and GitHub Actions paths: filters all treat a bare *.ext as root-only — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. Still worth flagging: bailOnChanges is the one user-facing safety valve and the failure mode is silent.
  • Suggestion: Either document that patterns are path-segment-scoped and **/*.css is required (there is currently no documentation for bailOnChanges/untraced anywhere in the repo), or auto-broaden bare-name patterns before compiling: const p = pattern.includes('/') ? pattern : '**/' + pattern;. If the current semantics stay, rename the test to state the intent ('a bare-extension pattern does not match nested paths') so the behavior reads as deliberate.

4. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run still proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate the existing message to log.warn, and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

5. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue:resolveAndIndex runs only when copy.type === 'src'. Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the external enrichment tool ever emits a third type carrying an absolute path — an asset, CSS, or JSON import — that raw absolute path ships unnormalized in the modules payload: it leaks the developer's home or CI runner path, and it cannot be correlated against the project-relative affectedNodes, which is a false-negative path.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe and type-agnostic.

6. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (correction: rename detection is git's default since 2.9, so the inconsistency is present out of the box, not only under custom config)
  • Issue:affectedNodes comes from git diff --name-only with rename detection active, which collapses a rename to the new path alone; getAffectedFileLocations runs the same diff with --no-renames, which yields a delete + add pair. The two views of one diff can disagree for renamed files. Low practical impact — the old path no longer exists to be imported — but it makes the two graph inputs non-comparable.
  • Suggestion: Add --no-renames to the gitDiffNames invocation for determinism and parity with :84.

7. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer (verified)
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. This is opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or a mis-selection — often inside a locked-down or air-gapped CI environment where the CDN is unreachable and the trace renders blank.
  • Suggestion: Vendor the (small) Drawflow bundle into the package and inline it into the template.

8. Whole-file .semgrepignore suppression for intelliStory.js

  • File:.semgrepignore:49-57
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: The suppression is file-level rather than scoped to the specific path.join/path.resolve sites that trigger the false positive. intelliStory.js does a fair amount of fs/path work, so any genuinely unsafe join added later goes unscanned. The stated rationale (inline // nosemgrep not honored by the CI semgrep version) is legitimate and well documented.
  • Suggestion: Track a follow-up to narrow the suppression once the CI semgrep version honors inline annotations.

9. No bail is logged at the throw site

  • File:packages/cli-command/src/intelliStory.js — all IntelliStoryBailError throw sites
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: Every bail constructs a carefully worded message but none logs at warn/info before throwing, so whether a user ever learns that filtering did not apply depends entirely on the catch site in @percy/storybook — outside this repo and, per the PR body, still a follow-up. If that catcher logs at debug or swallows while falling back, the fail-safe is invisible.
  • Suggestion:log.warn(message) at the throw site (or inside a small bail(message, log) helper) so visibility does not depend on the external consumer.

Verified non-issues

  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced anywhere in cli-command — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:01aa1f6Reviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), second independent pass; all findings re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

Note on this run. Head moved from 966c05d to 01aa1f6, but both new commits (01aa1f6, f6ef573) are Merge branch 'master' into PPLT-5844git diff 966c05d 01aa1f6 over the PR-owned paths is empty, so the reviewable code is byte-identical to the previously reviewed head. The fast-path did not fire because the intermediate merge f6ef573 carries no prior Claude Code Review status, so a full review ran. This second, independent pass surfaced finding 1, which the first pass missed. The code has not regressed — the assessment has sharpened, and the verdict changes accordingly.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync runs without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1: bailOnChanges/untraced globs are matched against repo-root-relative paths with no rebasing, while story importPaths in the same function are rebased from the invocation directory — so the user's safety valve is silently inert for monorepo/subdirectory invocation.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps are Medium, not High: the three API calls are unwrapped (finding 2), malformed globs are swallowed to false (finding 4), and dropped stats modules are logged at debug only (finding 5).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedFailBail branches are covered individually, but every fixture builds a git repo with files at its root — there is no cwd ≠ projectRoot case, which is exactly why finding 1 went unnoticed. No test simulates a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)PassAll CI checks were green on 966c05d, whose PR-owned tree is identical to this head.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. bailOnChanges/untraced globs are never rebased to the invocation directory — the safety valve is silently inert in a monorepo

  • File:packages/cli-command/src/intelliStory.js:250-252, :259-262 (compare :415-426)

  • Severity: High

  • Reviewer: stack:code-reviewer (second pass); mechanism verified by the orchestrator

  • Issue:affectedNodes comes from git diff --name-only, which always emits repo-root-relative paths regardless of cwd. assertNoBailOnChanges and enforceUntraced match user-supplied globs directly against those paths, and glob-to-regexp fully anchors the result (^…$). Meanwhile, twenty lines further down, the same function builds invocationDir = process.cwd() and rebases each story's importPath from the invocation directory to project-relative (:415-426) — so the file demonstrably does expect cwd ≠ projectRoot. User globs get no such treatment.

    Concrete failure: a monorepo user runs cd packages/ui && percy storybook ./storybook-static with bailOnChanges: ['webpack.config.js']. A change to that file lands in affectedNodes as packages/ui/webpack.config.js; the pattern compiles to /^webpack\.config\.js$/ and does not match. No bail fires, no warning is logged, and IntelliStory filters as though nothing risky changed — the under-snapshot direction, where a genuine visual regression ships unreviewed. Monorepo-with-subdirectory is the mainstream Storybook layout, and bailOnChanges exists precisely as the escape hatch for changes the graph can't reason about.

    Not covered by any test: every fixture in intelliStory.test.js constructs a repo with files at its root, so no cwd ≠ projectRoot case is exercised.

  • Suggestion: Rebase user patterns the same way story paths already are, before matching:

    constrebase=g=>{if(path.isAbsolute(g))returnpath.relative(projectRoot,g).split(path.sep).join('/');constabs=path.resolve(invocationDir,g);constrel=path.relative(projectRoot,abs);returnrel.startsWith('..') ? g : rel.split(path.sep).join('/');};

    and pass bailOnChanges.map(rebase) / untraced.map(rebase) into the two functions. Add a test with cwd set to a nested package directory. If root-relative globs are the intended contract instead, that must be documented and a bail should fire when cwd !== projectRoot and patterns are configured — silently doing nothing is the one outcome to avoid.

2. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer — independently reported by both review passes
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or connection reset therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); only thrown exceptions leak. diffLockfileDeps (:316) has the same shape: it remaps only SNYK_LOCKFILE_PARSER_UNAVAILABLE and re-throws anything else raw.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status. Add tests where each client method rejects.

3. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail. Same under-snapshot direction as finding 1, and the two compound for monorepo users.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

4. A malformed glob is silently treated as "matches nothing"

  • File:packages/cli-command/src/intelliStory.js:34-43 (matchesPattern)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (second pass); verified — glob-to-regexp('src/[') throws Unterminated character class, 'src/{a' throws Unterminated group
  • Issue:try { … } catch { return false } means a typo'd pattern in a user's config compiles-fails and is then treated as never matching. For untraced that fails safe (the file stays traced). For bailOnChanges it fails open: the guardrail is silently disabled with nothing logged anywhere. The over-length guard at :22 has the same shape and is explicitly tested as non-throwing (intelliStory.test.js:269-271).
  • Suggestion:log.warn when a pattern fails to compile — a broken guardrail must be visible:
    }catch(e){log.warn(`IntelliStory: ignoring invalid pattern "${pattern}": ${e.message}`);returnfalse;}
    Better still, validate the patterns once at config-parse time rather than per-path.

5. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator (first pass)
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate to log.warn and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

6. Bare-extension globs don't match nested paths

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: With { globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior — minimatch, picomatch, and GitHub Actions paths: filters agree — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. It is listed because it lands on the same safety valve as findings 1 and 4, and there is currently no documentation for bailOnChanges/untraced anywhere in the repo.
  • Suggestion: Document that patterns are path-segment-scoped and **/*.css is required. If you prefer forgiving semantics, auto-broaden bare-name patterns: const p = pattern.includes('/') ? pattern : '**/' + pattern; — note this must be decided together with finding 1's rebasing.

7. noRequireBinding guard is narrower than the crash class it protects

  • File:packages/cli-command/test/noRequireBinding.test.js:40
  • Severity: Low
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:FORBIDDEN = /\b(?:const|let|var)\s+require\s*=\s*createRequire\b/ only catches a single-line declaration. The same startup crash returns via import { createRequire as require } from 'module', const { default: require } = …, or a line-wrapped const require =\n createRequire(…) — none of which match.
  • Suggestion: Flag any binding of the bare identifier require (import specifier, declaration LHS, or parameter), not just the createRequire-RHS form.

8. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the enrichment tool ever emits a third type carrying an absolute path, that path ships unnormalized: it leaks a local or CI-runner path and cannot be correlated against project-relative affectedNodes.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe.

9. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Rename detection is git's default since 2.9, so affectedNodes collapses a rename to the new path alone while getAffectedFileLocations (run with --no-renames) sees a delete + add pair. The two views of one diff disagree for renamed files.
  • Suggestion: Add --no-renames to gitDiffNames for parity.

10. path.relative does not case-fold when computing project-relative module ids

  • File:packages/cli-command/src/intelliStory.js:117-128 (resolveAndIndex)
  • Severity: Low (mechanism demonstrated, real-world trigger unconfirmed)
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:path.relative('/Users/bob/proj', '/Users/Bob/Proj/src/App.js') returns '../../Bob/Proj/src/App.js' — exact segment comparison, no case folding, on every platform. On case-insensitive-but-preserving filesystems (APFS, NTFS), any casing divergence between the bundler's absolute module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that can never match a real repo-relative path. The reviewer could not force an actual divergence, so this is flagged as plausible rather than confirmed.
  • Suggestion: At minimum, warn when the computed relative path escapes projectRoot (starts with ..) rather than silently indexing it; optionally normalize both sides through fs.realpathSync.

11. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer — reported by both passes
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. Opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or mis-selection — often inside a locked-down or air-gapped CI runner where it renders blank.
  • Suggestion: Vendor the small Drawflow bundle and inline it into the template.

12. Minor: bail visibility, semgrep scope, stats-shape logging

  • Severity: Low
  • Reviewer: stack:code-reviewer (both passes)
  • Issue: (a) No bail logs at the throw site, so user visibility depends entirely on the @percy/storybook catch — outside this repo and still a follow-up per the PR body. (b) .semgrepignore:49-57 suppresses intelliStory.js file-wide rather than at the flagged joins, so future unsafe joins in this fs/path-heavy file go unscanned; the rationale is documented and the constraint (inline // nosemgrep unsupported by the CI version) is real. (c) readStats's stats.modules || [] (:161) makes a stats file with no modules key indistinguishable from a legitimately empty one.
  • Suggestion: (a) log.warn(message) inside a small bail(message, log) helper. (b) Track narrowing the suppression. (c) log.debug when the key is absent versus empty.

Informational — not counted as defects

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested at lockfileDiff.test.js:85): Storybook addons are typically devDependencies and are imported directly by preview.js/story files. A pure addon version bump with no source diff will not surface through this path. It is explicit and tested, so likely intentional (relying on the server-side module graph instead) — worth a sanity check with the team given how conservative this feature aims to be.
  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: FAIL

Comment threadpackages/cli-command/test/intelliStory.test.js Fixed
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:91bd770Reviewers:stack:code-reviewer (prior passes), fix verification by the orchestrator against source at this head

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (cjsRequire) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

This head resolves the High finding from the 01aa1f6 review along with three of the five Mediums. Verdict flips to PASS.

Fix verification (vs. 01aa1f6)

#FindingSevStatus at 91bd770
1bailOnChanges/untraced globs not rebased to the invocation dirHighFixed
2Client calls throw raw Error, not IntelliStoryBailErrorMediumOpen
3.storybook config dir hardcodedMediumFixed
4Malformed glob silently swallowed to "no match"MediumFixed
5Dropped stats modules logged at debug, no thresholdMediumFixed
6Bare-extension globs don't match nested pathsMediumOpen (by design; still undocumented)
7noRequireBinding regex narrower than the crash classLowOpen
8mapEntry normalizes source only for type === 'src'LowOpen
9gitDiffNames omits --no-renamesLowOpen
10path.relative does not case-foldLowOpen (was unconfirmed)
11trace.html loads Drawflow from a CDNLowOpen
12No log.warn at bail throw sitesLowPartially addressed

Finding 1 is fixed properly, not papered over. resolvePattern (intelliStory.js:88-100) rebases each user pattern from the invocation directory onto the repo root — the same basis normalizeImportPath already used for story paths, so the asymmetry that caused the bug is gone. Beyond that, the fix handles the two readings asymmetrically by direction of harm, which is the right call:

  • assertNoBailOnChanges (:394-425) bails when a pattern matches only under the repo-root reading, logging a warning with a concrete rewrite suggestion — a stale root-relative config still protects the user.
  • enforceUntraced (:433-462) declines to apply such a pattern and warns, since applying it would drop snapshots.

A <rootDir>/ prefix is an explicit opt-out, patterns resolving outside the repo bail rather than matching nothing, and resolveConfigDirs (:361-390) additionally checks that a configured directory actually exists — catching a wrong-basis configDir at config time instead of silently never matching. Findings 3, 4 and 5 are each fixed with matching care: isInsideDirs distinguishes single-segment (any depth) from multi-segment (prefix) config dirs so config/storybook can't match config/storybook-old; patternError validates globs up front and routes a compile failure to a bail for bailOnChanges and a warn-skip for untraced; and readStats now separates deliberately-excluded modules from unresolved ones, warns on the latter and bails past a 10% ratio of resolvable candidates — so a stats file heavy in node_modules no longer trips the threshold. intelliStory.test.js grows 364 lines with cwd ≠ projectRoot fixtures (projectRoot: '/repo', invocationDir: '/repo/packages/ui') and a resolveConfigDirs() suite — the gap that let finding 1 through is now covered.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth flows through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before spawnSync; statsFile narrowed to a bare .json basename; spawnSync runs without a shell. User globs are now validated up front via patternError. Dedicated security lens is disabled in this orchestrator.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassThe path-basis defect is fixed, with both readings handled by direction of harm. Remaining gaps are Medium/Low.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassGlob compile failures are no longer swallowed, and incomplete graphs now bail past a threshold. Finding 2 (unwrapped client calls) remains, at Medium.
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop; no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass+364 test lines this push, covering resolvePattern, resolveConfigDirs, and the <rootDir>/ opt-out.
MediumTestingError paths and edge cases testedPassThe cwd ≠ projectRoot gap is closed. Still untested: a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)Pass46 checks green, incl. Lint, Build & verify executable, Regression, all cli-command suites and the Node 20 leg. Test @percy/core still pending at review time — unrelated to the changed packages.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne GET, one POST, bounded poll (12 × 5s). In-memory stats parsing.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled.
MediumQualityFollows existing codebase patternsPassMatches PercyClient conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on and the CI leg that exercises the lockfile path.
LowQualityMeaningful names, no dead codePassstream-json confirmed absent. New helpers (resolvePattern, patternError, rootReadingHit, suggestRewrite) are well named and each has a stated reason to exist.
LowQualityComments explain why, not whatPassThe new comments are unusually good — they record the direction of harm reasoning behind each asymmetry ("recognising one directory too many only costs snapshots, while missing the real one loses comparisons"), which is exactly what a future maintainer needs.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp used; snyk-nodejs-lockfile-parser correctly optional with a Node 20 CI leg.

Findings

2. API/network failures still escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:292, :331, :555
  • Severity: Medium
  • Reviewer: stack:code-reviewer (reported independently by both prior passes); re-verified unfixed at this head
  • Issue: Verified still open: getStatus('intelli_story_graph', …) in pollGraphStatus (:292), getIntelliStorySnapshotNameToCommit (:331), and generateIntelliStoryGraph (:555) are all bare awaits with no surrounding try/catch. Every other failure mode in this file — git, fs, config, lockfile, malformed glob, degraded graph — is deliberately converted to IntelliStoryBailError so the caller can degrade to a full snapshot run. A transient 5xx, timeout, or connection reset from these three still propagates as a plain Error, so if @percy/storybook catches IntelliStoryBailError specifically, an API blip hard-fails the storybook command instead of falling back. The graph-job timeout path is handled correctly (:561); only thrown exceptions leak. Not a merge blocker under the verdict rule, but it is now the last inconsistency in an otherwise uniformly fail-safe module.
  • Suggestion:
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status so the existing :561 bail covers it. Add tests where each client method rejects.

6. Bare-extension globs still don't match nested paths, and the contract is undocumented

  • File:packages/cli-command/src/intelliStory.js:28
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass)
  • Issue:glob-to-regexp('*.css', { globstar: true }) compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior (minimatch, picomatch, GitHub Actions paths: all agree) and is deliberately asserted by the tests, so it is not a defect. It is still listed because the new rootReadingHit warning covers the basis mistake but not the depth mistake — a user who writes *.css gets no warning at all — and there remains no documentation for bailOnChanges/untraced/configDir anywhere in the repo.
  • Suggestion: Document the semantics now that the basis rules are non-trivial (invocation-relative, <rootDir>/ opt-out, path-segment-scoped globs). Optionally extend the existing warning: if a pattern has no / and no **, and some affected node matches **/<pattern>, warn the same way the root-reading case does.

7–11. Carried-over Lows, unchanged at this head

  • noRequireBinding.test.js:40FORBIDDEN still only matches a single-line const/let/var require = createRequire. The same crash returns via import { createRequire as require } from 'module' or a line-wrapped declaration. Broaden to flag any binding of the bare require identifier.
  • intelliStory.js:229mapEntry still gates normalization on copy.type === 'src'. Drop the gate; resolveAndIndex already passes non-absolute values through untouched.
  • intelliStory.js:138gitDiffNames still omits --no-renames while getAffectedFileLocations uses it, so the two views of one diff disagree for renamed files.
  • intelliStory.jsresolveAndIndexpath.relative does not case-fold, so a casing divergence between bundler module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that never match. Mechanism is real, real-world trigger still unconfirmed. Cheapest guard: warn when the computed relative path starts with ...
  • graphTraceTemplate.html:8, :12trace.html still loads Drawflow from cdn.jsdelivr.net, so the diagnostic renders blank in an air-gapped or locked-down CI runner. Vendor the bundle.

12. Bail visibility — partially addressed

  • Severity: Low
  • Issue: This push adds ten log.warn calls, so pattern and configDir problems are now visible. The IntelliStoryBailError throw sites themselves still don't log before throwing, so whether a user learns that filtering did not apply continues to depend on the @percy/storybook catch site — outside this repo and still a follow-up per the PR body.
  • Suggestion: A small bail(message, log) helper that warns and throws.

Informational

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested): Storybook addons are usually devDependencies imported directly by preview.js/story files, so a pure addon bump with no source diff won't surface here. Explicit and tested, so presumably intentional — worth a sanity check with the team.
  • .semgrepignore still suppresses intelliStory.js file-wide rather than at the flagged joins. The file grew ~290 lines this push, which widens the unscanned surface; the documented rationale (inline // nosemgrep unsupported by the CI semgrep version) still holds. Worth narrowing when tooling allows.
  • Version strings moved 1.32.6-beta.31.32.6 via the master release merge; no functional change.

Verdict: PASS

@RaghavsBrowserStack
RaghavsBrowserStack merged commit 8311242 into masterAug 10, 2026
48 checks passed
@RaghavsBrowserStack
RaghavsBrowserStack deleted the PPLT-5844 branch August 10, 2026 12:57
RaghavsBrowserStack added a commit that referenced this pull request Aug 11, 2026
* bump version
* remove stray archive test fixtures committed by mistake
packages/core/.test-archive-mixed and .test-archive-symlink were leftovers
from a local unit-test run that got picked up by the version-bump commit.
.test-archive-symlink/linked.json is an absolute symlink into a developer's
home directory, so it dangles on a fresh CI checkout and makes
`babel packages -d build` (build_cjs) fail with ENOENT, breaking the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix unwanted commits
* remove stray archive test fixtures reintroduced by the master merge
d302b09 deleted these on this branch, but master picked up the same
accidental add in d663543 (bump version, #2377), so merging master back in
(2405903) restored them and left PR #2378 with an empty diff.
packages/core/.test-archive-symlink/linked.json is an absolute symlink into a
developer's home directory. It dangles on a fresh CI checkout, so
`babel packages -d build` (build_cjs) fails with ENOENT and takes down the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* regenerate yarn.lock so `lerna publish` sees a clean tree
The lockfile block added in 8311242 (IntelliStory, #2339) was edited by hand
rather than regenerated, leaving it inconsistent with the dependency graph:
- @types/node@* had no entry, though @types/cacheable-request, @types/keyv,
@types/responselike and @types/yauzl all require it
- get-stream@^5.1.0 had no entry, though cacheable-request@^7.0.2 requires it
- @types/yauzl@^2.9.1 was orphaned — nothing referenced it
So `yarn` on a clean CI checkout resolved the two missing descriptors, pruned
the orphan and rewrote the file. That left the working tree dirty, and
`lerna publish from-package` aborted with EUNCOMMIT (M yarn.lock), failing the
Release job.
Regenerated with a plain `yarn install`. `yarn install --frozen-lockfile` is
now a no-op, so the release checkout stays clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@RaghavsBrowserStack@prklm10@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
RaghavsBrowserStack merged 29 commits into
masterfrom
PPLT-5844
Aug 10, 2026
Merged

PPLT-5844: add IntelliStory storybook affected-story filtering#2339
RaghavsBrowserStack merged 29 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).
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/test/graphTrace.test.js Fixed
…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.
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
@RaghavsBrowserStackRaghavsBrowserStack changed the title feat(cli-command,client): add IntelliStory storybook affected-story filteringPPLT-5844: add IntelliStory storybook affected-story filteringJul 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 ownerJuly 15, 2026 14:29
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
RaghavsBrowserStackand others added 3 commits July 21, 2026 13:20
Add tests for the graceful-bail paths added in "apply suggested fixed":
- malformed stats-file JSON (validateAndReadStats)
- unreadable current lockfile (getAffectedPackages)
- unreadable package.json (getAffectedPackages)
These run on Node 14 (they bail before the Node>=18 snyk path), fixing the
lines/statements coverage-threshold failure on the Node 14 test job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GLOB_CHARS only detected * and ? so brace ({a,b}) and bracket ([abc])
patterns fell through to exact-match, silently disabling the intended
glob semantics for these documented config options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:001c036Reviewers: stack:code-reviewer

Summary

Adds IntelliStory — an affected-story graph feature for @percy/storybook: parses Storybook build stats, diffs against a git baseline (incl. lockfile/dependency diffing via optional snyk-nodejs-lockfile-parser), enqueues a server-side affected-story graph job, tags snapshots for selection, and optionally renders a debug trace.html. Plumbs intelliStory/storybookPath through @percy/client and @percy/core, and adds a Node 20 CI leg for the Node-18+-only snyk path.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed.
HighSecurityInput validation and sanitizationPassassertSafeRef blocks flag-injection; safeJson escapes HTML/JS breakouts; paths anchored via basename.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource-ownership surface.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1 ($-pattern .replace) still open. Finding 2 (glob detection gap) resolved in 001c036.
HighCorrectnessError handling is explicit, no swallowed exceptionsPass*Mostly graceful IntelliStoryBailError; one inconsistent rethrow — Finding 3.
HighCorrectnessNo race conditions or concurrency issuesPassNo new concurrency.
MediumTestingNew code has corresponding testsPassBroad coverage across new modules.
MediumTestingError paths and edge cases testedPass*Package-affected merge branch istanbul ignored rather than tested — Finding 5.
MediumTestingExisting tests still pass (no regressions)PassNot re-run here; CI covers.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo queries.
MediumPerformanceLong-running tasks use background jobsPassGraph job is server-side/async with timeout bail.
MediumQualityFollows existing codebase patternsPassConsistent with client/core plumbing conventions.
MediumQualityChanges are focused (single concern)PassScoped to IntelliStory feature.
LowQualityMeaningful names, no dead codePassClear naming.
LowQualityComments explain why, not whatPass.semgrepignore rationale well-documented.
LowQualityNo unnecessary dependencies addedPasssnyk-nodejs-lockfile-parser is optional/lazy-loaded.

Findings

  • File:packages/cli-command/src/graphTrace.js:114-116

  • Severity: Medium

  • Reviewer: stack:code-reviewer (verified)

  • Issue:.replace('__VERTICES_JSON__', safeJson(...)) uses plain-string replacement values. JS honors $$, $&, $`, $', $n special patterns in the replacement string even when the search arg is a literal. Embedded JSON contains arbitrary file paths; a path with $' re-injects the rest of the template, breaking the embedded JS. safeJson's escaping doesn't cover this — the corruption happens in the .replace call itself.

  • Suggestion: Pass a function as the replacement: .replace('__VERTICES_JSON__', () => safeJson(...)). Add a graphTrace.test.js case with $&/$'/$$ in a vertex name.

  • File:packages/cli-command/src/intelliStory.js:16, 34-41

  • Severity: Medium — ✅ RESOLVED in 001c036

  • Reviewer: stack:code-reviewer (verified)

  • Issue:GLOB_CHARS = /[*?]/ only routed */? patterns through the glob matcher, but patternToRegex enables {extended:true, globstar:true} (brace/bracket support). A configured bailOnChanges: ['src/{a,b}.js'] or bracket pattern silently fell through to literal str === pattern and never matched — silently disabling a documented config option (bailOnChanges/untraced).

  • Fix applied: Broadened detection to const GLOB_CHARS = /[*?{}[\]]/;. Added brace- and bracket-glob tests to assertNoBailOnChanges() and enforceUntraced() — verified failing under the old regex and passing under the fix (full 156-spec suite green).

  • File:packages/cli-command/src/intelliStory.js:326-336

  • Severity: Medium

  • Reviewer: stack:code-reviewer

  • Issue: Only SNYK_LOCKFILE_PARSER_UNAVAILABLE is converted to a graceful IntelliStoryBailError; a buildDepTree parse failure (malformed/older-ref lockfile) rethrows as a raw Error. Every other failure path degrades to "running full snapshot set."

  • Suggestion: Confirm the @percy/storybook caller treats allapplyIntelliStory throws as fall-back-to-full-set. If not, wrap this rethrow in IntelliStoryBailError for consistency.

  • File:packages/cli-command/src/intelliStory.js:67-70

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue:assertSafeRef regex correctly blocks leading -, but also rejects ordinary safe ref syntax like HEAD~5 / abc123^2.

  • Suggestion: Confirm intentional, or widen charset to include ~^ (still safe given the leading-char restriction).

  • File:packages/cli-command/src/intelliStory.js:438-440

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The packageAffectedNodes merge into affectedNodes (the actual mechanism folding lockfile-driven dep changes into the graph) is /* istanbul ignore next */ rather than covered by a real test.

  • Suggestion: Add an itPosix-style Node≥18 integration test exercising the merge end-to-end.

  • File:packages/cli-command/src/intelliStory.js (resolveAndIndex)

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Absolute module ids resolving outside projectRoot (symlinked pkg, monorepo sibling) produce ../-prefixed paths added straight into the files index without a guard.

  • Suggestion: Bail or drop entries where rel.startsWith('..').

  • File:.semgrepignore:118-131 / packages/client/src/client.js:2591-2592 / .github/workflows/test.yml

  • Severity: Info

  • Reviewer: stack:code-reviewer

  • Issue: (a) Whole-file semgrep suppression means future traversal code in the growing intelliStory.js goes unchecked. (b) client.intelliStoryStats is lazily-created, never reset — accumulates across builds if a client is reused. (c) New test-node20 CI job (workflow change) — flagged for visibility; mirror logic looks consistent, no action requested.

  • Suggestion: Consider narrowing semgrep scope over time; reset stats in createBuild.


Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:966c05dReviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), findings then re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a new static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

The bail-first posture is the right architecture for a feature that can silently drop visual coverage, and it is applied thoroughly — 15+ explicit bail sites covering missing build id, unreadable/invalid stats, git failures, ambiguous or absent lockfiles, manifest changes spanning directories, browser-upgrade builds, and graph-job timeouts. All four Medium findings below are gaps in that otherwise-sound safety net, not architectural problems.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through the existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync is used without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassExtensive and largely correct bail coverage. Four Medium gaps: findings 1–4.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps: the three API calls are not (finding 1), and dropped stats modules are logged at debug only (finding 4).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedPassBail branches are covered individually (commit e071c952 restored 100% coverage). The glob semantics in finding 3 are asserted deliberately by intelliStory.test.js:286-289.
MediumTestingExisting tests still pass (no regressions)PassAll CI checks green on 966c05d: Lint, Build, Build & verify executable, Regression, every package suite (incl. the new Node 20 leg), CodeQL, Semgrep OSS.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover. Minor: findings 5–7.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer (refined by orchestrator verification)
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or DNS failure therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); it is only thrown exceptions that leak.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph, and in pollGraphStatus treat a throw as a non-done status.

2. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (verified)
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail, and filtering proceeds as though nothing global changed. This is the under-snapshot direction: real visual regressions ship unnoticed.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

3. Bare-extension bailOnChanges globs silently match nothing below the repo root

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex), consumed at :251
  • Severity: Medium
  • Reviewer: stack:code-reviewer (severity downgraded from High by the orchestrator — see below)
  • Issue: With { extended: true, globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$ — verified locally — so it matches only root-level files, never src/components/Button.css. A user who configures bailOnChanges: ['*.css'] expecting "bail on any stylesheet change" gets no bail, and IntelliStory filters as if nothing unsafe changed. untraced shares the semantics but fails safe (nothing is dropped, so more snapshots are taken).
    Downgraded from the sub-reviewer's High because this is standard glob behaviorminimatch, picomatch, and GitHub Actions paths: filters all treat a bare *.ext as root-only — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. Still worth flagging: bailOnChanges is the one user-facing safety valve and the failure mode is silent.
  • Suggestion: Either document that patterns are path-segment-scoped and **/*.css is required (there is currently no documentation for bailOnChanges/untraced anywhere in the repo), or auto-broaden bare-name patterns before compiling: const p = pattern.includes('/') ? pattern : '**/' + pattern;. If the current semantics stay, rename the test to state the intent ('a bare-extension pattern does not match nested paths') so the behavior reads as deliberate.

4. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run still proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate the existing message to log.warn, and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

5. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue:resolveAndIndex runs only when copy.type === 'src'. Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the external enrichment tool ever emits a third type carrying an absolute path — an asset, CSS, or JSON import — that raw absolute path ships unnormalized in the modules payload: it leaks the developer's home or CI runner path, and it cannot be correlated against the project-relative affectedNodes, which is a false-negative path.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe and type-agnostic.

6. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (correction: rename detection is git's default since 2.9, so the inconsistency is present out of the box, not only under custom config)
  • Issue:affectedNodes comes from git diff --name-only with rename detection active, which collapses a rename to the new path alone; getAffectedFileLocations runs the same diff with --no-renames, which yields a delete + add pair. The two views of one diff can disagree for renamed files. Low practical impact — the old path no longer exists to be imported — but it makes the two graph inputs non-comparable.
  • Suggestion: Add --no-renames to the gitDiffNames invocation for determinism and parity with :84.

7. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer (verified)
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. This is opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or a mis-selection — often inside a locked-down or air-gapped CI environment where the CDN is unreachable and the trace renders blank.
  • Suggestion: Vendor the (small) Drawflow bundle into the package and inline it into the template.

8. Whole-file .semgrepignore suppression for intelliStory.js

  • File:.semgrepignore:49-57
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: The suppression is file-level rather than scoped to the specific path.join/path.resolve sites that trigger the false positive. intelliStory.js does a fair amount of fs/path work, so any genuinely unsafe join added later goes unscanned. The stated rationale (inline // nosemgrep not honored by the CI semgrep version) is legitimate and well documented.
  • Suggestion: Track a follow-up to narrow the suppression once the CI semgrep version honors inline annotations.

9. No bail is logged at the throw site

  • File:packages/cli-command/src/intelliStory.js — all IntelliStoryBailError throw sites
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: Every bail constructs a carefully worded message but none logs at warn/info before throwing, so whether a user ever learns that filtering did not apply depends entirely on the catch site in @percy/storybook — outside this repo and, per the PR body, still a follow-up. If that catcher logs at debug or swallows while falling back, the fail-safe is invisible.
  • Suggestion:log.warn(message) at the throw site (or inside a small bail(message, log) helper) so visibility does not depend on the external consumer.

Verified non-issues

  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced anywhere in cli-command — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:01aa1f6Reviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), second independent pass; all findings re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

Note on this run. Head moved from 966c05d to 01aa1f6, but both new commits (01aa1f6, f6ef573) are Merge branch 'master' into PPLT-5844git diff 966c05d 01aa1f6 over the PR-owned paths is empty, so the reviewable code is byte-identical to the previously reviewed head. The fast-path did not fire because the intermediate merge f6ef573 carries no prior Claude Code Review status, so a full review ran. This second, independent pass surfaced finding 1, which the first pass missed. The code has not regressed — the assessment has sharpened, and the verdict changes accordingly.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync runs without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1: bailOnChanges/untraced globs are matched against repo-root-relative paths with no rebasing, while story importPaths in the same function are rebased from the invocation directory — so the user's safety valve is silently inert for monorepo/subdirectory invocation.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps are Medium, not High: the three API calls are unwrapped (finding 2), malformed globs are swallowed to false (finding 4), and dropped stats modules are logged at debug only (finding 5).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedFailBail branches are covered individually, but every fixture builds a git repo with files at its root — there is no cwd ≠ projectRoot case, which is exactly why finding 1 went unnoticed. No test simulates a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)PassAll CI checks were green on 966c05d, whose PR-owned tree is identical to this head.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. bailOnChanges/untraced globs are never rebased to the invocation directory — the safety valve is silently inert in a monorepo

  • File:packages/cli-command/src/intelliStory.js:250-252, :259-262 (compare :415-426)

  • Severity: High

  • Reviewer: stack:code-reviewer (second pass); mechanism verified by the orchestrator

  • Issue:affectedNodes comes from git diff --name-only, which always emits repo-root-relative paths regardless of cwd. assertNoBailOnChanges and enforceUntraced match user-supplied globs directly against those paths, and glob-to-regexp fully anchors the result (^…$). Meanwhile, twenty lines further down, the same function builds invocationDir = process.cwd() and rebases each story's importPath from the invocation directory to project-relative (:415-426) — so the file demonstrably does expect cwd ≠ projectRoot. User globs get no such treatment.

    Concrete failure: a monorepo user runs cd packages/ui && percy storybook ./storybook-static with bailOnChanges: ['webpack.config.js']. A change to that file lands in affectedNodes as packages/ui/webpack.config.js; the pattern compiles to /^webpack\.config\.js$/ and does not match. No bail fires, no warning is logged, and IntelliStory filters as though nothing risky changed — the under-snapshot direction, where a genuine visual regression ships unreviewed. Monorepo-with-subdirectory is the mainstream Storybook layout, and bailOnChanges exists precisely as the escape hatch for changes the graph can't reason about.

    Not covered by any test: every fixture in intelliStory.test.js constructs a repo with files at its root, so no cwd ≠ projectRoot case is exercised.

  • Suggestion: Rebase user patterns the same way story paths already are, before matching:

    constrebase=g=>{if(path.isAbsolute(g))returnpath.relative(projectRoot,g).split(path.sep).join('/');constabs=path.resolve(invocationDir,g);constrel=path.relative(projectRoot,abs);returnrel.startsWith('..') ? g : rel.split(path.sep).join('/');};

    and pass bailOnChanges.map(rebase) / untraced.map(rebase) into the two functions. Add a test with cwd set to a nested package directory. If root-relative globs are the intended contract instead, that must be documented and a bail should fire when cwd !== projectRoot and patterns are configured — silently doing nothing is the one outcome to avoid.

2. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer — independently reported by both review passes
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or connection reset therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); only thrown exceptions leak. diffLockfileDeps (:316) has the same shape: it remaps only SNYK_LOCKFILE_PARSER_UNAVAILABLE and re-throws anything else raw.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status. Add tests where each client method rejects.

3. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail. Same under-snapshot direction as finding 1, and the two compound for monorepo users.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

4. A malformed glob is silently treated as "matches nothing"

  • File:packages/cli-command/src/intelliStory.js:34-43 (matchesPattern)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (second pass); verified — glob-to-regexp('src/[') throws Unterminated character class, 'src/{a' throws Unterminated group
  • Issue:try { … } catch { return false } means a typo'd pattern in a user's config compiles-fails and is then treated as never matching. For untraced that fails safe (the file stays traced). For bailOnChanges it fails open: the guardrail is silently disabled with nothing logged anywhere. The over-length guard at :22 has the same shape and is explicitly tested as non-throwing (intelliStory.test.js:269-271).
  • Suggestion:log.warn when a pattern fails to compile — a broken guardrail must be visible:
    }catch(e){log.warn(`IntelliStory: ignoring invalid pattern "${pattern}": ${e.message}`);returnfalse;}
    Better still, validate the patterns once at config-parse time rather than per-path.

5. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator (first pass)
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate to log.warn and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

6. Bare-extension globs don't match nested paths

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: With { globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior — minimatch, picomatch, and GitHub Actions paths: filters agree — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. It is listed because it lands on the same safety valve as findings 1 and 4, and there is currently no documentation for bailOnChanges/untraced anywhere in the repo.
  • Suggestion: Document that patterns are path-segment-scoped and **/*.css is required. If you prefer forgiving semantics, auto-broaden bare-name patterns: const p = pattern.includes('/') ? pattern : '**/' + pattern; — note this must be decided together with finding 1's rebasing.

7. noRequireBinding guard is narrower than the crash class it protects

  • File:packages/cli-command/test/noRequireBinding.test.js:40
  • Severity: Low
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:FORBIDDEN = /\b(?:const|let|var)\s+require\s*=\s*createRequire\b/ only catches a single-line declaration. The same startup crash returns via import { createRequire as require } from 'module', const { default: require } = …, or a line-wrapped const require =\n createRequire(…) — none of which match.
  • Suggestion: Flag any binding of the bare identifier require (import specifier, declaration LHS, or parameter), not just the createRequire-RHS form.

8. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the enrichment tool ever emits a third type carrying an absolute path, that path ships unnormalized: it leaks a local or CI-runner path and cannot be correlated against project-relative affectedNodes.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe.

9. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Rename detection is git's default since 2.9, so affectedNodes collapses a rename to the new path alone while getAffectedFileLocations (run with --no-renames) sees a delete + add pair. The two views of one diff disagree for renamed files.
  • Suggestion: Add --no-renames to gitDiffNames for parity.

10. path.relative does not case-fold when computing project-relative module ids

  • File:packages/cli-command/src/intelliStory.js:117-128 (resolveAndIndex)
  • Severity: Low (mechanism demonstrated, real-world trigger unconfirmed)
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:path.relative('/Users/bob/proj', '/Users/Bob/Proj/src/App.js') returns '../../Bob/Proj/src/App.js' — exact segment comparison, no case folding, on every platform. On case-insensitive-but-preserving filesystems (APFS, NTFS), any casing divergence between the bundler's absolute module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that can never match a real repo-relative path. The reviewer could not force an actual divergence, so this is flagged as plausible rather than confirmed.
  • Suggestion: At minimum, warn when the computed relative path escapes projectRoot (starts with ..) rather than silently indexing it; optionally normalize both sides through fs.realpathSync.

11. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer — reported by both passes
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. Opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or mis-selection — often inside a locked-down or air-gapped CI runner where it renders blank.
  • Suggestion: Vendor the small Drawflow bundle and inline it into the template.

12. Minor: bail visibility, semgrep scope, stats-shape logging

  • Severity: Low
  • Reviewer: stack:code-reviewer (both passes)
  • Issue: (a) No bail logs at the throw site, so user visibility depends entirely on the @percy/storybook catch — outside this repo and still a follow-up per the PR body. (b) .semgrepignore:49-57 suppresses intelliStory.js file-wide rather than at the flagged joins, so future unsafe joins in this fs/path-heavy file go unscanned; the rationale is documented and the constraint (inline // nosemgrep unsupported by the CI version) is real. (c) readStats's stats.modules || [] (:161) makes a stats file with no modules key indistinguishable from a legitimately empty one.
  • Suggestion: (a) log.warn(message) inside a small bail(message, log) helper. (b) Track narrowing the suppression. (c) log.debug when the key is absent versus empty.

Informational — not counted as defects

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested at lockfileDiff.test.js:85): Storybook addons are typically devDependencies and are imported directly by preview.js/story files. A pure addon version bump with no source diff will not surface through this path. It is explicit and tested, so likely intentional (relying on the server-side module graph instead) — worth a sanity check with the team given how conservative this feature aims to be.
  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: FAIL

Comment threadpackages/cli-command/test/intelliStory.test.js Fixed
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:91bd770Reviewers:stack:code-reviewer (prior passes), fix verification by the orchestrator against source at this head

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (cjsRequire) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

This head resolves the High finding from the 01aa1f6 review along with three of the five Mediums. Verdict flips to PASS.

Fix verification (vs. 01aa1f6)

#FindingSevStatus at 91bd770
1bailOnChanges/untraced globs not rebased to the invocation dirHighFixed
2Client calls throw raw Error, not IntelliStoryBailErrorMediumOpen
3.storybook config dir hardcodedMediumFixed
4Malformed glob silently swallowed to "no match"MediumFixed
5Dropped stats modules logged at debug, no thresholdMediumFixed
6Bare-extension globs don't match nested pathsMediumOpen (by design; still undocumented)
7noRequireBinding regex narrower than the crash classLowOpen
8mapEntry normalizes source only for type === 'src'LowOpen
9gitDiffNames omits --no-renamesLowOpen
10path.relative does not case-foldLowOpen (was unconfirmed)
11trace.html loads Drawflow from a CDNLowOpen
12No log.warn at bail throw sitesLowPartially addressed

Finding 1 is fixed properly, not papered over. resolvePattern (intelliStory.js:88-100) rebases each user pattern from the invocation directory onto the repo root — the same basis normalizeImportPath already used for story paths, so the asymmetry that caused the bug is gone. Beyond that, the fix handles the two readings asymmetrically by direction of harm, which is the right call:

  • assertNoBailOnChanges (:394-425) bails when a pattern matches only under the repo-root reading, logging a warning with a concrete rewrite suggestion — a stale root-relative config still protects the user.
  • enforceUntraced (:433-462) declines to apply such a pattern and warns, since applying it would drop snapshots.

A <rootDir>/ prefix is an explicit opt-out, patterns resolving outside the repo bail rather than matching nothing, and resolveConfigDirs (:361-390) additionally checks that a configured directory actually exists — catching a wrong-basis configDir at config time instead of silently never matching. Findings 3, 4 and 5 are each fixed with matching care: isInsideDirs distinguishes single-segment (any depth) from multi-segment (prefix) config dirs so config/storybook can't match config/storybook-old; patternError validates globs up front and routes a compile failure to a bail for bailOnChanges and a warn-skip for untraced; and readStats now separates deliberately-excluded modules from unresolved ones, warns on the latter and bails past a 10% ratio of resolvable candidates — so a stats file heavy in node_modules no longer trips the threshold. intelliStory.test.js grows 364 lines with cwd ≠ projectRoot fixtures (projectRoot: '/repo', invocationDir: '/repo/packages/ui') and a resolveConfigDirs() suite — the gap that let finding 1 through is now covered.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth flows through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before spawnSync; statsFile narrowed to a bare .json basename; spawnSync runs without a shell. User globs are now validated up front via patternError. Dedicated security lens is disabled in this orchestrator.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassThe path-basis defect is fixed, with both readings handled by direction of harm. Remaining gaps are Medium/Low.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassGlob compile failures are no longer swallowed, and incomplete graphs now bail past a threshold. Finding 2 (unwrapped client calls) remains, at Medium.
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop; no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass+364 test lines this push, covering resolvePattern, resolveConfigDirs, and the <rootDir>/ opt-out.
MediumTestingError paths and edge cases testedPassThe cwd ≠ projectRoot gap is closed. Still untested: a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)Pass46 checks green, incl. Lint, Build & verify executable, Regression, all cli-command suites and the Node 20 leg. Test @percy/core still pending at review time — unrelated to the changed packages.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne GET, one POST, bounded poll (12 × 5s). In-memory stats parsing.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled.
MediumQualityFollows existing codebase patternsPassMatches PercyClient conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on and the CI leg that exercises the lockfile path.
LowQualityMeaningful names, no dead codePassstream-json confirmed absent. New helpers (resolvePattern, patternError, rootReadingHit, suggestRewrite) are well named and each has a stated reason to exist.
LowQualityComments explain why, not whatPassThe new comments are unusually good — they record the direction of harm reasoning behind each asymmetry ("recognising one directory too many only costs snapshots, while missing the real one loses comparisons"), which is exactly what a future maintainer needs.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp used; snyk-nodejs-lockfile-parser correctly optional with a Node 20 CI leg.

Findings

2. API/network failures still escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:292, :331, :555
  • Severity: Medium
  • Reviewer: stack:code-reviewer (reported independently by both prior passes); re-verified unfixed at this head
  • Issue: Verified still open: getStatus('intelli_story_graph', …) in pollGraphStatus (:292), getIntelliStorySnapshotNameToCommit (:331), and generateIntelliStoryGraph (:555) are all bare awaits with no surrounding try/catch. Every other failure mode in this file — git, fs, config, lockfile, malformed glob, degraded graph — is deliberately converted to IntelliStoryBailError so the caller can degrade to a full snapshot run. A transient 5xx, timeout, or connection reset from these three still propagates as a plain Error, so if @percy/storybook catches IntelliStoryBailError specifically, an API blip hard-fails the storybook command instead of falling back. The graph-job timeout path is handled correctly (:561); only thrown exceptions leak. Not a merge blocker under the verdict rule, but it is now the last inconsistency in an otherwise uniformly fail-safe module.
  • Suggestion:
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status so the existing :561 bail covers it. Add tests where each client method rejects.

6. Bare-extension globs still don't match nested paths, and the contract is undocumented

  • File:packages/cli-command/src/intelliStory.js:28
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass)
  • Issue:glob-to-regexp('*.css', { globstar: true }) compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior (minimatch, picomatch, GitHub Actions paths: all agree) and is deliberately asserted by the tests, so it is not a defect. It is still listed because the new rootReadingHit warning covers the basis mistake but not the depth mistake — a user who writes *.css gets no warning at all — and there remains no documentation for bailOnChanges/untraced/configDir anywhere in the repo.
  • Suggestion: Document the semantics now that the basis rules are non-trivial (invocation-relative, <rootDir>/ opt-out, path-segment-scoped globs). Optionally extend the existing warning: if a pattern has no / and no **, and some affected node matches **/<pattern>, warn the same way the root-reading case does.

7–11. Carried-over Lows, unchanged at this head

  • noRequireBinding.test.js:40FORBIDDEN still only matches a single-line const/let/var require = createRequire. The same crash returns via import { createRequire as require } from 'module' or a line-wrapped declaration. Broaden to flag any binding of the bare require identifier.
  • intelliStory.js:229mapEntry still gates normalization on copy.type === 'src'. Drop the gate; resolveAndIndex already passes non-absolute values through untouched.
  • intelliStory.js:138gitDiffNames still omits --no-renames while getAffectedFileLocations uses it, so the two views of one diff disagree for renamed files.
  • intelliStory.jsresolveAndIndexpath.relative does not case-fold, so a casing divergence between bundler module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that never match. Mechanism is real, real-world trigger still unconfirmed. Cheapest guard: warn when the computed relative path starts with ...
  • graphTraceTemplate.html:8, :12trace.html still loads Drawflow from cdn.jsdelivr.net, so the diagnostic renders blank in an air-gapped or locked-down CI runner. Vendor the bundle.

12. Bail visibility — partially addressed

  • Severity: Low
  • Issue: This push adds ten log.warn calls, so pattern and configDir problems are now visible. The IntelliStoryBailError throw sites themselves still don't log before throwing, so whether a user learns that filtering did not apply continues to depend on the @percy/storybook catch site — outside this repo and still a follow-up per the PR body.
  • Suggestion: A small bail(message, log) helper that warns and throws.

Informational

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested): Storybook addons are usually devDependencies imported directly by preview.js/story files, so a pure addon bump with no source diff won't surface here. Explicit and tested, so presumably intentional — worth a sanity check with the team.
  • .semgrepignore still suppresses intelliStory.js file-wide rather than at the flagged joins. The file grew ~290 lines this push, which widens the unscanned surface; the documented rationale (inline // nosemgrep unsupported by the CI semgrep version) still holds. Worth narrowing when tooling allows.
  • Version strings moved 1.32.6-beta.31.32.6 via the master release merge; no functional change.

Verdict: PASS

@RaghavsBrowserStack
RaghavsBrowserStack merged commit 8311242 into masterAug 10, 2026
48 checks passed
@RaghavsBrowserStack
RaghavsBrowserStack deleted the PPLT-5844 branch August 10, 2026 12:57
RaghavsBrowserStack added a commit that referenced this pull request Aug 11, 2026
* bump version
* remove stray archive test fixtures committed by mistake
packages/core/.test-archive-mixed and .test-archive-symlink were leftovers
from a local unit-test run that got picked up by the version-bump commit.
.test-archive-symlink/linked.json is an absolute symlink into a developer's
home directory, so it dangles on a fresh CI checkout and makes
`babel packages -d build` (build_cjs) fail with ENOENT, breaking the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix unwanted commits
* remove stray archive test fixtures reintroduced by the master merge
d302b09 deleted these on this branch, but master picked up the same
accidental add in d663543 (bump version, #2377), so merging master back in
(2405903) restored them and left PR #2378 with an empty diff.
packages/core/.test-archive-symlink/linked.json is an absolute symlink into a
developer's home directory. It dangles on a fresh CI checkout, so
`babel packages -d build` (build_cjs) fails with ENOENT and takes down the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* regenerate yarn.lock so `lerna publish` sees a clean tree
The lockfile block added in 8311242 (IntelliStory, #2339) was edited by hand
rather than regenerated, leaving it inconsistent with the dependency graph:
- @types/node@* had no entry, though @types/cacheable-request, @types/keyv,
@types/responselike and @types/yauzl all require it
- get-stream@^5.1.0 had no entry, though cacheable-request@^7.0.2 requires it
- @types/yauzl@^2.9.1 was orphaned — nothing referenced it
So `yarn` on a clean CI checkout resolved the two missing descriptors, pruned
the orphan and rewrote the file. That left the working tree dirty, and
`lerna publish from-package` aborted with EUNCOMMIT (M yarn.lock), failing the
Release job.
Regenerated with a plain `yarn install`. `yarn install --frozen-lockfile` is
now a no-op, so the release checkout stays clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@RaghavsBrowserStack@prklm10@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
RaghavsBrowserStack merged 29 commits into
masterfrom
PPLT-5844
Aug 10, 2026
Merged

PPLT-5844: add IntelliStory storybook affected-story filtering#2339
RaghavsBrowserStack merged 29 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).
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/test/graphTrace.test.js Fixed
…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.
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
@RaghavsBrowserStackRaghavsBrowserStack changed the title feat(cli-command,client): add IntelliStory storybook affected-story filteringPPLT-5844: add IntelliStory storybook affected-story filteringJul 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 ownerJuly 15, 2026 14:29
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
RaghavsBrowserStackand others added 3 commits July 21, 2026 13:20
Add tests for the graceful-bail paths added in "apply suggested fixed":
- malformed stats-file JSON (validateAndReadStats)
- unreadable current lockfile (getAffectedPackages)
- unreadable package.json (getAffectedPackages)
These run on Node 14 (they bail before the Node>=18 snyk path), fixing the
lines/statements coverage-threshold failure on the Node 14 test job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GLOB_CHARS only detected * and ? so brace ({a,b}) and bracket ([abc])
patterns fell through to exact-match, silently disabling the intended
glob semantics for these documented config options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:001c036Reviewers: stack:code-reviewer

Summary

Adds IntelliStory — an affected-story graph feature for @percy/storybook: parses Storybook build stats, diffs against a git baseline (incl. lockfile/dependency diffing via optional snyk-nodejs-lockfile-parser), enqueues a server-side affected-story graph job, tags snapshots for selection, and optionally renders a debug trace.html. Plumbs intelliStory/storybookPath through @percy/client and @percy/core, and adds a Node 20 CI leg for the Node-18+-only snyk path.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed.
HighSecurityInput validation and sanitizationPassassertSafeRef blocks flag-injection; safeJson escapes HTML/JS breakouts; paths anchored via basename.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource-ownership surface.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1 ($-pattern .replace) still open. Finding 2 (glob detection gap) resolved in 001c036.
HighCorrectnessError handling is explicit, no swallowed exceptionsPass*Mostly graceful IntelliStoryBailError; one inconsistent rethrow — Finding 3.
HighCorrectnessNo race conditions or concurrency issuesPassNo new concurrency.
MediumTestingNew code has corresponding testsPassBroad coverage across new modules.
MediumTestingError paths and edge cases testedPass*Package-affected merge branch istanbul ignored rather than tested — Finding 5.
MediumTestingExisting tests still pass (no regressions)PassNot re-run here; CI covers.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo queries.
MediumPerformanceLong-running tasks use background jobsPassGraph job is server-side/async with timeout bail.
MediumQualityFollows existing codebase patternsPassConsistent with client/core plumbing conventions.
MediumQualityChanges are focused (single concern)PassScoped to IntelliStory feature.
LowQualityMeaningful names, no dead codePassClear naming.
LowQualityComments explain why, not whatPass.semgrepignore rationale well-documented.
LowQualityNo unnecessary dependencies addedPasssnyk-nodejs-lockfile-parser is optional/lazy-loaded.

Findings

  • File:packages/cli-command/src/graphTrace.js:114-116

  • Severity: Medium

  • Reviewer: stack:code-reviewer (verified)

  • Issue:.replace('__VERTICES_JSON__', safeJson(...)) uses plain-string replacement values. JS honors $$, $&, $`, $', $n special patterns in the replacement string even when the search arg is a literal. Embedded JSON contains arbitrary file paths; a path with $' re-injects the rest of the template, breaking the embedded JS. safeJson's escaping doesn't cover this — the corruption happens in the .replace call itself.

  • Suggestion: Pass a function as the replacement: .replace('__VERTICES_JSON__', () => safeJson(...)). Add a graphTrace.test.js case with $&/$'/$$ in a vertex name.

  • File:packages/cli-command/src/intelliStory.js:16, 34-41

  • Severity: Medium — ✅ RESOLVED in 001c036

  • Reviewer: stack:code-reviewer (verified)

  • Issue:GLOB_CHARS = /[*?]/ only routed */? patterns through the glob matcher, but patternToRegex enables {extended:true, globstar:true} (brace/bracket support). A configured bailOnChanges: ['src/{a,b}.js'] or bracket pattern silently fell through to literal str === pattern and never matched — silently disabling a documented config option (bailOnChanges/untraced).

  • Fix applied: Broadened detection to const GLOB_CHARS = /[*?{}[\]]/;. Added brace- and bracket-glob tests to assertNoBailOnChanges() and enforceUntraced() — verified failing under the old regex and passing under the fix (full 156-spec suite green).

  • File:packages/cli-command/src/intelliStory.js:326-336

  • Severity: Medium

  • Reviewer: stack:code-reviewer

  • Issue: Only SNYK_LOCKFILE_PARSER_UNAVAILABLE is converted to a graceful IntelliStoryBailError; a buildDepTree parse failure (malformed/older-ref lockfile) rethrows as a raw Error. Every other failure path degrades to "running full snapshot set."

  • Suggestion: Confirm the @percy/storybook caller treats allapplyIntelliStory throws as fall-back-to-full-set. If not, wrap this rethrow in IntelliStoryBailError for consistency.

  • File:packages/cli-command/src/intelliStory.js:67-70

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue:assertSafeRef regex correctly blocks leading -, but also rejects ordinary safe ref syntax like HEAD~5 / abc123^2.

  • Suggestion: Confirm intentional, or widen charset to include ~^ (still safe given the leading-char restriction).

  • File:packages/cli-command/src/intelliStory.js:438-440

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The packageAffectedNodes merge into affectedNodes (the actual mechanism folding lockfile-driven dep changes into the graph) is /* istanbul ignore next */ rather than covered by a real test.

  • Suggestion: Add an itPosix-style Node≥18 integration test exercising the merge end-to-end.

  • File:packages/cli-command/src/intelliStory.js (resolveAndIndex)

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Absolute module ids resolving outside projectRoot (symlinked pkg, monorepo sibling) produce ../-prefixed paths added straight into the files index without a guard.

  • Suggestion: Bail or drop entries where rel.startsWith('..').

  • File:.semgrepignore:118-131 / packages/client/src/client.js:2591-2592 / .github/workflows/test.yml

  • Severity: Info

  • Reviewer: stack:code-reviewer

  • Issue: (a) Whole-file semgrep suppression means future traversal code in the growing intelliStory.js goes unchecked. (b) client.intelliStoryStats is lazily-created, never reset — accumulates across builds if a client is reused. (c) New test-node20 CI job (workflow change) — flagged for visibility; mirror logic looks consistent, no action requested.

  • Suggestion: Consider narrowing semgrep scope over time; reset stats in createBuild.


Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:966c05dReviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), findings then re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a new static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

The bail-first posture is the right architecture for a feature that can silently drop visual coverage, and it is applied thoroughly — 15+ explicit bail sites covering missing build id, unreadable/invalid stats, git failures, ambiguous or absent lockfiles, manifest changes spanning directories, browser-upgrade builds, and graph-job timeouts. All four Medium findings below are gaps in that otherwise-sound safety net, not architectural problems.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through the existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync is used without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassExtensive and largely correct bail coverage. Four Medium gaps: findings 1–4.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps: the three API calls are not (finding 1), and dropped stats modules are logged at debug only (finding 4).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedPassBail branches are covered individually (commit e071c952 restored 100% coverage). The glob semantics in finding 3 are asserted deliberately by intelliStory.test.js:286-289.
MediumTestingExisting tests still pass (no regressions)PassAll CI checks green on 966c05d: Lint, Build, Build & verify executable, Regression, every package suite (incl. the new Node 20 leg), CodeQL, Semgrep OSS.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover. Minor: findings 5–7.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer (refined by orchestrator verification)
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or DNS failure therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); it is only thrown exceptions that leak.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph, and in pollGraphStatus treat a throw as a non-done status.

2. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (verified)
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail, and filtering proceeds as though nothing global changed. This is the under-snapshot direction: real visual regressions ship unnoticed.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

3. Bare-extension bailOnChanges globs silently match nothing below the repo root

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex), consumed at :251
  • Severity: Medium
  • Reviewer: stack:code-reviewer (severity downgraded from High by the orchestrator — see below)
  • Issue: With { extended: true, globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$ — verified locally — so it matches only root-level files, never src/components/Button.css. A user who configures bailOnChanges: ['*.css'] expecting "bail on any stylesheet change" gets no bail, and IntelliStory filters as if nothing unsafe changed. untraced shares the semantics but fails safe (nothing is dropped, so more snapshots are taken).
    Downgraded from the sub-reviewer's High because this is standard glob behaviorminimatch, picomatch, and GitHub Actions paths: filters all treat a bare *.ext as root-only — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. Still worth flagging: bailOnChanges is the one user-facing safety valve and the failure mode is silent.
  • Suggestion: Either document that patterns are path-segment-scoped and **/*.css is required (there is currently no documentation for bailOnChanges/untraced anywhere in the repo), or auto-broaden bare-name patterns before compiling: const p = pattern.includes('/') ? pattern : '**/' + pattern;. If the current semantics stay, rename the test to state the intent ('a bare-extension pattern does not match nested paths') so the behavior reads as deliberate.

4. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run still proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate the existing message to log.warn, and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

5. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue:resolveAndIndex runs only when copy.type === 'src'. Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the external enrichment tool ever emits a third type carrying an absolute path — an asset, CSS, or JSON import — that raw absolute path ships unnormalized in the modules payload: it leaks the developer's home or CI runner path, and it cannot be correlated against the project-relative affectedNodes, which is a false-negative path.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe and type-agnostic.

6. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (correction: rename detection is git's default since 2.9, so the inconsistency is present out of the box, not only under custom config)
  • Issue:affectedNodes comes from git diff --name-only with rename detection active, which collapses a rename to the new path alone; getAffectedFileLocations runs the same diff with --no-renames, which yields a delete + add pair. The two views of one diff can disagree for renamed files. Low practical impact — the old path no longer exists to be imported — but it makes the two graph inputs non-comparable.
  • Suggestion: Add --no-renames to the gitDiffNames invocation for determinism and parity with :84.

7. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer (verified)
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. This is opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or a mis-selection — often inside a locked-down or air-gapped CI environment where the CDN is unreachable and the trace renders blank.
  • Suggestion: Vendor the (small) Drawflow bundle into the package and inline it into the template.

8. Whole-file .semgrepignore suppression for intelliStory.js

  • File:.semgrepignore:49-57
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: The suppression is file-level rather than scoped to the specific path.join/path.resolve sites that trigger the false positive. intelliStory.js does a fair amount of fs/path work, so any genuinely unsafe join added later goes unscanned. The stated rationale (inline // nosemgrep not honored by the CI semgrep version) is legitimate and well documented.
  • Suggestion: Track a follow-up to narrow the suppression once the CI semgrep version honors inline annotations.

9. No bail is logged at the throw site

  • File:packages/cli-command/src/intelliStory.js — all IntelliStoryBailError throw sites
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: Every bail constructs a carefully worded message but none logs at warn/info before throwing, so whether a user ever learns that filtering did not apply depends entirely on the catch site in @percy/storybook — outside this repo and, per the PR body, still a follow-up. If that catcher logs at debug or swallows while falling back, the fail-safe is invisible.
  • Suggestion:log.warn(message) at the throw site (or inside a small bail(message, log) helper) so visibility does not depend on the external consumer.

Verified non-issues

  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced anywhere in cli-command — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:01aa1f6Reviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), second independent pass; all findings re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

Note on this run. Head moved from 966c05d to 01aa1f6, but both new commits (01aa1f6, f6ef573) are Merge branch 'master' into PPLT-5844git diff 966c05d 01aa1f6 over the PR-owned paths is empty, so the reviewable code is byte-identical to the previously reviewed head. The fast-path did not fire because the intermediate merge f6ef573 carries no prior Claude Code Review status, so a full review ran. This second, independent pass surfaced finding 1, which the first pass missed. The code has not regressed — the assessment has sharpened, and the verdict changes accordingly.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync runs without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1: bailOnChanges/untraced globs are matched against repo-root-relative paths with no rebasing, while story importPaths in the same function are rebased from the invocation directory — so the user's safety valve is silently inert for monorepo/subdirectory invocation.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps are Medium, not High: the three API calls are unwrapped (finding 2), malformed globs are swallowed to false (finding 4), and dropped stats modules are logged at debug only (finding 5).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedFailBail branches are covered individually, but every fixture builds a git repo with files at its root — there is no cwd ≠ projectRoot case, which is exactly why finding 1 went unnoticed. No test simulates a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)PassAll CI checks were green on 966c05d, whose PR-owned tree is identical to this head.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. bailOnChanges/untraced globs are never rebased to the invocation directory — the safety valve is silently inert in a monorepo

  • File:packages/cli-command/src/intelliStory.js:250-252, :259-262 (compare :415-426)

  • Severity: High

  • Reviewer: stack:code-reviewer (second pass); mechanism verified by the orchestrator

  • Issue:affectedNodes comes from git diff --name-only, which always emits repo-root-relative paths regardless of cwd. assertNoBailOnChanges and enforceUntraced match user-supplied globs directly against those paths, and glob-to-regexp fully anchors the result (^…$). Meanwhile, twenty lines further down, the same function builds invocationDir = process.cwd() and rebases each story's importPath from the invocation directory to project-relative (:415-426) — so the file demonstrably does expect cwd ≠ projectRoot. User globs get no such treatment.

    Concrete failure: a monorepo user runs cd packages/ui && percy storybook ./storybook-static with bailOnChanges: ['webpack.config.js']. A change to that file lands in affectedNodes as packages/ui/webpack.config.js; the pattern compiles to /^webpack\.config\.js$/ and does not match. No bail fires, no warning is logged, and IntelliStory filters as though nothing risky changed — the under-snapshot direction, where a genuine visual regression ships unreviewed. Monorepo-with-subdirectory is the mainstream Storybook layout, and bailOnChanges exists precisely as the escape hatch for changes the graph can't reason about.

    Not covered by any test: every fixture in intelliStory.test.js constructs a repo with files at its root, so no cwd ≠ projectRoot case is exercised.

  • Suggestion: Rebase user patterns the same way story paths already are, before matching:

    constrebase=g=>{if(path.isAbsolute(g))returnpath.relative(projectRoot,g).split(path.sep).join('/');constabs=path.resolve(invocationDir,g);constrel=path.relative(projectRoot,abs);returnrel.startsWith('..') ? g : rel.split(path.sep).join('/');};

    and pass bailOnChanges.map(rebase) / untraced.map(rebase) into the two functions. Add a test with cwd set to a nested package directory. If root-relative globs are the intended contract instead, that must be documented and a bail should fire when cwd !== projectRoot and patterns are configured — silently doing nothing is the one outcome to avoid.

2. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer — independently reported by both review passes
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or connection reset therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); only thrown exceptions leak. diffLockfileDeps (:316) has the same shape: it remaps only SNYK_LOCKFILE_PARSER_UNAVAILABLE and re-throws anything else raw.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status. Add tests where each client method rejects.

3. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail. Same under-snapshot direction as finding 1, and the two compound for monorepo users.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

4. A malformed glob is silently treated as "matches nothing"

  • File:packages/cli-command/src/intelliStory.js:34-43 (matchesPattern)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (second pass); verified — glob-to-regexp('src/[') throws Unterminated character class, 'src/{a' throws Unterminated group
  • Issue:try { … } catch { return false } means a typo'd pattern in a user's config compiles-fails and is then treated as never matching. For untraced that fails safe (the file stays traced). For bailOnChanges it fails open: the guardrail is silently disabled with nothing logged anywhere. The over-length guard at :22 has the same shape and is explicitly tested as non-throwing (intelliStory.test.js:269-271).
  • Suggestion:log.warn when a pattern fails to compile — a broken guardrail must be visible:
    }catch(e){log.warn(`IntelliStory: ignoring invalid pattern "${pattern}": ${e.message}`);returnfalse;}
    Better still, validate the patterns once at config-parse time rather than per-path.

5. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator (first pass)
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate to log.warn and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

6. Bare-extension globs don't match nested paths

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: With { globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior — minimatch, picomatch, and GitHub Actions paths: filters agree — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. It is listed because it lands on the same safety valve as findings 1 and 4, and there is currently no documentation for bailOnChanges/untraced anywhere in the repo.
  • Suggestion: Document that patterns are path-segment-scoped and **/*.css is required. If you prefer forgiving semantics, auto-broaden bare-name patterns: const p = pattern.includes('/') ? pattern : '**/' + pattern; — note this must be decided together with finding 1's rebasing.

7. noRequireBinding guard is narrower than the crash class it protects

  • File:packages/cli-command/test/noRequireBinding.test.js:40
  • Severity: Low
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:FORBIDDEN = /\b(?:const|let|var)\s+require\s*=\s*createRequire\b/ only catches a single-line declaration. The same startup crash returns via import { createRequire as require } from 'module', const { default: require } = …, or a line-wrapped const require =\n createRequire(…) — none of which match.
  • Suggestion: Flag any binding of the bare identifier require (import specifier, declaration LHS, or parameter), not just the createRequire-RHS form.

8. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the enrichment tool ever emits a third type carrying an absolute path, that path ships unnormalized: it leaks a local or CI-runner path and cannot be correlated against project-relative affectedNodes.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe.

9. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Rename detection is git's default since 2.9, so affectedNodes collapses a rename to the new path alone while getAffectedFileLocations (run with --no-renames) sees a delete + add pair. The two views of one diff disagree for renamed files.
  • Suggestion: Add --no-renames to gitDiffNames for parity.

10. path.relative does not case-fold when computing project-relative module ids

  • File:packages/cli-command/src/intelliStory.js:117-128 (resolveAndIndex)
  • Severity: Low (mechanism demonstrated, real-world trigger unconfirmed)
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:path.relative('/Users/bob/proj', '/Users/Bob/Proj/src/App.js') returns '../../Bob/Proj/src/App.js' — exact segment comparison, no case folding, on every platform. On case-insensitive-but-preserving filesystems (APFS, NTFS), any casing divergence between the bundler's absolute module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that can never match a real repo-relative path. The reviewer could not force an actual divergence, so this is flagged as plausible rather than confirmed.
  • Suggestion: At minimum, warn when the computed relative path escapes projectRoot (starts with ..) rather than silently indexing it; optionally normalize both sides through fs.realpathSync.

11. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer — reported by both passes
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. Opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or mis-selection — often inside a locked-down or air-gapped CI runner where it renders blank.
  • Suggestion: Vendor the small Drawflow bundle and inline it into the template.

12. Minor: bail visibility, semgrep scope, stats-shape logging

  • Severity: Low
  • Reviewer: stack:code-reviewer (both passes)
  • Issue: (a) No bail logs at the throw site, so user visibility depends entirely on the @percy/storybook catch — outside this repo and still a follow-up per the PR body. (b) .semgrepignore:49-57 suppresses intelliStory.js file-wide rather than at the flagged joins, so future unsafe joins in this fs/path-heavy file go unscanned; the rationale is documented and the constraint (inline // nosemgrep unsupported by the CI version) is real. (c) readStats's stats.modules || [] (:161) makes a stats file with no modules key indistinguishable from a legitimately empty one.
  • Suggestion: (a) log.warn(message) inside a small bail(message, log) helper. (b) Track narrowing the suppression. (c) log.debug when the key is absent versus empty.

Informational — not counted as defects

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested at lockfileDiff.test.js:85): Storybook addons are typically devDependencies and are imported directly by preview.js/story files. A pure addon version bump with no source diff will not surface through this path. It is explicit and tested, so likely intentional (relying on the server-side module graph instead) — worth a sanity check with the team given how conservative this feature aims to be.
  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: FAIL

Comment threadpackages/cli-command/test/intelliStory.test.js Fixed
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:91bd770Reviewers:stack:code-reviewer (prior passes), fix verification by the orchestrator against source at this head

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (cjsRequire) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

This head resolves the High finding from the 01aa1f6 review along with three of the five Mediums. Verdict flips to PASS.

Fix verification (vs. 01aa1f6)

#FindingSevStatus at 91bd770
1bailOnChanges/untraced globs not rebased to the invocation dirHighFixed
2Client calls throw raw Error, not IntelliStoryBailErrorMediumOpen
3.storybook config dir hardcodedMediumFixed
4Malformed glob silently swallowed to "no match"MediumFixed
5Dropped stats modules logged at debug, no thresholdMediumFixed
6Bare-extension globs don't match nested pathsMediumOpen (by design; still undocumented)
7noRequireBinding regex narrower than the crash classLowOpen
8mapEntry normalizes source only for type === 'src'LowOpen
9gitDiffNames omits --no-renamesLowOpen
10path.relative does not case-foldLowOpen (was unconfirmed)
11trace.html loads Drawflow from a CDNLowOpen
12No log.warn at bail throw sitesLowPartially addressed

Finding 1 is fixed properly, not papered over. resolvePattern (intelliStory.js:88-100) rebases each user pattern from the invocation directory onto the repo root — the same basis normalizeImportPath already used for story paths, so the asymmetry that caused the bug is gone. Beyond that, the fix handles the two readings asymmetrically by direction of harm, which is the right call:

  • assertNoBailOnChanges (:394-425) bails when a pattern matches only under the repo-root reading, logging a warning with a concrete rewrite suggestion — a stale root-relative config still protects the user.
  • enforceUntraced (:433-462) declines to apply such a pattern and warns, since applying it would drop snapshots.

A <rootDir>/ prefix is an explicit opt-out, patterns resolving outside the repo bail rather than matching nothing, and resolveConfigDirs (:361-390) additionally checks that a configured directory actually exists — catching a wrong-basis configDir at config time instead of silently never matching. Findings 3, 4 and 5 are each fixed with matching care: isInsideDirs distinguishes single-segment (any depth) from multi-segment (prefix) config dirs so config/storybook can't match config/storybook-old; patternError validates globs up front and routes a compile failure to a bail for bailOnChanges and a warn-skip for untraced; and readStats now separates deliberately-excluded modules from unresolved ones, warns on the latter and bails past a 10% ratio of resolvable candidates — so a stats file heavy in node_modules no longer trips the threshold. intelliStory.test.js grows 364 lines with cwd ≠ projectRoot fixtures (projectRoot: '/repo', invocationDir: '/repo/packages/ui') and a resolveConfigDirs() suite — the gap that let finding 1 through is now covered.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth flows through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before spawnSync; statsFile narrowed to a bare .json basename; spawnSync runs without a shell. User globs are now validated up front via patternError. Dedicated security lens is disabled in this orchestrator.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassThe path-basis defect is fixed, with both readings handled by direction of harm. Remaining gaps are Medium/Low.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassGlob compile failures are no longer swallowed, and incomplete graphs now bail past a threshold. Finding 2 (unwrapped client calls) remains, at Medium.
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop; no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass+364 test lines this push, covering resolvePattern, resolveConfigDirs, and the <rootDir>/ opt-out.
MediumTestingError paths and edge cases testedPassThe cwd ≠ projectRoot gap is closed. Still untested: a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)Pass46 checks green, incl. Lint, Build & verify executable, Regression, all cli-command suites and the Node 20 leg. Test @percy/core still pending at review time — unrelated to the changed packages.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne GET, one POST, bounded poll (12 × 5s). In-memory stats parsing.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled.
MediumQualityFollows existing codebase patternsPassMatches PercyClient conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on and the CI leg that exercises the lockfile path.
LowQualityMeaningful names, no dead codePassstream-json confirmed absent. New helpers (resolvePattern, patternError, rootReadingHit, suggestRewrite) are well named and each has a stated reason to exist.
LowQualityComments explain why, not whatPassThe new comments are unusually good — they record the direction of harm reasoning behind each asymmetry ("recognising one directory too many only costs snapshots, while missing the real one loses comparisons"), which is exactly what a future maintainer needs.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp used; snyk-nodejs-lockfile-parser correctly optional with a Node 20 CI leg.

Findings

2. API/network failures still escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:292, :331, :555
  • Severity: Medium
  • Reviewer: stack:code-reviewer (reported independently by both prior passes); re-verified unfixed at this head
  • Issue: Verified still open: getStatus('intelli_story_graph', …) in pollGraphStatus (:292), getIntelliStorySnapshotNameToCommit (:331), and generateIntelliStoryGraph (:555) are all bare awaits with no surrounding try/catch. Every other failure mode in this file — git, fs, config, lockfile, malformed glob, degraded graph — is deliberately converted to IntelliStoryBailError so the caller can degrade to a full snapshot run. A transient 5xx, timeout, or connection reset from these three still propagates as a plain Error, so if @percy/storybook catches IntelliStoryBailError specifically, an API blip hard-fails the storybook command instead of falling back. The graph-job timeout path is handled correctly (:561); only thrown exceptions leak. Not a merge blocker under the verdict rule, but it is now the last inconsistency in an otherwise uniformly fail-safe module.
  • Suggestion:
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status so the existing :561 bail covers it. Add tests where each client method rejects.

6. Bare-extension globs still don't match nested paths, and the contract is undocumented

  • File:packages/cli-command/src/intelliStory.js:28
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass)
  • Issue:glob-to-regexp('*.css', { globstar: true }) compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior (minimatch, picomatch, GitHub Actions paths: all agree) and is deliberately asserted by the tests, so it is not a defect. It is still listed because the new rootReadingHit warning covers the basis mistake but not the depth mistake — a user who writes *.css gets no warning at all — and there remains no documentation for bailOnChanges/untraced/configDir anywhere in the repo.
  • Suggestion: Document the semantics now that the basis rules are non-trivial (invocation-relative, <rootDir>/ opt-out, path-segment-scoped globs). Optionally extend the existing warning: if a pattern has no / and no **, and some affected node matches **/<pattern>, warn the same way the root-reading case does.

7–11. Carried-over Lows, unchanged at this head

  • noRequireBinding.test.js:40FORBIDDEN still only matches a single-line const/let/var require = createRequire. The same crash returns via import { createRequire as require } from 'module' or a line-wrapped declaration. Broaden to flag any binding of the bare require identifier.
  • intelliStory.js:229mapEntry still gates normalization on copy.type === 'src'. Drop the gate; resolveAndIndex already passes non-absolute values through untouched.
  • intelliStory.js:138gitDiffNames still omits --no-renames while getAffectedFileLocations uses it, so the two views of one diff disagree for renamed files.
  • intelliStory.jsresolveAndIndexpath.relative does not case-fold, so a casing divergence between bundler module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that never match. Mechanism is real, real-world trigger still unconfirmed. Cheapest guard: warn when the computed relative path starts with ...
  • graphTraceTemplate.html:8, :12trace.html still loads Drawflow from cdn.jsdelivr.net, so the diagnostic renders blank in an air-gapped or locked-down CI runner. Vendor the bundle.

12. Bail visibility — partially addressed

  • Severity: Low
  • Issue: This push adds ten log.warn calls, so pattern and configDir problems are now visible. The IntelliStoryBailError throw sites themselves still don't log before throwing, so whether a user learns that filtering did not apply continues to depend on the @percy/storybook catch site — outside this repo and still a follow-up per the PR body.
  • Suggestion: A small bail(message, log) helper that warns and throws.

Informational

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested): Storybook addons are usually devDependencies imported directly by preview.js/story files, so a pure addon bump with no source diff won't surface here. Explicit and tested, so presumably intentional — worth a sanity check with the team.
  • .semgrepignore still suppresses intelliStory.js file-wide rather than at the flagged joins. The file grew ~290 lines this push, which widens the unscanned surface; the documented rationale (inline // nosemgrep unsupported by the CI semgrep version) still holds. Worth narrowing when tooling allows.
  • Version strings moved 1.32.6-beta.31.32.6 via the master release merge; no functional change.

Verdict: PASS

@RaghavsBrowserStack
RaghavsBrowserStack merged commit 8311242 into masterAug 10, 2026
48 checks passed
@RaghavsBrowserStack
RaghavsBrowserStack deleted the PPLT-5844 branch August 10, 2026 12:57
RaghavsBrowserStack added a commit that referenced this pull request Aug 11, 2026
* bump version
* remove stray archive test fixtures committed by mistake
packages/core/.test-archive-mixed and .test-archive-symlink were leftovers
from a local unit-test run that got picked up by the version-bump commit.
.test-archive-symlink/linked.json is an absolute symlink into a developer's
home directory, so it dangles on a fresh CI checkout and makes
`babel packages -d build` (build_cjs) fail with ENOENT, breaking the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix unwanted commits
* remove stray archive test fixtures reintroduced by the master merge
d302b09 deleted these on this branch, but master picked up the same
accidental add in d663543 (bump version, #2377), so merging master back in
(2405903) restored them and left PR #2378 with an empty diff.
packages/core/.test-archive-symlink/linked.json is an absolute symlink into a
developer's home directory. It dangles on a fresh CI checkout, so
`babel packages -d build` (build_cjs) fails with ENOENT and takes down the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* regenerate yarn.lock so `lerna publish` sees a clean tree
The lockfile block added in 8311242 (IntelliStory, #2339) was edited by hand
rather than regenerated, leaving it inconsistent with the dependency graph:
- @types/node@* had no entry, though @types/cacheable-request, @types/keyv,
@types/responselike and @types/yauzl all require it
- get-stream@^5.1.0 had no entry, though cacheable-request@^7.0.2 requires it
- @types/yauzl@^2.9.1 was orphaned — nothing referenced it
So `yarn` on a clean CI checkout resolved the two missing descriptors, pruned
the orphan and rewrote the file. That left the working tree dirty, and
`lerna publish from-package` aborted with EUNCOMMIT (M yarn.lock), failing the
Release job.
Regenerated with a plain `yarn install`. `yarn install --frozen-lockfile` is
now a no-op, so the release checkout stays clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@RaghavsBrowserStack@prklm10@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Merged
RaghavsBrowserStack merged 29 commits into
masterfrom
PPLT-5844
Aug 10, 2026
Merged

PPLT-5844: add IntelliStory storybook affected-story filtering#2339
RaghavsBrowserStack merged 29 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).
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/test/graphTrace.test.js Fixed
…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.
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
@RaghavsBrowserStackRaghavsBrowserStack changed the title feat(cli-command,client): add IntelliStory storybook affected-story filteringPPLT-5844: add IntelliStory storybook affected-story filteringJul 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 ownerJuly 15, 2026 14:29
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
RaghavsBrowserStackand others added 3 commits July 21, 2026 13:20
Add tests for the graceful-bail paths added in "apply suggested fixed":
- malformed stats-file JSON (validateAndReadStats)
- unreadable current lockfile (getAffectedPackages)
- unreadable package.json (getAffectedPackages)
These run on Node 14 (they bail before the Node>=18 snyk path), fixing the
lines/statements coverage-threshold failure on the Node 14 test job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GLOB_CHARS only detected * and ? so brace ({a,b}) and bracket ([abc])
patterns fell through to exact-match, silently disabling the intended
glob semantics for these documented config options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:001c036Reviewers: stack:code-reviewer

Summary

Adds IntelliStory — an affected-story graph feature for @percy/storybook: parses Storybook build stats, diffs against a git baseline (incl. lockfile/dependency diffing via optional snyk-nodejs-lockfile-parser), enqueues a server-side affected-story graph job, tags snapshots for selection, and optionally renders a debug trace.html. Plumbs intelliStory/storybookPath through @percy/client and @percy/core, and adds a Node 20 CI leg for the Node-18+-only snyk path.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed.
HighSecurityInput validation and sanitizationPassassertSafeRef blocks flag-injection; safeJson escapes HTML/JS breakouts; paths anchored via basename.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource-ownership surface.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1 ($-pattern .replace) still open. Finding 2 (glob detection gap) resolved in 001c036.
HighCorrectnessError handling is explicit, no swallowed exceptionsPass*Mostly graceful IntelliStoryBailError; one inconsistent rethrow — Finding 3.
HighCorrectnessNo race conditions or concurrency issuesPassNo new concurrency.
MediumTestingNew code has corresponding testsPassBroad coverage across new modules.
MediumTestingError paths and edge cases testedPass*Package-affected merge branch istanbul ignored rather than tested — Finding 5.
MediumTestingExisting tests still pass (no regressions)PassNot re-run here; CI covers.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo queries.
MediumPerformanceLong-running tasks use background jobsPassGraph job is server-side/async with timeout bail.
MediumQualityFollows existing codebase patternsPassConsistent with client/core plumbing conventions.
MediumQualityChanges are focused (single concern)PassScoped to IntelliStory feature.
LowQualityMeaningful names, no dead codePassClear naming.
LowQualityComments explain why, not whatPass.semgrepignore rationale well-documented.
LowQualityNo unnecessary dependencies addedPasssnyk-nodejs-lockfile-parser is optional/lazy-loaded.

Findings

  • File:packages/cli-command/src/graphTrace.js:114-116

  • Severity: Medium

  • Reviewer: stack:code-reviewer (verified)

  • Issue:.replace('__VERTICES_JSON__', safeJson(...)) uses plain-string replacement values. JS honors $$, $&, $`, $', $n special patterns in the replacement string even when the search arg is a literal. Embedded JSON contains arbitrary file paths; a path with $' re-injects the rest of the template, breaking the embedded JS. safeJson's escaping doesn't cover this — the corruption happens in the .replace call itself.

  • Suggestion: Pass a function as the replacement: .replace('__VERTICES_JSON__', () => safeJson(...)). Add a graphTrace.test.js case with $&/$'/$$ in a vertex name.

  • File:packages/cli-command/src/intelliStory.js:16, 34-41

  • Severity: Medium — ✅ RESOLVED in 001c036

  • Reviewer: stack:code-reviewer (verified)

  • Issue:GLOB_CHARS = /[*?]/ only routed */? patterns through the glob matcher, but patternToRegex enables {extended:true, globstar:true} (brace/bracket support). A configured bailOnChanges: ['src/{a,b}.js'] or bracket pattern silently fell through to literal str === pattern and never matched — silently disabling a documented config option (bailOnChanges/untraced).

  • Fix applied: Broadened detection to const GLOB_CHARS = /[*?{}[\]]/;. Added brace- and bracket-glob tests to assertNoBailOnChanges() and enforceUntraced() — verified failing under the old regex and passing under the fix (full 156-spec suite green).

  • File:packages/cli-command/src/intelliStory.js:326-336

  • Severity: Medium

  • Reviewer: stack:code-reviewer

  • Issue: Only SNYK_LOCKFILE_PARSER_UNAVAILABLE is converted to a graceful IntelliStoryBailError; a buildDepTree parse failure (malformed/older-ref lockfile) rethrows as a raw Error. Every other failure path degrades to "running full snapshot set."

  • Suggestion: Confirm the @percy/storybook caller treats allapplyIntelliStory throws as fall-back-to-full-set. If not, wrap this rethrow in IntelliStoryBailError for consistency.

  • File:packages/cli-command/src/intelliStory.js:67-70

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue:assertSafeRef regex correctly blocks leading -, but also rejects ordinary safe ref syntax like HEAD~5 / abc123^2.

  • Suggestion: Confirm intentional, or widen charset to include ~^ (still safe given the leading-char restriction).

  • File:packages/cli-command/src/intelliStory.js:438-440

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The packageAffectedNodes merge into affectedNodes (the actual mechanism folding lockfile-driven dep changes into the graph) is /* istanbul ignore next */ rather than covered by a real test.

  • Suggestion: Add an itPosix-style Node≥18 integration test exercising the merge end-to-end.

  • File:packages/cli-command/src/intelliStory.js (resolveAndIndex)

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Absolute module ids resolving outside projectRoot (symlinked pkg, monorepo sibling) produce ../-prefixed paths added straight into the files index without a guard.

  • Suggestion: Bail or drop entries where rel.startsWith('..').

  • File:.semgrepignore:118-131 / packages/client/src/client.js:2591-2592 / .github/workflows/test.yml

  • Severity: Info

  • Reviewer: stack:code-reviewer

  • Issue: (a) Whole-file semgrep suppression means future traversal code in the growing intelliStory.js goes unchecked. (b) client.intelliStoryStats is lazily-created, never reset — accumulates across builds if a client is reused. (c) New test-node20 CI job (workflow change) — flagged for visibility; mirror logic looks consistent, no action requested.

  • Suggestion: Consider narrowing semgrep scope over time; reset stats in createBuild.


Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:966c05dReviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), findings then re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a new static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

The bail-first posture is the right architecture for a feature that can silently drop visual coverage, and it is applied thoroughly — 15+ explicit bail sites covering missing build id, unreadable/invalid stats, git failures, ambiguous or absent lockfiles, manifest changes spanning directories, browser-upgrade builds, and graph-job timeouts. All four Medium findings below are gaps in that otherwise-sound safety net, not architectural problems.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through the existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync is used without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassExtensive and largely correct bail coverage. Four Medium gaps: findings 1–4.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps: the three API calls are not (finding 1), and dropped stats modules are logged at debug only (finding 4).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedPassBail branches are covered individually (commit e071c952 restored 100% coverage). The glob semantics in finding 3 are asserted deliberately by intelliStory.test.js:286-289.
MediumTestingExisting tests still pass (no regressions)PassAll CI checks green on 966c05d: Lint, Build, Build & verify executable, Regression, every package suite (incl. the new Node 20 leg), CodeQL, Semgrep OSS.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover. Minor: findings 5–7.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer (refined by orchestrator verification)
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or DNS failure therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); it is only thrown exceptions that leak.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph, and in pollGraphStatus treat a throw as a non-done status.

2. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (verified)
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail, and filtering proceeds as though nothing global changed. This is the under-snapshot direction: real visual regressions ship unnoticed.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

3. Bare-extension bailOnChanges globs silently match nothing below the repo root

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex), consumed at :251
  • Severity: Medium
  • Reviewer: stack:code-reviewer (severity downgraded from High by the orchestrator — see below)
  • Issue: With { extended: true, globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$ — verified locally — so it matches only root-level files, never src/components/Button.css. A user who configures bailOnChanges: ['*.css'] expecting "bail on any stylesheet change" gets no bail, and IntelliStory filters as if nothing unsafe changed. untraced shares the semantics but fails safe (nothing is dropped, so more snapshots are taken).
    Downgraded from the sub-reviewer's High because this is standard glob behaviorminimatch, picomatch, and GitHub Actions paths: filters all treat a bare *.ext as root-only — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. Still worth flagging: bailOnChanges is the one user-facing safety valve and the failure mode is silent.
  • Suggestion: Either document that patterns are path-segment-scoped and **/*.css is required (there is currently no documentation for bailOnChanges/untraced anywhere in the repo), or auto-broaden bare-name patterns before compiling: const p = pattern.includes('/') ? pattern : '**/' + pattern;. If the current semantics stay, rename the test to state the intent ('a bare-extension pattern does not match nested paths') so the behavior reads as deliberate.

4. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run still proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate the existing message to log.warn, and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

5. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue:resolveAndIndex runs only when copy.type === 'src'. Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the external enrichment tool ever emits a third type carrying an absolute path — an asset, CSS, or JSON import — that raw absolute path ships unnormalized in the modules payload: it leaks the developer's home or CI runner path, and it cannot be correlated against the project-relative affectedNodes, which is a false-negative path.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe and type-agnostic.

6. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (correction: rename detection is git's default since 2.9, so the inconsistency is present out of the box, not only under custom config)
  • Issue:affectedNodes comes from git diff --name-only with rename detection active, which collapses a rename to the new path alone; getAffectedFileLocations runs the same diff with --no-renames, which yields a delete + add pair. The two views of one diff can disagree for renamed files. Low practical impact — the old path no longer exists to be imported — but it makes the two graph inputs non-comparable.
  • Suggestion: Add --no-renames to the gitDiffNames invocation for determinism and parity with :84.

7. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer (verified)
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. This is opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or a mis-selection — often inside a locked-down or air-gapped CI environment where the CDN is unreachable and the trace renders blank.
  • Suggestion: Vendor the (small) Drawflow bundle into the package and inline it into the template.

8. Whole-file .semgrepignore suppression for intelliStory.js

  • File:.semgrepignore:49-57
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: The suppression is file-level rather than scoped to the specific path.join/path.resolve sites that trigger the false positive. intelliStory.js does a fair amount of fs/path work, so any genuinely unsafe join added later goes unscanned. The stated rationale (inline // nosemgrep not honored by the CI semgrep version) is legitimate and well documented.
  • Suggestion: Track a follow-up to narrow the suppression once the CI semgrep version honors inline annotations.

9. No bail is logged at the throw site

  • File:packages/cli-command/src/intelliStory.js — all IntelliStoryBailError throw sites
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: Every bail constructs a carefully worded message but none logs at warn/info before throwing, so whether a user ever learns that filtering did not apply depends entirely on the catch site in @percy/storybook — outside this repo and, per the PR body, still a follow-up. If that catcher logs at debug or swallows while falling back, the fail-safe is invisible.
  • Suggestion:log.warn(message) at the throw site (or inside a small bail(message, log) helper) so visibility does not depend on the external consumer.

Verified non-issues

  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced anywhere in cli-command — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:01aa1f6Reviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), second independent pass; all findings re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

Note on this run. Head moved from 966c05d to 01aa1f6, but both new commits (01aa1f6, f6ef573) are Merge branch 'master' into PPLT-5844git diff 966c05d 01aa1f6 over the PR-owned paths is empty, so the reviewable code is byte-identical to the previously reviewed head. The fast-path did not fire because the intermediate merge f6ef573 carries no prior Claude Code Review status, so a full review ran. This second, independent pass surfaced finding 1, which the first pass missed. The code has not regressed — the assessment has sharpened, and the verdict changes accordingly.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync runs without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1: bailOnChanges/untraced globs are matched against repo-root-relative paths with no rebasing, while story importPaths in the same function are rebased from the invocation directory — so the user's safety valve is silently inert for monorepo/subdirectory invocation.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps are Medium, not High: the three API calls are unwrapped (finding 2), malformed globs are swallowed to false (finding 4), and dropped stats modules are logged at debug only (finding 5).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedFailBail branches are covered individually, but every fixture builds a git repo with files at its root — there is no cwd ≠ projectRoot case, which is exactly why finding 1 went unnoticed. No test simulates a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)PassAll CI checks were green on 966c05d, whose PR-owned tree is identical to this head.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. bailOnChanges/untraced globs are never rebased to the invocation directory — the safety valve is silently inert in a monorepo

  • File:packages/cli-command/src/intelliStory.js:250-252, :259-262 (compare :415-426)

  • Severity: High

  • Reviewer: stack:code-reviewer (second pass); mechanism verified by the orchestrator

  • Issue:affectedNodes comes from git diff --name-only, which always emits repo-root-relative paths regardless of cwd. assertNoBailOnChanges and enforceUntraced match user-supplied globs directly against those paths, and glob-to-regexp fully anchors the result (^…$). Meanwhile, twenty lines further down, the same function builds invocationDir = process.cwd() and rebases each story's importPath from the invocation directory to project-relative (:415-426) — so the file demonstrably does expect cwd ≠ projectRoot. User globs get no such treatment.

    Concrete failure: a monorepo user runs cd packages/ui && percy storybook ./storybook-static with bailOnChanges: ['webpack.config.js']. A change to that file lands in affectedNodes as packages/ui/webpack.config.js; the pattern compiles to /^webpack\.config\.js$/ and does not match. No bail fires, no warning is logged, and IntelliStory filters as though nothing risky changed — the under-snapshot direction, where a genuine visual regression ships unreviewed. Monorepo-with-subdirectory is the mainstream Storybook layout, and bailOnChanges exists precisely as the escape hatch for changes the graph can't reason about.

    Not covered by any test: every fixture in intelliStory.test.js constructs a repo with files at its root, so no cwd ≠ projectRoot case is exercised.

  • Suggestion: Rebase user patterns the same way story paths already are, before matching:

    constrebase=g=>{if(path.isAbsolute(g))returnpath.relative(projectRoot,g).split(path.sep).join('/');constabs=path.resolve(invocationDir,g);constrel=path.relative(projectRoot,abs);returnrel.startsWith('..') ? g : rel.split(path.sep).join('/');};

    and pass bailOnChanges.map(rebase) / untraced.map(rebase) into the two functions. Add a test with cwd set to a nested package directory. If root-relative globs are the intended contract instead, that must be documented and a bail should fire when cwd !== projectRoot and patterns are configured — silently doing nothing is the one outcome to avoid.

2. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer — independently reported by both review passes
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or connection reset therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); only thrown exceptions leak. diffLockfileDeps (:316) has the same shape: it remaps only SNYK_LOCKFILE_PARSER_UNAVAILABLE and re-throws anything else raw.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status. Add tests where each client method rejects.

3. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail. Same under-snapshot direction as finding 1, and the two compound for monorepo users.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

4. A malformed glob is silently treated as "matches nothing"

  • File:packages/cli-command/src/intelliStory.js:34-43 (matchesPattern)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (second pass); verified — glob-to-regexp('src/[') throws Unterminated character class, 'src/{a' throws Unterminated group
  • Issue:try { … } catch { return false } means a typo'd pattern in a user's config compiles-fails and is then treated as never matching. For untraced that fails safe (the file stays traced). For bailOnChanges it fails open: the guardrail is silently disabled with nothing logged anywhere. The over-length guard at :22 has the same shape and is explicitly tested as non-throwing (intelliStory.test.js:269-271).
  • Suggestion:log.warn when a pattern fails to compile — a broken guardrail must be visible:
    }catch(e){log.warn(`IntelliStory: ignoring invalid pattern "${pattern}": ${e.message}`);returnfalse;}
    Better still, validate the patterns once at config-parse time rather than per-path.

5. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator (first pass)
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate to log.warn and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

6. Bare-extension globs don't match nested paths

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: With { globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior — minimatch, picomatch, and GitHub Actions paths: filters agree — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. It is listed because it lands on the same safety valve as findings 1 and 4, and there is currently no documentation for bailOnChanges/untraced anywhere in the repo.
  • Suggestion: Document that patterns are path-segment-scoped and **/*.css is required. If you prefer forgiving semantics, auto-broaden bare-name patterns: const p = pattern.includes('/') ? pattern : '**/' + pattern; — note this must be decided together with finding 1's rebasing.

7. noRequireBinding guard is narrower than the crash class it protects

  • File:packages/cli-command/test/noRequireBinding.test.js:40
  • Severity: Low
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:FORBIDDEN = /\b(?:const|let|var)\s+require\s*=\s*createRequire\b/ only catches a single-line declaration. The same startup crash returns via import { createRequire as require } from 'module', const { default: require } = …, or a line-wrapped const require =\n createRequire(…) — none of which match.
  • Suggestion: Flag any binding of the bare identifier require (import specifier, declaration LHS, or parameter), not just the createRequire-RHS form.

8. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the enrichment tool ever emits a third type carrying an absolute path, that path ships unnormalized: it leaks a local or CI-runner path and cannot be correlated against project-relative affectedNodes.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe.

9. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Rename detection is git's default since 2.9, so affectedNodes collapses a rename to the new path alone while getAffectedFileLocations (run with --no-renames) sees a delete + add pair. The two views of one diff disagree for renamed files.
  • Suggestion: Add --no-renames to gitDiffNames for parity.

10. path.relative does not case-fold when computing project-relative module ids

  • File:packages/cli-command/src/intelliStory.js:117-128 (resolveAndIndex)
  • Severity: Low (mechanism demonstrated, real-world trigger unconfirmed)
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:path.relative('/Users/bob/proj', '/Users/Bob/Proj/src/App.js') returns '../../Bob/Proj/src/App.js' — exact segment comparison, no case folding, on every platform. On case-insensitive-but-preserving filesystems (APFS, NTFS), any casing divergence between the bundler's absolute module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that can never match a real repo-relative path. The reviewer could not force an actual divergence, so this is flagged as plausible rather than confirmed.
  • Suggestion: At minimum, warn when the computed relative path escapes projectRoot (starts with ..) rather than silently indexing it; optionally normalize both sides through fs.realpathSync.

11. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer — reported by both passes
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. Opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or mis-selection — often inside a locked-down or air-gapped CI runner where it renders blank.
  • Suggestion: Vendor the small Drawflow bundle and inline it into the template.

12. Minor: bail visibility, semgrep scope, stats-shape logging

  • Severity: Low
  • Reviewer: stack:code-reviewer (both passes)
  • Issue: (a) No bail logs at the throw site, so user visibility depends entirely on the @percy/storybook catch — outside this repo and still a follow-up per the PR body. (b) .semgrepignore:49-57 suppresses intelliStory.js file-wide rather than at the flagged joins, so future unsafe joins in this fs/path-heavy file go unscanned; the rationale is documented and the constraint (inline // nosemgrep unsupported by the CI version) is real. (c) readStats's stats.modules || [] (:161) makes a stats file with no modules key indistinguishable from a legitimately empty one.
  • Suggestion: (a) log.warn(message) inside a small bail(message, log) helper. (b) Track narrowing the suppression. (c) log.debug when the key is absent versus empty.

Informational — not counted as defects

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested at lockfileDiff.test.js:85): Storybook addons are typically devDependencies and are imported directly by preview.js/story files. A pure addon version bump with no source diff will not surface through this path. It is explicit and tested, so likely intentional (relying on the server-side module graph instead) — worth a sanity check with the team given how conservative this feature aims to be.
  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: FAIL

Comment threadpackages/cli-command/test/intelliStory.test.js Fixed
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:91bd770Reviewers:stack:code-reviewer (prior passes), fix verification by the orchestrator against source at this head

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (cjsRequire) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

This head resolves the High finding from the 01aa1f6 review along with three of the five Mediums. Verdict flips to PASS.

Fix verification (vs. 01aa1f6)

#FindingSevStatus at 91bd770
1bailOnChanges/untraced globs not rebased to the invocation dirHighFixed
2Client calls throw raw Error, not IntelliStoryBailErrorMediumOpen
3.storybook config dir hardcodedMediumFixed
4Malformed glob silently swallowed to "no match"MediumFixed
5Dropped stats modules logged at debug, no thresholdMediumFixed
6Bare-extension globs don't match nested pathsMediumOpen (by design; still undocumented)
7noRequireBinding regex narrower than the crash classLowOpen
8mapEntry normalizes source only for type === 'src'LowOpen
9gitDiffNames omits --no-renamesLowOpen
10path.relative does not case-foldLowOpen (was unconfirmed)
11trace.html loads Drawflow from a CDNLowOpen
12No log.warn at bail throw sitesLowPartially addressed

Finding 1 is fixed properly, not papered over. resolvePattern (intelliStory.js:88-100) rebases each user pattern from the invocation directory onto the repo root — the same basis normalizeImportPath already used for story paths, so the asymmetry that caused the bug is gone. Beyond that, the fix handles the two readings asymmetrically by direction of harm, which is the right call:

  • assertNoBailOnChanges (:394-425) bails when a pattern matches only under the repo-root reading, logging a warning with a concrete rewrite suggestion — a stale root-relative config still protects the user.
  • enforceUntraced (:433-462) declines to apply such a pattern and warns, since applying it would drop snapshots.

A <rootDir>/ prefix is an explicit opt-out, patterns resolving outside the repo bail rather than matching nothing, and resolveConfigDirs (:361-390) additionally checks that a configured directory actually exists — catching a wrong-basis configDir at config time instead of silently never matching. Findings 3, 4 and 5 are each fixed with matching care: isInsideDirs distinguishes single-segment (any depth) from multi-segment (prefix) config dirs so config/storybook can't match config/storybook-old; patternError validates globs up front and routes a compile failure to a bail for bailOnChanges and a warn-skip for untraced; and readStats now separates deliberately-excluded modules from unresolved ones, warns on the latter and bails past a 10% ratio of resolvable candidates — so a stats file heavy in node_modules no longer trips the threshold. intelliStory.test.js grows 364 lines with cwd ≠ projectRoot fixtures (projectRoot: '/repo', invocationDir: '/repo/packages/ui') and a resolveConfigDirs() suite — the gap that let finding 1 through is now covered.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth flows through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before spawnSync; statsFile narrowed to a bare .json basename; spawnSync runs without a shell. User globs are now validated up front via patternError. Dedicated security lens is disabled in this orchestrator.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassThe path-basis defect is fixed, with both readings handled by direction of harm. Remaining gaps are Medium/Low.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassGlob compile failures are no longer swallowed, and incomplete graphs now bail past a threshold. Finding 2 (unwrapped client calls) remains, at Medium.
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop; no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass+364 test lines this push, covering resolvePattern, resolveConfigDirs, and the <rootDir>/ opt-out.
MediumTestingError paths and edge cases testedPassThe cwd ≠ projectRoot gap is closed. Still untested: a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)Pass46 checks green, incl. Lint, Build & verify executable, Regression, all cli-command suites and the Node 20 leg. Test @percy/core still pending at review time — unrelated to the changed packages.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne GET, one POST, bounded poll (12 × 5s). In-memory stats parsing.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled.
MediumQualityFollows existing codebase patternsPassMatches PercyClient conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on and the CI leg that exercises the lockfile path.
LowQualityMeaningful names, no dead codePassstream-json confirmed absent. New helpers (resolvePattern, patternError, rootReadingHit, suggestRewrite) are well named and each has a stated reason to exist.
LowQualityComments explain why, not whatPassThe new comments are unusually good — they record the direction of harm reasoning behind each asymmetry ("recognising one directory too many only costs snapshots, while missing the real one loses comparisons"), which is exactly what a future maintainer needs.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp used; snyk-nodejs-lockfile-parser correctly optional with a Node 20 CI leg.

Findings

2. API/network failures still escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:292, :331, :555
  • Severity: Medium
  • Reviewer: stack:code-reviewer (reported independently by both prior passes); re-verified unfixed at this head
  • Issue: Verified still open: getStatus('intelli_story_graph', …) in pollGraphStatus (:292), getIntelliStorySnapshotNameToCommit (:331), and generateIntelliStoryGraph (:555) are all bare awaits with no surrounding try/catch. Every other failure mode in this file — git, fs, config, lockfile, malformed glob, degraded graph — is deliberately converted to IntelliStoryBailError so the caller can degrade to a full snapshot run. A transient 5xx, timeout, or connection reset from these three still propagates as a plain Error, so if @percy/storybook catches IntelliStoryBailError specifically, an API blip hard-fails the storybook command instead of falling back. The graph-job timeout path is handled correctly (:561); only thrown exceptions leak. Not a merge blocker under the verdict rule, but it is now the last inconsistency in an otherwise uniformly fail-safe module.
  • Suggestion:
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status so the existing :561 bail covers it. Add tests where each client method rejects.

6. Bare-extension globs still don't match nested paths, and the contract is undocumented

  • File:packages/cli-command/src/intelliStory.js:28
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass)
  • Issue:glob-to-regexp('*.css', { globstar: true }) compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior (minimatch, picomatch, GitHub Actions paths: all agree) and is deliberately asserted by the tests, so it is not a defect. It is still listed because the new rootReadingHit warning covers the basis mistake but not the depth mistake — a user who writes *.css gets no warning at all — and there remains no documentation for bailOnChanges/untraced/configDir anywhere in the repo.
  • Suggestion: Document the semantics now that the basis rules are non-trivial (invocation-relative, <rootDir>/ opt-out, path-segment-scoped globs). Optionally extend the existing warning: if a pattern has no / and no **, and some affected node matches **/<pattern>, warn the same way the root-reading case does.

7–11. Carried-over Lows, unchanged at this head

  • noRequireBinding.test.js:40FORBIDDEN still only matches a single-line const/let/var require = createRequire. The same crash returns via import { createRequire as require } from 'module' or a line-wrapped declaration. Broaden to flag any binding of the bare require identifier.
  • intelliStory.js:229mapEntry still gates normalization on copy.type === 'src'. Drop the gate; resolveAndIndex already passes non-absolute values through untouched.
  • intelliStory.js:138gitDiffNames still omits --no-renames while getAffectedFileLocations uses it, so the two views of one diff disagree for renamed files.
  • intelliStory.jsresolveAndIndexpath.relative does not case-fold, so a casing divergence between bundler module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that never match. Mechanism is real, real-world trigger still unconfirmed. Cheapest guard: warn when the computed relative path starts with ...
  • graphTraceTemplate.html:8, :12trace.html still loads Drawflow from cdn.jsdelivr.net, so the diagnostic renders blank in an air-gapped or locked-down CI runner. Vendor the bundle.

12. Bail visibility — partially addressed

  • Severity: Low
  • Issue: This push adds ten log.warn calls, so pattern and configDir problems are now visible. The IntelliStoryBailError throw sites themselves still don't log before throwing, so whether a user learns that filtering did not apply continues to depend on the @percy/storybook catch site — outside this repo and still a follow-up per the PR body.
  • Suggestion: A small bail(message, log) helper that warns and throws.

Informational

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested): Storybook addons are usually devDependencies imported directly by preview.js/story files, so a pure addon bump with no source diff won't surface here. Explicit and tested, so presumably intentional — worth a sanity check with the team.
  • .semgrepignore still suppresses intelliStory.js file-wide rather than at the flagged joins. The file grew ~290 lines this push, which widens the unscanned surface; the documented rationale (inline // nosemgrep unsupported by the CI semgrep version) still holds. Worth narrowing when tooling allows.
  • Version strings moved 1.32.6-beta.31.32.6 via the master release merge; no functional change.

Verdict: PASS

@RaghavsBrowserStack
RaghavsBrowserStack merged commit 8311242 into masterAug 10, 2026
48 checks passed
@RaghavsBrowserStack
RaghavsBrowserStack deleted the PPLT-5844 branch August 10, 2026 12:57
RaghavsBrowserStack added a commit that referenced this pull request Aug 11, 2026
* bump version
* remove stray archive test fixtures committed by mistake
packages/core/.test-archive-mixed and .test-archive-symlink were leftovers
from a local unit-test run that got picked up by the version-bump commit.
.test-archive-symlink/linked.json is an absolute symlink into a developer's
home directory, so it dangles on a fresh CI checkout and makes
`babel packages -d build` (build_cjs) fail with ENOENT, breaking the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix unwanted commits
* remove stray archive test fixtures reintroduced by the master merge
d302b09 deleted these on this branch, but master picked up the same
accidental add in d663543 (bump version, #2377), so merging master back in
(2405903) restored them and left PR #2378 with an empty diff.
packages/core/.test-archive-symlink/linked.json is an absolute symlink into a
developer's home directory. It dangles on a fresh CI checkout, so
`babel packages -d build` (build_cjs) fails with ENOENT and takes down the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* regenerate yarn.lock so `lerna publish` sees a clean tree
The lockfile block added in 8311242 (IntelliStory, #2339) was edited by hand
rather than regenerated, leaving it inconsistent with the dependency graph:
- @types/node@* had no entry, though @types/cacheable-request, @types/keyv,
@types/responselike and @types/yauzl all require it
- get-stream@^5.1.0 had no entry, though cacheable-request@^7.0.2 requires it
- @types/yauzl@^2.9.1 was orphaned — nothing referenced it
So `yarn` on a clean CI checkout resolved the two missing descriptors, pruned
the orphan and rewrote the file. That left the working tree dirty, and
`lerna publish from-package` aborted with EUNCOMMIT (M yarn.lock), failing the
Release job.
Regenerated with a plain `yarn install`. `yarn install --frozen-lockfile` is
now a no-op, so the release checkout stays clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@RaghavsBrowserStack@prklm10@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
RaghavsBrowserStack merged 29 commits into
masterfrom
PPLT-5844
Aug 10, 2026
Merged

PPLT-5844: add IntelliStory storybook affected-story filtering#2339
RaghavsBrowserStack merged 29 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).
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/test/graphTrace.test.js Fixed
…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.
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
@RaghavsBrowserStackRaghavsBrowserStack changed the title feat(cli-command,client): add IntelliStory storybook affected-story filteringPPLT-5844: add IntelliStory storybook affected-story filteringJul 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 ownerJuly 15, 2026 14:29
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
RaghavsBrowserStackand others added 3 commits July 21, 2026 13:20
Add tests for the graceful-bail paths added in "apply suggested fixed":
- malformed stats-file JSON (validateAndReadStats)
- unreadable current lockfile (getAffectedPackages)
- unreadable package.json (getAffectedPackages)
These run on Node 14 (they bail before the Node>=18 snyk path), fixing the
lines/statements coverage-threshold failure on the Node 14 test job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GLOB_CHARS only detected * and ? so brace ({a,b}) and bracket ([abc])
patterns fell through to exact-match, silently disabling the intended
glob semantics for these documented config options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:001c036Reviewers: stack:code-reviewer

Summary

Adds IntelliStory — an affected-story graph feature for @percy/storybook: parses Storybook build stats, diffs against a git baseline (incl. lockfile/dependency diffing via optional snyk-nodejs-lockfile-parser), enqueues a server-side affected-story graph job, tags snapshots for selection, and optionally renders a debug trace.html. Plumbs intelliStory/storybookPath through @percy/client and @percy/core, and adds a Node 20 CI leg for the Node-18+-only snyk path.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed.
HighSecurityInput validation and sanitizationPassassertSafeRef blocks flag-injection; safeJson escapes HTML/JS breakouts; paths anchored via basename.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource-ownership surface.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1 ($-pattern .replace) still open. Finding 2 (glob detection gap) resolved in 001c036.
HighCorrectnessError handling is explicit, no swallowed exceptionsPass*Mostly graceful IntelliStoryBailError; one inconsistent rethrow — Finding 3.
HighCorrectnessNo race conditions or concurrency issuesPassNo new concurrency.
MediumTestingNew code has corresponding testsPassBroad coverage across new modules.
MediumTestingError paths and edge cases testedPass*Package-affected merge branch istanbul ignored rather than tested — Finding 5.
MediumTestingExisting tests still pass (no regressions)PassNot re-run here; CI covers.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo queries.
MediumPerformanceLong-running tasks use background jobsPassGraph job is server-side/async with timeout bail.
MediumQualityFollows existing codebase patternsPassConsistent with client/core plumbing conventions.
MediumQualityChanges are focused (single concern)PassScoped to IntelliStory feature.
LowQualityMeaningful names, no dead codePassClear naming.
LowQualityComments explain why, not whatPass.semgrepignore rationale well-documented.
LowQualityNo unnecessary dependencies addedPasssnyk-nodejs-lockfile-parser is optional/lazy-loaded.

Findings

  • File:packages/cli-command/src/graphTrace.js:114-116

  • Severity: Medium

  • Reviewer: stack:code-reviewer (verified)

  • Issue:.replace('__VERTICES_JSON__', safeJson(...)) uses plain-string replacement values. JS honors $$, $&, $`, $', $n special patterns in the replacement string even when the search arg is a literal. Embedded JSON contains arbitrary file paths; a path with $' re-injects the rest of the template, breaking the embedded JS. safeJson's escaping doesn't cover this — the corruption happens in the .replace call itself.

  • Suggestion: Pass a function as the replacement: .replace('__VERTICES_JSON__', () => safeJson(...)). Add a graphTrace.test.js case with $&/$'/$$ in a vertex name.

  • File:packages/cli-command/src/intelliStory.js:16, 34-41

  • Severity: Medium — ✅ RESOLVED in 001c036

  • Reviewer: stack:code-reviewer (verified)

  • Issue:GLOB_CHARS = /[*?]/ only routed */? patterns through the glob matcher, but patternToRegex enables {extended:true, globstar:true} (brace/bracket support). A configured bailOnChanges: ['src/{a,b}.js'] or bracket pattern silently fell through to literal str === pattern and never matched — silently disabling a documented config option (bailOnChanges/untraced).

  • Fix applied: Broadened detection to const GLOB_CHARS = /[*?{}[\]]/;. Added brace- and bracket-glob tests to assertNoBailOnChanges() and enforceUntraced() — verified failing under the old regex and passing under the fix (full 156-spec suite green).

  • File:packages/cli-command/src/intelliStory.js:326-336

  • Severity: Medium

  • Reviewer: stack:code-reviewer

  • Issue: Only SNYK_LOCKFILE_PARSER_UNAVAILABLE is converted to a graceful IntelliStoryBailError; a buildDepTree parse failure (malformed/older-ref lockfile) rethrows as a raw Error. Every other failure path degrades to "running full snapshot set."

  • Suggestion: Confirm the @percy/storybook caller treats allapplyIntelliStory throws as fall-back-to-full-set. If not, wrap this rethrow in IntelliStoryBailError for consistency.

  • File:packages/cli-command/src/intelliStory.js:67-70

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue:assertSafeRef regex correctly blocks leading -, but also rejects ordinary safe ref syntax like HEAD~5 / abc123^2.

  • Suggestion: Confirm intentional, or widen charset to include ~^ (still safe given the leading-char restriction).

  • File:packages/cli-command/src/intelliStory.js:438-440

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The packageAffectedNodes merge into affectedNodes (the actual mechanism folding lockfile-driven dep changes into the graph) is /* istanbul ignore next */ rather than covered by a real test.

  • Suggestion: Add an itPosix-style Node≥18 integration test exercising the merge end-to-end.

  • File:packages/cli-command/src/intelliStory.js (resolveAndIndex)

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Absolute module ids resolving outside projectRoot (symlinked pkg, monorepo sibling) produce ../-prefixed paths added straight into the files index without a guard.

  • Suggestion: Bail or drop entries where rel.startsWith('..').

  • File:.semgrepignore:118-131 / packages/client/src/client.js:2591-2592 / .github/workflows/test.yml

  • Severity: Info

  • Reviewer: stack:code-reviewer

  • Issue: (a) Whole-file semgrep suppression means future traversal code in the growing intelliStory.js goes unchecked. (b) client.intelliStoryStats is lazily-created, never reset — accumulates across builds if a client is reused. (c) New test-node20 CI job (workflow change) — flagged for visibility; mirror logic looks consistent, no action requested.

  • Suggestion: Consider narrowing semgrep scope over time; reset stats in createBuild.


Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:966c05dReviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), findings then re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a new static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

The bail-first posture is the right architecture for a feature that can silently drop visual coverage, and it is applied thoroughly — 15+ explicit bail sites covering missing build id, unreadable/invalid stats, git failures, ambiguous or absent lockfiles, manifest changes spanning directories, browser-upgrade builds, and graph-job timeouts. All four Medium findings below are gaps in that otherwise-sound safety net, not architectural problems.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through the existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync is used without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassExtensive and largely correct bail coverage. Four Medium gaps: findings 1–4.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps: the three API calls are not (finding 1), and dropped stats modules are logged at debug only (finding 4).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedPassBail branches are covered individually (commit e071c952 restored 100% coverage). The glob semantics in finding 3 are asserted deliberately by intelliStory.test.js:286-289.
MediumTestingExisting tests still pass (no regressions)PassAll CI checks green on 966c05d: Lint, Build, Build & verify executable, Regression, every package suite (incl. the new Node 20 leg), CodeQL, Semgrep OSS.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover. Minor: findings 5–7.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer (refined by orchestrator verification)
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or DNS failure therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); it is only thrown exceptions that leak.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph, and in pollGraphStatus treat a throw as a non-done status.

2. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (verified)
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail, and filtering proceeds as though nothing global changed. This is the under-snapshot direction: real visual regressions ship unnoticed.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

3. Bare-extension bailOnChanges globs silently match nothing below the repo root

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex), consumed at :251
  • Severity: Medium
  • Reviewer: stack:code-reviewer (severity downgraded from High by the orchestrator — see below)
  • Issue: With { extended: true, globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$ — verified locally — so it matches only root-level files, never src/components/Button.css. A user who configures bailOnChanges: ['*.css'] expecting "bail on any stylesheet change" gets no bail, and IntelliStory filters as if nothing unsafe changed. untraced shares the semantics but fails safe (nothing is dropped, so more snapshots are taken).
    Downgraded from the sub-reviewer's High because this is standard glob behaviorminimatch, picomatch, and GitHub Actions paths: filters all treat a bare *.ext as root-only — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. Still worth flagging: bailOnChanges is the one user-facing safety valve and the failure mode is silent.
  • Suggestion: Either document that patterns are path-segment-scoped and **/*.css is required (there is currently no documentation for bailOnChanges/untraced anywhere in the repo), or auto-broaden bare-name patterns before compiling: const p = pattern.includes('/') ? pattern : '**/' + pattern;. If the current semantics stay, rename the test to state the intent ('a bare-extension pattern does not match nested paths') so the behavior reads as deliberate.

4. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run still proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate the existing message to log.warn, and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

5. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue:resolveAndIndex runs only when copy.type === 'src'. Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the external enrichment tool ever emits a third type carrying an absolute path — an asset, CSS, or JSON import — that raw absolute path ships unnormalized in the modules payload: it leaks the developer's home or CI runner path, and it cannot be correlated against the project-relative affectedNodes, which is a false-negative path.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe and type-agnostic.

6. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (correction: rename detection is git's default since 2.9, so the inconsistency is present out of the box, not only under custom config)
  • Issue:affectedNodes comes from git diff --name-only with rename detection active, which collapses a rename to the new path alone; getAffectedFileLocations runs the same diff with --no-renames, which yields a delete + add pair. The two views of one diff can disagree for renamed files. Low practical impact — the old path no longer exists to be imported — but it makes the two graph inputs non-comparable.
  • Suggestion: Add --no-renames to the gitDiffNames invocation for determinism and parity with :84.

7. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer (verified)
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. This is opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or a mis-selection — often inside a locked-down or air-gapped CI environment where the CDN is unreachable and the trace renders blank.
  • Suggestion: Vendor the (small) Drawflow bundle into the package and inline it into the template.

8. Whole-file .semgrepignore suppression for intelliStory.js

  • File:.semgrepignore:49-57
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: The suppression is file-level rather than scoped to the specific path.join/path.resolve sites that trigger the false positive. intelliStory.js does a fair amount of fs/path work, so any genuinely unsafe join added later goes unscanned. The stated rationale (inline // nosemgrep not honored by the CI semgrep version) is legitimate and well documented.
  • Suggestion: Track a follow-up to narrow the suppression once the CI semgrep version honors inline annotations.

9. No bail is logged at the throw site

  • File:packages/cli-command/src/intelliStory.js — all IntelliStoryBailError throw sites
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: Every bail constructs a carefully worded message but none logs at warn/info before throwing, so whether a user ever learns that filtering did not apply depends entirely on the catch site in @percy/storybook — outside this repo and, per the PR body, still a follow-up. If that catcher logs at debug or swallows while falling back, the fail-safe is invisible.
  • Suggestion:log.warn(message) at the throw site (or inside a small bail(message, log) helper) so visibility does not depend on the external consumer.

Verified non-issues

  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced anywhere in cli-command — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:01aa1f6Reviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), second independent pass; all findings re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

Note on this run. Head moved from 966c05d to 01aa1f6, but both new commits (01aa1f6, f6ef573) are Merge branch 'master' into PPLT-5844git diff 966c05d 01aa1f6 over the PR-owned paths is empty, so the reviewable code is byte-identical to the previously reviewed head. The fast-path did not fire because the intermediate merge f6ef573 carries no prior Claude Code Review status, so a full review ran. This second, independent pass surfaced finding 1, which the first pass missed. The code has not regressed — the assessment has sharpened, and the verdict changes accordingly.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync runs without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1: bailOnChanges/untraced globs are matched against repo-root-relative paths with no rebasing, while story importPaths in the same function are rebased from the invocation directory — so the user's safety valve is silently inert for monorepo/subdirectory invocation.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps are Medium, not High: the three API calls are unwrapped (finding 2), malformed globs are swallowed to false (finding 4), and dropped stats modules are logged at debug only (finding 5).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedFailBail branches are covered individually, but every fixture builds a git repo with files at its root — there is no cwd ≠ projectRoot case, which is exactly why finding 1 went unnoticed. No test simulates a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)PassAll CI checks were green on 966c05d, whose PR-owned tree is identical to this head.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. bailOnChanges/untraced globs are never rebased to the invocation directory — the safety valve is silently inert in a monorepo

  • File:packages/cli-command/src/intelliStory.js:250-252, :259-262 (compare :415-426)

  • Severity: High

  • Reviewer: stack:code-reviewer (second pass); mechanism verified by the orchestrator

  • Issue:affectedNodes comes from git diff --name-only, which always emits repo-root-relative paths regardless of cwd. assertNoBailOnChanges and enforceUntraced match user-supplied globs directly against those paths, and glob-to-regexp fully anchors the result (^…$). Meanwhile, twenty lines further down, the same function builds invocationDir = process.cwd() and rebases each story's importPath from the invocation directory to project-relative (:415-426) — so the file demonstrably does expect cwd ≠ projectRoot. User globs get no such treatment.

    Concrete failure: a monorepo user runs cd packages/ui && percy storybook ./storybook-static with bailOnChanges: ['webpack.config.js']. A change to that file lands in affectedNodes as packages/ui/webpack.config.js; the pattern compiles to /^webpack\.config\.js$/ and does not match. No bail fires, no warning is logged, and IntelliStory filters as though nothing risky changed — the under-snapshot direction, where a genuine visual regression ships unreviewed. Monorepo-with-subdirectory is the mainstream Storybook layout, and bailOnChanges exists precisely as the escape hatch for changes the graph can't reason about.

    Not covered by any test: every fixture in intelliStory.test.js constructs a repo with files at its root, so no cwd ≠ projectRoot case is exercised.

  • Suggestion: Rebase user patterns the same way story paths already are, before matching:

    constrebase=g=>{if(path.isAbsolute(g))returnpath.relative(projectRoot,g).split(path.sep).join('/');constabs=path.resolve(invocationDir,g);constrel=path.relative(projectRoot,abs);returnrel.startsWith('..') ? g : rel.split(path.sep).join('/');};

    and pass bailOnChanges.map(rebase) / untraced.map(rebase) into the two functions. Add a test with cwd set to a nested package directory. If root-relative globs are the intended contract instead, that must be documented and a bail should fire when cwd !== projectRoot and patterns are configured — silently doing nothing is the one outcome to avoid.

2. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer — independently reported by both review passes
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or connection reset therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); only thrown exceptions leak. diffLockfileDeps (:316) has the same shape: it remaps only SNYK_LOCKFILE_PARSER_UNAVAILABLE and re-throws anything else raw.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status. Add tests where each client method rejects.

3. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail. Same under-snapshot direction as finding 1, and the two compound for monorepo users.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

4. A malformed glob is silently treated as "matches nothing"

  • File:packages/cli-command/src/intelliStory.js:34-43 (matchesPattern)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (second pass); verified — glob-to-regexp('src/[') throws Unterminated character class, 'src/{a' throws Unterminated group
  • Issue:try { … } catch { return false } means a typo'd pattern in a user's config compiles-fails and is then treated as never matching. For untraced that fails safe (the file stays traced). For bailOnChanges it fails open: the guardrail is silently disabled with nothing logged anywhere. The over-length guard at :22 has the same shape and is explicitly tested as non-throwing (intelliStory.test.js:269-271).
  • Suggestion:log.warn when a pattern fails to compile — a broken guardrail must be visible:
    }catch(e){log.warn(`IntelliStory: ignoring invalid pattern "${pattern}": ${e.message}`);returnfalse;}
    Better still, validate the patterns once at config-parse time rather than per-path.

5. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator (first pass)
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate to log.warn and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

6. Bare-extension globs don't match nested paths

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: With { globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior — minimatch, picomatch, and GitHub Actions paths: filters agree — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. It is listed because it lands on the same safety valve as findings 1 and 4, and there is currently no documentation for bailOnChanges/untraced anywhere in the repo.
  • Suggestion: Document that patterns are path-segment-scoped and **/*.css is required. If you prefer forgiving semantics, auto-broaden bare-name patterns: const p = pattern.includes('/') ? pattern : '**/' + pattern; — note this must be decided together with finding 1's rebasing.

7. noRequireBinding guard is narrower than the crash class it protects

  • File:packages/cli-command/test/noRequireBinding.test.js:40
  • Severity: Low
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:FORBIDDEN = /\b(?:const|let|var)\s+require\s*=\s*createRequire\b/ only catches a single-line declaration. The same startup crash returns via import { createRequire as require } from 'module', const { default: require } = …, or a line-wrapped const require =\n createRequire(…) — none of which match.
  • Suggestion: Flag any binding of the bare identifier require (import specifier, declaration LHS, or parameter), not just the createRequire-RHS form.

8. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the enrichment tool ever emits a third type carrying an absolute path, that path ships unnormalized: it leaks a local or CI-runner path and cannot be correlated against project-relative affectedNodes.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe.

9. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Rename detection is git's default since 2.9, so affectedNodes collapses a rename to the new path alone while getAffectedFileLocations (run with --no-renames) sees a delete + add pair. The two views of one diff disagree for renamed files.
  • Suggestion: Add --no-renames to gitDiffNames for parity.

10. path.relative does not case-fold when computing project-relative module ids

  • File:packages/cli-command/src/intelliStory.js:117-128 (resolveAndIndex)
  • Severity: Low (mechanism demonstrated, real-world trigger unconfirmed)
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:path.relative('/Users/bob/proj', '/Users/Bob/Proj/src/App.js') returns '../../Bob/Proj/src/App.js' — exact segment comparison, no case folding, on every platform. On case-insensitive-but-preserving filesystems (APFS, NTFS), any casing divergence between the bundler's absolute module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that can never match a real repo-relative path. The reviewer could not force an actual divergence, so this is flagged as plausible rather than confirmed.
  • Suggestion: At minimum, warn when the computed relative path escapes projectRoot (starts with ..) rather than silently indexing it; optionally normalize both sides through fs.realpathSync.

11. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer — reported by both passes
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. Opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or mis-selection — often inside a locked-down or air-gapped CI runner where it renders blank.
  • Suggestion: Vendor the small Drawflow bundle and inline it into the template.

12. Minor: bail visibility, semgrep scope, stats-shape logging

  • Severity: Low
  • Reviewer: stack:code-reviewer (both passes)
  • Issue: (a) No bail logs at the throw site, so user visibility depends entirely on the @percy/storybook catch — outside this repo and still a follow-up per the PR body. (b) .semgrepignore:49-57 suppresses intelliStory.js file-wide rather than at the flagged joins, so future unsafe joins in this fs/path-heavy file go unscanned; the rationale is documented and the constraint (inline // nosemgrep unsupported by the CI version) is real. (c) readStats's stats.modules || [] (:161) makes a stats file with no modules key indistinguishable from a legitimately empty one.
  • Suggestion: (a) log.warn(message) inside a small bail(message, log) helper. (b) Track narrowing the suppression. (c) log.debug when the key is absent versus empty.

Informational — not counted as defects

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested at lockfileDiff.test.js:85): Storybook addons are typically devDependencies and are imported directly by preview.js/story files. A pure addon version bump with no source diff will not surface through this path. It is explicit and tested, so likely intentional (relying on the server-side module graph instead) — worth a sanity check with the team given how conservative this feature aims to be.
  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: FAIL

Comment threadpackages/cli-command/test/intelliStory.test.js Fixed
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:91bd770Reviewers:stack:code-reviewer (prior passes), fix verification by the orchestrator against source at this head

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (cjsRequire) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

This head resolves the High finding from the 01aa1f6 review along with three of the five Mediums. Verdict flips to PASS.

Fix verification (vs. 01aa1f6)

#FindingSevStatus at 91bd770
1bailOnChanges/untraced globs not rebased to the invocation dirHighFixed
2Client calls throw raw Error, not IntelliStoryBailErrorMediumOpen
3.storybook config dir hardcodedMediumFixed
4Malformed glob silently swallowed to "no match"MediumFixed
5Dropped stats modules logged at debug, no thresholdMediumFixed
6Bare-extension globs don't match nested pathsMediumOpen (by design; still undocumented)
7noRequireBinding regex narrower than the crash classLowOpen
8mapEntry normalizes source only for type === 'src'LowOpen
9gitDiffNames omits --no-renamesLowOpen
10path.relative does not case-foldLowOpen (was unconfirmed)
11trace.html loads Drawflow from a CDNLowOpen
12No log.warn at bail throw sitesLowPartially addressed

Finding 1 is fixed properly, not papered over. resolvePattern (intelliStory.js:88-100) rebases each user pattern from the invocation directory onto the repo root — the same basis normalizeImportPath already used for story paths, so the asymmetry that caused the bug is gone. Beyond that, the fix handles the two readings asymmetrically by direction of harm, which is the right call:

  • assertNoBailOnChanges (:394-425) bails when a pattern matches only under the repo-root reading, logging a warning with a concrete rewrite suggestion — a stale root-relative config still protects the user.
  • enforceUntraced (:433-462) declines to apply such a pattern and warns, since applying it would drop snapshots.

A <rootDir>/ prefix is an explicit opt-out, patterns resolving outside the repo bail rather than matching nothing, and resolveConfigDirs (:361-390) additionally checks that a configured directory actually exists — catching a wrong-basis configDir at config time instead of silently never matching. Findings 3, 4 and 5 are each fixed with matching care: isInsideDirs distinguishes single-segment (any depth) from multi-segment (prefix) config dirs so config/storybook can't match config/storybook-old; patternError validates globs up front and routes a compile failure to a bail for bailOnChanges and a warn-skip for untraced; and readStats now separates deliberately-excluded modules from unresolved ones, warns on the latter and bails past a 10% ratio of resolvable candidates — so a stats file heavy in node_modules no longer trips the threshold. intelliStory.test.js grows 364 lines with cwd ≠ projectRoot fixtures (projectRoot: '/repo', invocationDir: '/repo/packages/ui') and a resolveConfigDirs() suite — the gap that let finding 1 through is now covered.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth flows through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before spawnSync; statsFile narrowed to a bare .json basename; spawnSync runs without a shell. User globs are now validated up front via patternError. Dedicated security lens is disabled in this orchestrator.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassThe path-basis defect is fixed, with both readings handled by direction of harm. Remaining gaps are Medium/Low.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassGlob compile failures are no longer swallowed, and incomplete graphs now bail past a threshold. Finding 2 (unwrapped client calls) remains, at Medium.
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop; no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass+364 test lines this push, covering resolvePattern, resolveConfigDirs, and the <rootDir>/ opt-out.
MediumTestingError paths and edge cases testedPassThe cwd ≠ projectRoot gap is closed. Still untested: a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)Pass46 checks green, incl. Lint, Build & verify executable, Regression, all cli-command suites and the Node 20 leg. Test @percy/core still pending at review time — unrelated to the changed packages.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne GET, one POST, bounded poll (12 × 5s). In-memory stats parsing.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled.
MediumQualityFollows existing codebase patternsPassMatches PercyClient conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on and the CI leg that exercises the lockfile path.
LowQualityMeaningful names, no dead codePassstream-json confirmed absent. New helpers (resolvePattern, patternError, rootReadingHit, suggestRewrite) are well named and each has a stated reason to exist.
LowQualityComments explain why, not whatPassThe new comments are unusually good — they record the direction of harm reasoning behind each asymmetry ("recognising one directory too many only costs snapshots, while missing the real one loses comparisons"), which is exactly what a future maintainer needs.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp used; snyk-nodejs-lockfile-parser correctly optional with a Node 20 CI leg.

Findings

2. API/network failures still escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:292, :331, :555
  • Severity: Medium
  • Reviewer: stack:code-reviewer (reported independently by both prior passes); re-verified unfixed at this head
  • Issue: Verified still open: getStatus('intelli_story_graph', …) in pollGraphStatus (:292), getIntelliStorySnapshotNameToCommit (:331), and generateIntelliStoryGraph (:555) are all bare awaits with no surrounding try/catch. Every other failure mode in this file — git, fs, config, lockfile, malformed glob, degraded graph — is deliberately converted to IntelliStoryBailError so the caller can degrade to a full snapshot run. A transient 5xx, timeout, or connection reset from these three still propagates as a plain Error, so if @percy/storybook catches IntelliStoryBailError specifically, an API blip hard-fails the storybook command instead of falling back. The graph-job timeout path is handled correctly (:561); only thrown exceptions leak. Not a merge blocker under the verdict rule, but it is now the last inconsistency in an otherwise uniformly fail-safe module.
  • Suggestion:
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status so the existing :561 bail covers it. Add tests where each client method rejects.

6. Bare-extension globs still don't match nested paths, and the contract is undocumented

  • File:packages/cli-command/src/intelliStory.js:28
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass)
  • Issue:glob-to-regexp('*.css', { globstar: true }) compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior (minimatch, picomatch, GitHub Actions paths: all agree) and is deliberately asserted by the tests, so it is not a defect. It is still listed because the new rootReadingHit warning covers the basis mistake but not the depth mistake — a user who writes *.css gets no warning at all — and there remains no documentation for bailOnChanges/untraced/configDir anywhere in the repo.
  • Suggestion: Document the semantics now that the basis rules are non-trivial (invocation-relative, <rootDir>/ opt-out, path-segment-scoped globs). Optionally extend the existing warning: if a pattern has no / and no **, and some affected node matches **/<pattern>, warn the same way the root-reading case does.

7–11. Carried-over Lows, unchanged at this head

  • noRequireBinding.test.js:40FORBIDDEN still only matches a single-line const/let/var require = createRequire. The same crash returns via import { createRequire as require } from 'module' or a line-wrapped declaration. Broaden to flag any binding of the bare require identifier.
  • intelliStory.js:229mapEntry still gates normalization on copy.type === 'src'. Drop the gate; resolveAndIndex already passes non-absolute values through untouched.
  • intelliStory.js:138gitDiffNames still omits --no-renames while getAffectedFileLocations uses it, so the two views of one diff disagree for renamed files.
  • intelliStory.jsresolveAndIndexpath.relative does not case-fold, so a casing divergence between bundler module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that never match. Mechanism is real, real-world trigger still unconfirmed. Cheapest guard: warn when the computed relative path starts with ...
  • graphTraceTemplate.html:8, :12trace.html still loads Drawflow from cdn.jsdelivr.net, so the diagnostic renders blank in an air-gapped or locked-down CI runner. Vendor the bundle.

12. Bail visibility — partially addressed

  • Severity: Low
  • Issue: This push adds ten log.warn calls, so pattern and configDir problems are now visible. The IntelliStoryBailError throw sites themselves still don't log before throwing, so whether a user learns that filtering did not apply continues to depend on the @percy/storybook catch site — outside this repo and still a follow-up per the PR body.
  • Suggestion: A small bail(message, log) helper that warns and throws.

Informational

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested): Storybook addons are usually devDependencies imported directly by preview.js/story files, so a pure addon bump with no source diff won't surface here. Explicit and tested, so presumably intentional — worth a sanity check with the team.
  • .semgrepignore still suppresses intelliStory.js file-wide rather than at the flagged joins. The file grew ~290 lines this push, which widens the unscanned surface; the documented rationale (inline // nosemgrep unsupported by the CI semgrep version) still holds. Worth narrowing when tooling allows.
  • Version strings moved 1.32.6-beta.31.32.6 via the master release merge; no functional change.

Verdict: PASS

@RaghavsBrowserStack
RaghavsBrowserStack merged commit 8311242 into masterAug 10, 2026
48 checks passed
@RaghavsBrowserStack
RaghavsBrowserStack deleted the PPLT-5844 branch August 10, 2026 12:57
RaghavsBrowserStack added a commit that referenced this pull request Aug 11, 2026
* bump version
* remove stray archive test fixtures committed by mistake
packages/core/.test-archive-mixed and .test-archive-symlink were leftovers
from a local unit-test run that got picked up by the version-bump commit.
.test-archive-symlink/linked.json is an absolute symlink into a developer's
home directory, so it dangles on a fresh CI checkout and makes
`babel packages -d build` (build_cjs) fail with ENOENT, breaking the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix unwanted commits
* remove stray archive test fixtures reintroduced by the master merge
d302b09 deleted these on this branch, but master picked up the same
accidental add in d663543 (bump version, #2377), so merging master back in
(2405903) restored them and left PR #2378 with an empty diff.
packages/core/.test-archive-symlink/linked.json is an absolute symlink into a
developer's home directory. It dangles on a fresh CI checkout, so
`babel packages -d build` (build_cjs) fails with ENOENT and takes down the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* regenerate yarn.lock so `lerna publish` sees a clean tree
The lockfile block added in 8311242 (IntelliStory, #2339) was edited by hand
rather than regenerated, leaving it inconsistent with the dependency graph:
- @types/node@* had no entry, though @types/cacheable-request, @types/keyv,
@types/responselike and @types/yauzl all require it
- get-stream@^5.1.0 had no entry, though cacheable-request@^7.0.2 requires it
- @types/yauzl@^2.9.1 was orphaned — nothing referenced it
So `yarn` on a clean CI checkout resolved the two missing descriptors, pruned
the orphan and rewrote the file. That left the working tree dirty, and
`lerna publish from-package` aborted with EUNCOMMIT (M yarn.lock), failing the
Release job.
Regenerated with a plain `yarn install`. `yarn install --frozen-lockfile` is
now a no-op, so the release checkout stays clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@RaghavsBrowserStack@prklm10@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Merged
RaghavsBrowserStack merged 29 commits into
masterfrom
PPLT-5844
Aug 10, 2026
Merged

PPLT-5844: add IntelliStory storybook affected-story filtering#2339
RaghavsBrowserStack merged 29 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).
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/test/graphTrace.test.js Fixed
…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.
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
@RaghavsBrowserStackRaghavsBrowserStack changed the title feat(cli-command,client): add IntelliStory storybook affected-story filteringPPLT-5844: add IntelliStory storybook affected-story filteringJul 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 ownerJuly 15, 2026 14:29
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
RaghavsBrowserStackand others added 3 commits July 21, 2026 13:20
Add tests for the graceful-bail paths added in "apply suggested fixed":
- malformed stats-file JSON (validateAndReadStats)
- unreadable current lockfile (getAffectedPackages)
- unreadable package.json (getAffectedPackages)
These run on Node 14 (they bail before the Node>=18 snyk path), fixing the
lines/statements coverage-threshold failure on the Node 14 test job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GLOB_CHARS only detected * and ? so brace ({a,b}) and bracket ([abc])
patterns fell through to exact-match, silently disabling the intended
glob semantics for these documented config options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:001c036Reviewers: stack:code-reviewer

Summary

Adds IntelliStory — an affected-story graph feature for @percy/storybook: parses Storybook build stats, diffs against a git baseline (incl. lockfile/dependency diffing via optional snyk-nodejs-lockfile-parser), enqueues a server-side affected-story graph job, tags snapshots for selection, and optionally renders a debug trace.html. Plumbs intelliStory/storybookPath through @percy/client and @percy/core, and adds a Node 20 CI leg for the Node-18+-only snyk path.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed.
HighSecurityInput validation and sanitizationPassassertSafeRef blocks flag-injection; safeJson escapes HTML/JS breakouts; paths anchored via basename.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource-ownership surface.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1 ($-pattern .replace) still open. Finding 2 (glob detection gap) resolved in 001c036.
HighCorrectnessError handling is explicit, no swallowed exceptionsPass*Mostly graceful IntelliStoryBailError; one inconsistent rethrow — Finding 3.
HighCorrectnessNo race conditions or concurrency issuesPassNo new concurrency.
MediumTestingNew code has corresponding testsPassBroad coverage across new modules.
MediumTestingError paths and edge cases testedPass*Package-affected merge branch istanbul ignored rather than tested — Finding 5.
MediumTestingExisting tests still pass (no regressions)PassNot re-run here; CI covers.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo queries.
MediumPerformanceLong-running tasks use background jobsPassGraph job is server-side/async with timeout bail.
MediumQualityFollows existing codebase patternsPassConsistent with client/core plumbing conventions.
MediumQualityChanges are focused (single concern)PassScoped to IntelliStory feature.
LowQualityMeaningful names, no dead codePassClear naming.
LowQualityComments explain why, not whatPass.semgrepignore rationale well-documented.
LowQualityNo unnecessary dependencies addedPasssnyk-nodejs-lockfile-parser is optional/lazy-loaded.

Findings

  • File:packages/cli-command/src/graphTrace.js:114-116

  • Severity: Medium

  • Reviewer: stack:code-reviewer (verified)

  • Issue:.replace('__VERTICES_JSON__', safeJson(...)) uses plain-string replacement values. JS honors $$, $&, $`, $', $n special patterns in the replacement string even when the search arg is a literal. Embedded JSON contains arbitrary file paths; a path with $' re-injects the rest of the template, breaking the embedded JS. safeJson's escaping doesn't cover this — the corruption happens in the .replace call itself.

  • Suggestion: Pass a function as the replacement: .replace('__VERTICES_JSON__', () => safeJson(...)). Add a graphTrace.test.js case with $&/$'/$$ in a vertex name.

  • File:packages/cli-command/src/intelliStory.js:16, 34-41

  • Severity: Medium — ✅ RESOLVED in 001c036

  • Reviewer: stack:code-reviewer (verified)

  • Issue:GLOB_CHARS = /[*?]/ only routed */? patterns through the glob matcher, but patternToRegex enables {extended:true, globstar:true} (brace/bracket support). A configured bailOnChanges: ['src/{a,b}.js'] or bracket pattern silently fell through to literal str === pattern and never matched — silently disabling a documented config option (bailOnChanges/untraced).

  • Fix applied: Broadened detection to const GLOB_CHARS = /[*?{}[\]]/;. Added brace- and bracket-glob tests to assertNoBailOnChanges() and enforceUntraced() — verified failing under the old regex and passing under the fix (full 156-spec suite green).

  • File:packages/cli-command/src/intelliStory.js:326-336

  • Severity: Medium

  • Reviewer: stack:code-reviewer

  • Issue: Only SNYK_LOCKFILE_PARSER_UNAVAILABLE is converted to a graceful IntelliStoryBailError; a buildDepTree parse failure (malformed/older-ref lockfile) rethrows as a raw Error. Every other failure path degrades to "running full snapshot set."

  • Suggestion: Confirm the @percy/storybook caller treats allapplyIntelliStory throws as fall-back-to-full-set. If not, wrap this rethrow in IntelliStoryBailError for consistency.

  • File:packages/cli-command/src/intelliStory.js:67-70

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue:assertSafeRef regex correctly blocks leading -, but also rejects ordinary safe ref syntax like HEAD~5 / abc123^2.

  • Suggestion: Confirm intentional, or widen charset to include ~^ (still safe given the leading-char restriction).

  • File:packages/cli-command/src/intelliStory.js:438-440

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The packageAffectedNodes merge into affectedNodes (the actual mechanism folding lockfile-driven dep changes into the graph) is /* istanbul ignore next */ rather than covered by a real test.

  • Suggestion: Add an itPosix-style Node≥18 integration test exercising the merge end-to-end.

  • File:packages/cli-command/src/intelliStory.js (resolveAndIndex)

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Absolute module ids resolving outside projectRoot (symlinked pkg, monorepo sibling) produce ../-prefixed paths added straight into the files index without a guard.

  • Suggestion: Bail or drop entries where rel.startsWith('..').

  • File:.semgrepignore:118-131 / packages/client/src/client.js:2591-2592 / .github/workflows/test.yml

  • Severity: Info

  • Reviewer: stack:code-reviewer

  • Issue: (a) Whole-file semgrep suppression means future traversal code in the growing intelliStory.js goes unchecked. (b) client.intelliStoryStats is lazily-created, never reset — accumulates across builds if a client is reused. (c) New test-node20 CI job (workflow change) — flagged for visibility; mirror logic looks consistent, no action requested.

  • Suggestion: Consider narrowing semgrep scope over time; reset stats in createBuild.


Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:966c05dReviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), findings then re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a new static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

The bail-first posture is the right architecture for a feature that can silently drop visual coverage, and it is applied thoroughly — 15+ explicit bail sites covering missing build id, unreadable/invalid stats, git failures, ambiguous or absent lockfiles, manifest changes spanning directories, browser-upgrade builds, and graph-job timeouts. All four Medium findings below are gaps in that otherwise-sound safety net, not architectural problems.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through the existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync is used without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassExtensive and largely correct bail coverage. Four Medium gaps: findings 1–4.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps: the three API calls are not (finding 1), and dropped stats modules are logged at debug only (finding 4).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedPassBail branches are covered individually (commit e071c952 restored 100% coverage). The glob semantics in finding 3 are asserted deliberately by intelliStory.test.js:286-289.
MediumTestingExisting tests still pass (no regressions)PassAll CI checks green on 966c05d: Lint, Build, Build & verify executable, Regression, every package suite (incl. the new Node 20 leg), CodeQL, Semgrep OSS.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover. Minor: findings 5–7.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer (refined by orchestrator verification)
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or DNS failure therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); it is only thrown exceptions that leak.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph, and in pollGraphStatus treat a throw as a non-done status.

2. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (verified)
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail, and filtering proceeds as though nothing global changed. This is the under-snapshot direction: real visual regressions ship unnoticed.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

3. Bare-extension bailOnChanges globs silently match nothing below the repo root

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex), consumed at :251
  • Severity: Medium
  • Reviewer: stack:code-reviewer (severity downgraded from High by the orchestrator — see below)
  • Issue: With { extended: true, globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$ — verified locally — so it matches only root-level files, never src/components/Button.css. A user who configures bailOnChanges: ['*.css'] expecting "bail on any stylesheet change" gets no bail, and IntelliStory filters as if nothing unsafe changed. untraced shares the semantics but fails safe (nothing is dropped, so more snapshots are taken).
    Downgraded from the sub-reviewer's High because this is standard glob behaviorminimatch, picomatch, and GitHub Actions paths: filters all treat a bare *.ext as root-only — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. Still worth flagging: bailOnChanges is the one user-facing safety valve and the failure mode is silent.
  • Suggestion: Either document that patterns are path-segment-scoped and **/*.css is required (there is currently no documentation for bailOnChanges/untraced anywhere in the repo), or auto-broaden bare-name patterns before compiling: const p = pattern.includes('/') ? pattern : '**/' + pattern;. If the current semantics stay, rename the test to state the intent ('a bare-extension pattern does not match nested paths') so the behavior reads as deliberate.

4. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run still proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate the existing message to log.warn, and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

5. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue:resolveAndIndex runs only when copy.type === 'src'. Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the external enrichment tool ever emits a third type carrying an absolute path — an asset, CSS, or JSON import — that raw absolute path ships unnormalized in the modules payload: it leaks the developer's home or CI runner path, and it cannot be correlated against the project-relative affectedNodes, which is a false-negative path.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe and type-agnostic.

6. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (correction: rename detection is git's default since 2.9, so the inconsistency is present out of the box, not only under custom config)
  • Issue:affectedNodes comes from git diff --name-only with rename detection active, which collapses a rename to the new path alone; getAffectedFileLocations runs the same diff with --no-renames, which yields a delete + add pair. The two views of one diff can disagree for renamed files. Low practical impact — the old path no longer exists to be imported — but it makes the two graph inputs non-comparable.
  • Suggestion: Add --no-renames to the gitDiffNames invocation for determinism and parity with :84.

7. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer (verified)
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. This is opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or a mis-selection — often inside a locked-down or air-gapped CI environment where the CDN is unreachable and the trace renders blank.
  • Suggestion: Vendor the (small) Drawflow bundle into the package and inline it into the template.

8. Whole-file .semgrepignore suppression for intelliStory.js

  • File:.semgrepignore:49-57
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: The suppression is file-level rather than scoped to the specific path.join/path.resolve sites that trigger the false positive. intelliStory.js does a fair amount of fs/path work, so any genuinely unsafe join added later goes unscanned. The stated rationale (inline // nosemgrep not honored by the CI semgrep version) is legitimate and well documented.
  • Suggestion: Track a follow-up to narrow the suppression once the CI semgrep version honors inline annotations.

9. No bail is logged at the throw site

  • File:packages/cli-command/src/intelliStory.js — all IntelliStoryBailError throw sites
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: Every bail constructs a carefully worded message but none logs at warn/info before throwing, so whether a user ever learns that filtering did not apply depends entirely on the catch site in @percy/storybook — outside this repo and, per the PR body, still a follow-up. If that catcher logs at debug or swallows while falling back, the fail-safe is invisible.
  • Suggestion:log.warn(message) at the throw site (or inside a small bail(message, log) helper) so visibility does not depend on the external consumer.

Verified non-issues

  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced anywhere in cli-command — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:01aa1f6Reviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), second independent pass; all findings re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

Note on this run. Head moved from 966c05d to 01aa1f6, but both new commits (01aa1f6, f6ef573) are Merge branch 'master' into PPLT-5844git diff 966c05d 01aa1f6 over the PR-owned paths is empty, so the reviewable code is byte-identical to the previously reviewed head. The fast-path did not fire because the intermediate merge f6ef573 carries no prior Claude Code Review status, so a full review ran. This second, independent pass surfaced finding 1, which the first pass missed. The code has not regressed — the assessment has sharpened, and the verdict changes accordingly.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync runs without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1: bailOnChanges/untraced globs are matched against repo-root-relative paths with no rebasing, while story importPaths in the same function are rebased from the invocation directory — so the user's safety valve is silently inert for monorepo/subdirectory invocation.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps are Medium, not High: the three API calls are unwrapped (finding 2), malformed globs are swallowed to false (finding 4), and dropped stats modules are logged at debug only (finding 5).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedFailBail branches are covered individually, but every fixture builds a git repo with files at its root — there is no cwd ≠ projectRoot case, which is exactly why finding 1 went unnoticed. No test simulates a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)PassAll CI checks were green on 966c05d, whose PR-owned tree is identical to this head.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. bailOnChanges/untraced globs are never rebased to the invocation directory — the safety valve is silently inert in a monorepo

  • File:packages/cli-command/src/intelliStory.js:250-252, :259-262 (compare :415-426)

  • Severity: High

  • Reviewer: stack:code-reviewer (second pass); mechanism verified by the orchestrator

  • Issue:affectedNodes comes from git diff --name-only, which always emits repo-root-relative paths regardless of cwd. assertNoBailOnChanges and enforceUntraced match user-supplied globs directly against those paths, and glob-to-regexp fully anchors the result (^…$). Meanwhile, twenty lines further down, the same function builds invocationDir = process.cwd() and rebases each story's importPath from the invocation directory to project-relative (:415-426) — so the file demonstrably does expect cwd ≠ projectRoot. User globs get no such treatment.

    Concrete failure: a monorepo user runs cd packages/ui && percy storybook ./storybook-static with bailOnChanges: ['webpack.config.js']. A change to that file lands in affectedNodes as packages/ui/webpack.config.js; the pattern compiles to /^webpack\.config\.js$/ and does not match. No bail fires, no warning is logged, and IntelliStory filters as though nothing risky changed — the under-snapshot direction, where a genuine visual regression ships unreviewed. Monorepo-with-subdirectory is the mainstream Storybook layout, and bailOnChanges exists precisely as the escape hatch for changes the graph can't reason about.

    Not covered by any test: every fixture in intelliStory.test.js constructs a repo with files at its root, so no cwd ≠ projectRoot case is exercised.

  • Suggestion: Rebase user patterns the same way story paths already are, before matching:

    constrebase=g=>{if(path.isAbsolute(g))returnpath.relative(projectRoot,g).split(path.sep).join('/');constabs=path.resolve(invocationDir,g);constrel=path.relative(projectRoot,abs);returnrel.startsWith('..') ? g : rel.split(path.sep).join('/');};

    and pass bailOnChanges.map(rebase) / untraced.map(rebase) into the two functions. Add a test with cwd set to a nested package directory. If root-relative globs are the intended contract instead, that must be documented and a bail should fire when cwd !== projectRoot and patterns are configured — silently doing nothing is the one outcome to avoid.

2. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer — independently reported by both review passes
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or connection reset therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); only thrown exceptions leak. diffLockfileDeps (:316) has the same shape: it remaps only SNYK_LOCKFILE_PARSER_UNAVAILABLE and re-throws anything else raw.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status. Add tests where each client method rejects.

3. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail. Same under-snapshot direction as finding 1, and the two compound for monorepo users.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

4. A malformed glob is silently treated as "matches nothing"

  • File:packages/cli-command/src/intelliStory.js:34-43 (matchesPattern)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (second pass); verified — glob-to-regexp('src/[') throws Unterminated character class, 'src/{a' throws Unterminated group
  • Issue:try { … } catch { return false } means a typo'd pattern in a user's config compiles-fails and is then treated as never matching. For untraced that fails safe (the file stays traced). For bailOnChanges it fails open: the guardrail is silently disabled with nothing logged anywhere. The over-length guard at :22 has the same shape and is explicitly tested as non-throwing (intelliStory.test.js:269-271).
  • Suggestion:log.warn when a pattern fails to compile — a broken guardrail must be visible:
    }catch(e){log.warn(`IntelliStory: ignoring invalid pattern "${pattern}": ${e.message}`);returnfalse;}
    Better still, validate the patterns once at config-parse time rather than per-path.

5. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator (first pass)
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate to log.warn and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

6. Bare-extension globs don't match nested paths

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: With { globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior — minimatch, picomatch, and GitHub Actions paths: filters agree — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. It is listed because it lands on the same safety valve as findings 1 and 4, and there is currently no documentation for bailOnChanges/untraced anywhere in the repo.
  • Suggestion: Document that patterns are path-segment-scoped and **/*.css is required. If you prefer forgiving semantics, auto-broaden bare-name patterns: const p = pattern.includes('/') ? pattern : '**/' + pattern; — note this must be decided together with finding 1's rebasing.

7. noRequireBinding guard is narrower than the crash class it protects

  • File:packages/cli-command/test/noRequireBinding.test.js:40
  • Severity: Low
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:FORBIDDEN = /\b(?:const|let|var)\s+require\s*=\s*createRequire\b/ only catches a single-line declaration. The same startup crash returns via import { createRequire as require } from 'module', const { default: require } = …, or a line-wrapped const require =\n createRequire(…) — none of which match.
  • Suggestion: Flag any binding of the bare identifier require (import specifier, declaration LHS, or parameter), not just the createRequire-RHS form.

8. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the enrichment tool ever emits a third type carrying an absolute path, that path ships unnormalized: it leaks a local or CI-runner path and cannot be correlated against project-relative affectedNodes.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe.

9. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Rename detection is git's default since 2.9, so affectedNodes collapses a rename to the new path alone while getAffectedFileLocations (run with --no-renames) sees a delete + add pair. The two views of one diff disagree for renamed files.
  • Suggestion: Add --no-renames to gitDiffNames for parity.

10. path.relative does not case-fold when computing project-relative module ids

  • File:packages/cli-command/src/intelliStory.js:117-128 (resolveAndIndex)
  • Severity: Low (mechanism demonstrated, real-world trigger unconfirmed)
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:path.relative('/Users/bob/proj', '/Users/Bob/Proj/src/App.js') returns '../../Bob/Proj/src/App.js' — exact segment comparison, no case folding, on every platform. On case-insensitive-but-preserving filesystems (APFS, NTFS), any casing divergence between the bundler's absolute module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that can never match a real repo-relative path. The reviewer could not force an actual divergence, so this is flagged as plausible rather than confirmed.
  • Suggestion: At minimum, warn when the computed relative path escapes projectRoot (starts with ..) rather than silently indexing it; optionally normalize both sides through fs.realpathSync.

11. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer — reported by both passes
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. Opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or mis-selection — often inside a locked-down or air-gapped CI runner where it renders blank.
  • Suggestion: Vendor the small Drawflow bundle and inline it into the template.

12. Minor: bail visibility, semgrep scope, stats-shape logging

  • Severity: Low
  • Reviewer: stack:code-reviewer (both passes)
  • Issue: (a) No bail logs at the throw site, so user visibility depends entirely on the @percy/storybook catch — outside this repo and still a follow-up per the PR body. (b) .semgrepignore:49-57 suppresses intelliStory.js file-wide rather than at the flagged joins, so future unsafe joins in this fs/path-heavy file go unscanned; the rationale is documented and the constraint (inline // nosemgrep unsupported by the CI version) is real. (c) readStats's stats.modules || [] (:161) makes a stats file with no modules key indistinguishable from a legitimately empty one.
  • Suggestion: (a) log.warn(message) inside a small bail(message, log) helper. (b) Track narrowing the suppression. (c) log.debug when the key is absent versus empty.

Informational — not counted as defects

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested at lockfileDiff.test.js:85): Storybook addons are typically devDependencies and are imported directly by preview.js/story files. A pure addon version bump with no source diff will not surface through this path. It is explicit and tested, so likely intentional (relying on the server-side module graph instead) — worth a sanity check with the team given how conservative this feature aims to be.
  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: FAIL

Comment threadpackages/cli-command/test/intelliStory.test.js Fixed
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:91bd770Reviewers:stack:code-reviewer (prior passes), fix verification by the orchestrator against source at this head

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (cjsRequire) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

This head resolves the High finding from the 01aa1f6 review along with three of the five Mediums. Verdict flips to PASS.

Fix verification (vs. 01aa1f6)

#FindingSevStatus at 91bd770
1bailOnChanges/untraced globs not rebased to the invocation dirHighFixed
2Client calls throw raw Error, not IntelliStoryBailErrorMediumOpen
3.storybook config dir hardcodedMediumFixed
4Malformed glob silently swallowed to "no match"MediumFixed
5Dropped stats modules logged at debug, no thresholdMediumFixed
6Bare-extension globs don't match nested pathsMediumOpen (by design; still undocumented)
7noRequireBinding regex narrower than the crash classLowOpen
8mapEntry normalizes source only for type === 'src'LowOpen
9gitDiffNames omits --no-renamesLowOpen
10path.relative does not case-foldLowOpen (was unconfirmed)
11trace.html loads Drawflow from a CDNLowOpen
12No log.warn at bail throw sitesLowPartially addressed

Finding 1 is fixed properly, not papered over. resolvePattern (intelliStory.js:88-100) rebases each user pattern from the invocation directory onto the repo root — the same basis normalizeImportPath already used for story paths, so the asymmetry that caused the bug is gone. Beyond that, the fix handles the two readings asymmetrically by direction of harm, which is the right call:

  • assertNoBailOnChanges (:394-425) bails when a pattern matches only under the repo-root reading, logging a warning with a concrete rewrite suggestion — a stale root-relative config still protects the user.
  • enforceUntraced (:433-462) declines to apply such a pattern and warns, since applying it would drop snapshots.

A <rootDir>/ prefix is an explicit opt-out, patterns resolving outside the repo bail rather than matching nothing, and resolveConfigDirs (:361-390) additionally checks that a configured directory actually exists — catching a wrong-basis configDir at config time instead of silently never matching. Findings 3, 4 and 5 are each fixed with matching care: isInsideDirs distinguishes single-segment (any depth) from multi-segment (prefix) config dirs so config/storybook can't match config/storybook-old; patternError validates globs up front and routes a compile failure to a bail for bailOnChanges and a warn-skip for untraced; and readStats now separates deliberately-excluded modules from unresolved ones, warns on the latter and bails past a 10% ratio of resolvable candidates — so a stats file heavy in node_modules no longer trips the threshold. intelliStory.test.js grows 364 lines with cwd ≠ projectRoot fixtures (projectRoot: '/repo', invocationDir: '/repo/packages/ui') and a resolveConfigDirs() suite — the gap that let finding 1 through is now covered.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth flows through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before spawnSync; statsFile narrowed to a bare .json basename; spawnSync runs without a shell. User globs are now validated up front via patternError. Dedicated security lens is disabled in this orchestrator.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassThe path-basis defect is fixed, with both readings handled by direction of harm. Remaining gaps are Medium/Low.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassGlob compile failures are no longer swallowed, and incomplete graphs now bail past a threshold. Finding 2 (unwrapped client calls) remains, at Medium.
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop; no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass+364 test lines this push, covering resolvePattern, resolveConfigDirs, and the <rootDir>/ opt-out.
MediumTestingError paths and edge cases testedPassThe cwd ≠ projectRoot gap is closed. Still untested: a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)Pass46 checks green, incl. Lint, Build & verify executable, Regression, all cli-command suites and the Node 20 leg. Test @percy/core still pending at review time — unrelated to the changed packages.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne GET, one POST, bounded poll (12 × 5s). In-memory stats parsing.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled.
MediumQualityFollows existing codebase patternsPassMatches PercyClient conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on and the CI leg that exercises the lockfile path.
LowQualityMeaningful names, no dead codePassstream-json confirmed absent. New helpers (resolvePattern, patternError, rootReadingHit, suggestRewrite) are well named and each has a stated reason to exist.
LowQualityComments explain why, not whatPassThe new comments are unusually good — they record the direction of harm reasoning behind each asymmetry ("recognising one directory too many only costs snapshots, while missing the real one loses comparisons"), which is exactly what a future maintainer needs.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp used; snyk-nodejs-lockfile-parser correctly optional with a Node 20 CI leg.

Findings

2. API/network failures still escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:292, :331, :555
  • Severity: Medium
  • Reviewer: stack:code-reviewer (reported independently by both prior passes); re-verified unfixed at this head
  • Issue: Verified still open: getStatus('intelli_story_graph', …) in pollGraphStatus (:292), getIntelliStorySnapshotNameToCommit (:331), and generateIntelliStoryGraph (:555) are all bare awaits with no surrounding try/catch. Every other failure mode in this file — git, fs, config, lockfile, malformed glob, degraded graph — is deliberately converted to IntelliStoryBailError so the caller can degrade to a full snapshot run. A transient 5xx, timeout, or connection reset from these three still propagates as a plain Error, so if @percy/storybook catches IntelliStoryBailError specifically, an API blip hard-fails the storybook command instead of falling back. The graph-job timeout path is handled correctly (:561); only thrown exceptions leak. Not a merge blocker under the verdict rule, but it is now the last inconsistency in an otherwise uniformly fail-safe module.
  • Suggestion:
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status so the existing :561 bail covers it. Add tests where each client method rejects.

6. Bare-extension globs still don't match nested paths, and the contract is undocumented

  • File:packages/cli-command/src/intelliStory.js:28
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass)
  • Issue:glob-to-regexp('*.css', { globstar: true }) compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior (minimatch, picomatch, GitHub Actions paths: all agree) and is deliberately asserted by the tests, so it is not a defect. It is still listed because the new rootReadingHit warning covers the basis mistake but not the depth mistake — a user who writes *.css gets no warning at all — and there remains no documentation for bailOnChanges/untraced/configDir anywhere in the repo.
  • Suggestion: Document the semantics now that the basis rules are non-trivial (invocation-relative, <rootDir>/ opt-out, path-segment-scoped globs). Optionally extend the existing warning: if a pattern has no / and no **, and some affected node matches **/<pattern>, warn the same way the root-reading case does.

7–11. Carried-over Lows, unchanged at this head

  • noRequireBinding.test.js:40FORBIDDEN still only matches a single-line const/let/var require = createRequire. The same crash returns via import { createRequire as require } from 'module' or a line-wrapped declaration. Broaden to flag any binding of the bare require identifier.
  • intelliStory.js:229mapEntry still gates normalization on copy.type === 'src'. Drop the gate; resolveAndIndex already passes non-absolute values through untouched.
  • intelliStory.js:138gitDiffNames still omits --no-renames while getAffectedFileLocations uses it, so the two views of one diff disagree for renamed files.
  • intelliStory.jsresolveAndIndexpath.relative does not case-fold, so a casing divergence between bundler module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that never match. Mechanism is real, real-world trigger still unconfirmed. Cheapest guard: warn when the computed relative path starts with ...
  • graphTraceTemplate.html:8, :12trace.html still loads Drawflow from cdn.jsdelivr.net, so the diagnostic renders blank in an air-gapped or locked-down CI runner. Vendor the bundle.

12. Bail visibility — partially addressed

  • Severity: Low
  • Issue: This push adds ten log.warn calls, so pattern and configDir problems are now visible. The IntelliStoryBailError throw sites themselves still don't log before throwing, so whether a user learns that filtering did not apply continues to depend on the @percy/storybook catch site — outside this repo and still a follow-up per the PR body.
  • Suggestion: A small bail(message, log) helper that warns and throws.

Informational

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested): Storybook addons are usually devDependencies imported directly by preview.js/story files, so a pure addon bump with no source diff won't surface here. Explicit and tested, so presumably intentional — worth a sanity check with the team.
  • .semgrepignore still suppresses intelliStory.js file-wide rather than at the flagged joins. The file grew ~290 lines this push, which widens the unscanned surface; the documented rationale (inline // nosemgrep unsupported by the CI semgrep version) still holds. Worth narrowing when tooling allows.
  • Version strings moved 1.32.6-beta.31.32.6 via the master release merge; no functional change.

Verdict: PASS

@RaghavsBrowserStack
RaghavsBrowserStack merged commit 8311242 into masterAug 10, 2026
48 checks passed
@RaghavsBrowserStack
RaghavsBrowserStack deleted the PPLT-5844 branch August 10, 2026 12:57
RaghavsBrowserStack added a commit that referenced this pull request Aug 11, 2026
* bump version
* remove stray archive test fixtures committed by mistake
packages/core/.test-archive-mixed and .test-archive-symlink were leftovers
from a local unit-test run that got picked up by the version-bump commit.
.test-archive-symlink/linked.json is an absolute symlink into a developer's
home directory, so it dangles on a fresh CI checkout and makes
`babel packages -d build` (build_cjs) fail with ENOENT, breaking the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix unwanted commits
* remove stray archive test fixtures reintroduced by the master merge
d302b09 deleted these on this branch, but master picked up the same
accidental add in d663543 (bump version, #2377), so merging master back in
(2405903) restored them and left PR #2378 with an empty diff.
packages/core/.test-archive-symlink/linked.json is an absolute symlink into a
developer's home directory. It dangles on a fresh CI checkout, so
`babel packages -d build` (build_cjs) fails with ENOENT and takes down the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* regenerate yarn.lock so `lerna publish` sees a clean tree
The lockfile block added in 8311242 (IntelliStory, #2339) was edited by hand
rather than regenerated, leaving it inconsistent with the dependency graph:
- @types/node@* had no entry, though @types/cacheable-request, @types/keyv,
@types/responselike and @types/yauzl all require it
- get-stream@^5.1.0 had no entry, though cacheable-request@^7.0.2 requires it
- @types/yauzl@^2.9.1 was orphaned — nothing referenced it
So `yarn` on a clean CI checkout resolved the two missing descriptors, pruned
the orphan and rewrote the file. That left the working tree dirty, and
`lerna publish from-package` aborted with EUNCOMMIT (M yarn.lock), failing the
Release job.
Regenerated with a plain `yarn install`. `yarn install --frozen-lockfile` is
now a no-op, so the release checkout stays clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@RaghavsBrowserStack@prklm10@github-advanced-security
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Merged
RaghavsBrowserStack merged 29 commits into
masterfrom
PPLT-5844
Aug 10, 2026
Merged

PPLT-5844: add IntelliStory storybook affected-story filtering#2339
RaghavsBrowserStack merged 29 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).
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/test/graphTrace.test.js Fixed
…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.
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
@RaghavsBrowserStackRaghavsBrowserStack changed the title feat(cli-command,client): add IntelliStory storybook affected-story filteringPPLT-5844: add IntelliStory storybook affected-story filteringJul 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 ownerJuly 15, 2026 14:29
Comment threadpackages/cli-command/src/intelliStory.js Fixed
Comment threadpackages/cli-command/src/intelliStory.js Fixed
RaghavsBrowserStackand others added 3 commits July 21, 2026 13:20
Add tests for the graceful-bail paths added in "apply suggested fixed":
- malformed stats-file JSON (validateAndReadStats)
- unreadable current lockfile (getAffectedPackages)
- unreadable package.json (getAffectedPackages)
These run on Node 14 (they bail before the Node>=18 snyk path), fixing the
lines/statements coverage-threshold failure on the Node 14 test job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GLOB_CHARS only detected * and ? so brace ({a,b}) and bracket ([abc])
patterns fell through to exact-match, silently disabling the intended
glob semantics for these documented config options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:001c036Reviewers: stack:code-reviewer

Summary

Adds IntelliStory — an affected-story graph feature for @percy/storybook: parses Storybook build stats, diffs against a git baseline (incl. lockfile/dependency diffing via optional snyk-nodejs-lockfile-parser), enqueues a server-side affected-story graph job, tags snapshots for selection, and optionally renders a debug trace.html. Plumbs intelliStory/storybookPath through @percy/client and @percy/core, and adds a Node 20 CI leg for the Node-18+-only snyk path.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNone introduced.
HighSecurityAuthentication/authorization checks presentN/ANo auth surface changed.
HighSecurityInput validation and sanitizationPassassertSafeRef blocks flag-injection; safeJson escapes HTML/JS breakouts; paths anchored via basename.
HighSecurityNo IDOR — resource ownership validatedN/ANo resource-ownership surface.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1 ($-pattern .replace) still open. Finding 2 (glob detection gap) resolved in 001c036.
HighCorrectnessError handling is explicit, no swallowed exceptionsPass*Mostly graceful IntelliStoryBailError; one inconsistent rethrow — Finding 3.
HighCorrectnessNo race conditions or concurrency issuesPassNo new concurrency.
MediumTestingNew code has corresponding testsPassBroad coverage across new modules.
MediumTestingError paths and edge cases testedPass*Package-affected merge branch istanbul ignored rather than tested — Finding 5.
MediumTestingExisting tests still pass (no regressions)PassNot re-run here; CI covers.
MediumPerformanceNo N+1 queries or unbounded data fetchingN/ANo queries.
MediumPerformanceLong-running tasks use background jobsPassGraph job is server-side/async with timeout bail.
MediumQualityFollows existing codebase patternsPassConsistent with client/core plumbing conventions.
MediumQualityChanges are focused (single concern)PassScoped to IntelliStory feature.
LowQualityMeaningful names, no dead codePassClear naming.
LowQualityComments explain why, not whatPass.semgrepignore rationale well-documented.
LowQualityNo unnecessary dependencies addedPasssnyk-nodejs-lockfile-parser is optional/lazy-loaded.

Findings

  • File:packages/cli-command/src/graphTrace.js:114-116

  • Severity: Medium

  • Reviewer: stack:code-reviewer (verified)

  • Issue:.replace('__VERTICES_JSON__', safeJson(...)) uses plain-string replacement values. JS honors $$, $&, $`, $', $n special patterns in the replacement string even when the search arg is a literal. Embedded JSON contains arbitrary file paths; a path with $' re-injects the rest of the template, breaking the embedded JS. safeJson's escaping doesn't cover this — the corruption happens in the .replace call itself.

  • Suggestion: Pass a function as the replacement: .replace('__VERTICES_JSON__', () => safeJson(...)). Add a graphTrace.test.js case with $&/$'/$$ in a vertex name.

  • File:packages/cli-command/src/intelliStory.js:16, 34-41

  • Severity: Medium — ✅ RESOLVED in 001c036

  • Reviewer: stack:code-reviewer (verified)

  • Issue:GLOB_CHARS = /[*?]/ only routed */? patterns through the glob matcher, but patternToRegex enables {extended:true, globstar:true} (brace/bracket support). A configured bailOnChanges: ['src/{a,b}.js'] or bracket pattern silently fell through to literal str === pattern and never matched — silently disabling a documented config option (bailOnChanges/untraced).

  • Fix applied: Broadened detection to const GLOB_CHARS = /[*?{}[\]]/;. Added brace- and bracket-glob tests to assertNoBailOnChanges() and enforceUntraced() — verified failing under the old regex and passing under the fix (full 156-spec suite green).

  • File:packages/cli-command/src/intelliStory.js:326-336

  • Severity: Medium

  • Reviewer: stack:code-reviewer

  • Issue: Only SNYK_LOCKFILE_PARSER_UNAVAILABLE is converted to a graceful IntelliStoryBailError; a buildDepTree parse failure (malformed/older-ref lockfile) rethrows as a raw Error. Every other failure path degrades to "running full snapshot set."

  • Suggestion: Confirm the @percy/storybook caller treats allapplyIntelliStory throws as fall-back-to-full-set. If not, wrap this rethrow in IntelliStoryBailError for consistency.

  • File:packages/cli-command/src/intelliStory.js:67-70

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue:assertSafeRef regex correctly blocks leading -, but also rejects ordinary safe ref syntax like HEAD~5 / abc123^2.

  • Suggestion: Confirm intentional, or widen charset to include ~^ (still safe given the leading-char restriction).

  • File:packages/cli-command/src/intelliStory.js:438-440

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: The packageAffectedNodes merge into affectedNodes (the actual mechanism folding lockfile-driven dep changes into the graph) is /* istanbul ignore next */ rather than covered by a real test.

  • Suggestion: Add an itPosix-style Node≥18 integration test exercising the merge end-to-end.

  • File:packages/cli-command/src/intelliStory.js (resolveAndIndex)

  • Severity: Low

  • Reviewer: stack:code-reviewer

  • Issue: Absolute module ids resolving outside projectRoot (symlinked pkg, monorepo sibling) produce ../-prefixed paths added straight into the files index without a guard.

  • Suggestion: Bail or drop entries where rel.startsWith('..').

  • File:.semgrepignore:118-131 / packages/client/src/client.js:2591-2592 / .github/workflows/test.yml

  • Severity: Info

  • Reviewer: stack:code-reviewer

  • Issue: (a) Whole-file semgrep suppression means future traversal code in the growing intelliStory.js goes unchecked. (b) client.intelliStoryStats is lazily-created, never reset — accumulates across builds if a client is reused. (c) New test-node20 CI job (workflow change) — flagged for visibility; mirror logic looks consistent, no action requested.

  • Suggestion: Consider narrowing semgrep scope over time; reset stats in createBuild.


Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:966c05dReviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), findings then re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a new static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

The bail-first posture is the right architecture for a feature that can silently drop visual coverage, and it is applied thoroughly — 15+ explicit bail sites covering missing build id, unreadable/invalid stats, git failures, ambiguous or absent lockfiles, manifest changes spanning directories, browser-upgrade builds, and graph-job timeouts. All four Medium findings below are gaps in that otherwise-sound safety net, not architectural problems.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through the existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync is used without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassExtensive and largely correct bail coverage. Four Medium gaps: findings 1–4.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps: the three API calls are not (finding 1), and dropped stats modules are logged at debug only (finding 4).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedPassBail branches are covered individually (commit e071c952 restored 100% coverage). The glob semantics in finding 3 are asserted deliberately by intelliStory.test.js:286-289.
MediumTestingExisting tests still pass (no regressions)PassAll CI checks green on 966c05d: Lint, Build, Build & verify executable, Regression, every package suite (incl. the new Node 20 leg), CodeQL, Semgrep OSS.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover. Minor: findings 5–7.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer (refined by orchestrator verification)
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or DNS failure therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); it is only thrown exceptions that leak.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph, and in pollGraphStatus treat a throw as a non-done status.

2. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (verified)
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail, and filtering proceeds as though nothing global changed. This is the under-snapshot direction: real visual regressions ship unnoticed.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

3. Bare-extension bailOnChanges globs silently match nothing below the repo root

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex), consumed at :251
  • Severity: Medium
  • Reviewer: stack:code-reviewer (severity downgraded from High by the orchestrator — see below)
  • Issue: With { extended: true, globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$ — verified locally — so it matches only root-level files, never src/components/Button.css. A user who configures bailOnChanges: ['*.css'] expecting "bail on any stylesheet change" gets no bail, and IntelliStory filters as if nothing unsafe changed. untraced shares the semantics but fails safe (nothing is dropped, so more snapshots are taken).
    Downgraded from the sub-reviewer's High because this is standard glob behaviorminimatch, picomatch, and GitHub Actions paths: filters all treat a bare *.ext as root-only — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. Still worth flagging: bailOnChanges is the one user-facing safety valve and the failure mode is silent.
  • Suggestion: Either document that patterns are path-segment-scoped and **/*.css is required (there is currently no documentation for bailOnChanges/untraced anywhere in the repo), or auto-broaden bare-name patterns before compiling: const p = pattern.includes('/') ? pattern : '**/' + pattern;. If the current semantics stay, rename the test to state the intent ('a bare-extension pattern does not match nested paths') so the behavior reads as deliberate.

4. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run still proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate the existing message to log.warn, and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

5. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue:resolveAndIndex runs only when copy.type === 'src'. Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the external enrichment tool ever emits a third type carrying an absolute path — an asset, CSS, or JSON import — that raw absolute path ships unnormalized in the modules payload: it leaks the developer's home or CI runner path, and it cannot be correlated against the project-relative affectedNodes, which is a false-negative path.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe and type-agnostic.

6. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (correction: rename detection is git's default since 2.9, so the inconsistency is present out of the box, not only under custom config)
  • Issue:affectedNodes comes from git diff --name-only with rename detection active, which collapses a rename to the new path alone; getAffectedFileLocations runs the same diff with --no-renames, which yields a delete + add pair. The two views of one diff can disagree for renamed files. Low practical impact — the old path no longer exists to be imported — but it makes the two graph inputs non-comparable.
  • Suggestion: Add --no-renames to the gitDiffNames invocation for determinism and parity with :84.

7. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer (verified)
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. This is opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or a mis-selection — often inside a locked-down or air-gapped CI environment where the CDN is unreachable and the trace renders blank.
  • Suggestion: Vendor the (small) Drawflow bundle into the package and inline it into the template.

8. Whole-file .semgrepignore suppression for intelliStory.js

  • File:.semgrepignore:49-57
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: The suppression is file-level rather than scoped to the specific path.join/path.resolve sites that trigger the false positive. intelliStory.js does a fair amount of fs/path work, so any genuinely unsafe join added later goes unscanned. The stated rationale (inline // nosemgrep not honored by the CI semgrep version) is legitimate and well documented.
  • Suggestion: Track a follow-up to narrow the suppression once the CI semgrep version honors inline annotations.

9. No bail is logged at the throw site

  • File:packages/cli-command/src/intelliStory.js — all IntelliStoryBailError throw sites
  • Severity: Low
  • Reviewer: stack:code-reviewer
  • Issue: Every bail constructs a carefully worded message but none logs at warn/info before throwing, so whether a user ever learns that filtering did not apply depends entirely on the catch site in @percy/storybook — outside this repo and, per the PR body, still a follow-up. If that catcher logs at debug or swallows while falling back, the fail-safe is invisible.
  • Suggestion:log.warn(message) at the throw site (or inside a small bail(message, log) helper) so visibility does not depend on the external consumer.

Verified non-issues

  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced anywhere in cli-command — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: PASS

@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:01aa1f6Reviewers:stack:code-reviewer (definition run inline — the colon-prefixed agent name is not registered as a launchable agent type), second independent pass; all findings re-verified against source by the orchestrator

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (createRequire bound as cjsRequire, guarded by a static noRequireBinding test) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

Note on this run. Head moved from 966c05d to 01aa1f6, but both new commits (01aa1f6, f6ef573) are Merge branch 'master' into PPLT-5844git diff 966c05d 01aa1f6 over the PR-owned paths is empty, so the reviewable code is byte-identical to the previously reviewed head. The fast-path did not fire because the intermediate merge f6ef573 carries no prior Claude Code Review status, so a full review ran. This second, independent pass surfaced finding 1, which the first pass missed. The code has not regressed — the assessment has sharpened, and the verdict changes accordingly.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth continues to flow through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse PercyClient's existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before they reach spawnSync (intelliStory.js:65); statsFile is narrowed to a bare .json basename (:192); spawnSync runs without a shell. The dedicated security lens is disabled in this orchestrator, so this row is a correctness-level read only.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; the new endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesFailFinding 1: bailOnChanges/untraced globs are matched against repo-root-relative paths with no rebasing, while story importPaths in the same function are rebased from the invocation directory — so the user's safety valve is silently inert for monorepo/subdirectory invocation.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassEvery git/fs/config failure is wrapped in IntelliStoryBailError. Gaps are Medium, not High: the three API calls are unwrapped (finding 2), malformed globs are swallowed to false (finding 4), and dropped stats modules are logged at debug only (finding 5).
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop (12 × 5s); no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass~1200 new test lines across intelliStory, lockfileDiff, graphTrace, index, noRequireBinding; PR reports 145/145 cli-command specs green.
MediumTestingError paths and edge cases testedFailBail branches are covered individually, but every fixture builds a git repo with files at its root — there is no cwd ≠ projectRoot case, which is exactly why finding 1 went unnoticed. No test simulates a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)PassAll CI checks were green on 966c05d, whose PR-owned tree is identical to this head.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne snapshot-name-to-commit GET, one generate-graph POST, and a bounded poll (max 12 attempts / ~60s). Stats parsing is in-memory after stream-json was dropped.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled; selection happens server-side at snapshot-post time.
MediumQualityFollows existing codebase patternsPassMatches PercyClient method/identifier conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on (folds in #2306) and the CI leg needed to exercise the lockfile path — all justified in the PR body.
LowQualityMeaningful names, no dead codePassstream-json confirmed fully removed from packages/cli-command/package.json and unreferenced — not a leftover.
LowQualityComments explain why, not whatPassNotably good: the cjsRequire rationale, the server-side-selection comment at intelliStory.js:437-447, and the .semgrepignore entries all explain why.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp is small and used; snyk-nodejs-lockfile-parser is correctly an optionalDependency with a Node 20 CI leg to cover it.

Findings

1. bailOnChanges/untraced globs are never rebased to the invocation directory — the safety valve is silently inert in a monorepo

  • File:packages/cli-command/src/intelliStory.js:250-252, :259-262 (compare :415-426)

  • Severity: High

  • Reviewer: stack:code-reviewer (second pass); mechanism verified by the orchestrator

  • Issue:affectedNodes comes from git diff --name-only, which always emits repo-root-relative paths regardless of cwd. assertNoBailOnChanges and enforceUntraced match user-supplied globs directly against those paths, and glob-to-regexp fully anchors the result (^…$). Meanwhile, twenty lines further down, the same function builds invocationDir = process.cwd() and rebases each story's importPath from the invocation directory to project-relative (:415-426) — so the file demonstrably does expect cwd ≠ projectRoot. User globs get no such treatment.

    Concrete failure: a monorepo user runs cd packages/ui && percy storybook ./storybook-static with bailOnChanges: ['webpack.config.js']. A change to that file lands in affectedNodes as packages/ui/webpack.config.js; the pattern compiles to /^webpack\.config\.js$/ and does not match. No bail fires, no warning is logged, and IntelliStory filters as though nothing risky changed — the under-snapshot direction, where a genuine visual regression ships unreviewed. Monorepo-with-subdirectory is the mainstream Storybook layout, and bailOnChanges exists precisely as the escape hatch for changes the graph can't reason about.

    Not covered by any test: every fixture in intelliStory.test.js constructs a repo with files at its root, so no cwd ≠ projectRoot case is exercised.

  • Suggestion: Rebase user patterns the same way story paths already are, before matching:

    constrebase=g=>{if(path.isAbsolute(g))returnpath.relative(projectRoot,g).split(path.sep).join('/');constabs=path.resolve(invocationDir,g);constrel=path.relative(projectRoot,abs);returnrel.startsWith('..') ? g : rel.split(path.sep).join('/');};

    and pass bailOnChanges.map(rebase) / untraced.map(rebase) into the two functions. Add a test with cwd set to a nested package directory. If root-relative globs are the intended contract instead, that must be documented and a bail should fire when cwd !== projectRoot and patterns are configured — silently doing nothing is the one outcome to avoid.

2. API/network failures escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:181, :220, :354
  • Severity: Medium
  • Reviewer: stack:code-reviewer — independently reported by both review passes
  • Issue: Every git, fs, config, and lockfile failure in this module is wrapped in IntelliStoryBailError so the caller can degrade to a full snapshot run. The three percy.client calls — getIntelliStorySnapshotNameToCommit (:220), getStatus('intelli_story_graph', …) inside pollGraphStatus (:181), and generateIntelliStoryGraph (:354) — are not. A transient 5xx, timeout, or connection reset therefore propagates as a plain Error. If @percy/storybook catches IntelliStoryBailError specifically (the type this module exports for exactly that purpose), an API blip hard-fails the whole storybook command instead of falling back to a full run — the opposite of the module's fail-safe posture. The graph-job timeout path is handled correctly (:359); only thrown exceptions leak. diffLockfileDeps (:316) has the same shape: it remaps only SNYK_LOCKFILE_PARSER_UNAVAILABLE and re-throws anything else raw.
  • Suggestion: Wrap each client call and rethrow, e.g.
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status. Add tests where each client method rejects.

3. .storybook config directory is hardcoded — a custom configDir never trips the bail

  • File:packages/cli-command/src/intelliStory.js:114, :244
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: Both the graph exclusion (EXCLUDED_DIRS = new Set(['node_modules', '.storybook'])) and the bail check (assertNoDotStorybookChange) match the literal path segment .storybook. Storybook's config directory is configurable (-c <dir> / configDir), and intelliStoryConfig has no field to communicate it. For a project using e.g. config/storybook/, a change to main.js or preview.js — which can affect every story — produces no bail. Same under-snapshot direction as finding 1, and the two compound for monorepo users.
  • Suggestion: Thread the resolved config-dir name through intelliStoryConfig from @percy/storybook into both call sites, defaulting to .storybook:
    exportfunctionassertNoDotStorybookChange(affectedNodes,configDir='.storybook'){consthit=affectedNodes.find(p=>p.split(/[/\\]/).includes(configDir));}

4. A malformed glob is silently treated as "matches nothing"

  • File:packages/cli-command/src/intelliStory.js:34-43 (matchesPattern)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (second pass); verified — glob-to-regexp('src/[') throws Unterminated character class, 'src/{a' throws Unterminated group
  • Issue:try { … } catch { return false } means a typo'd pattern in a user's config compiles-fails and is then treated as never matching. For untraced that fails safe (the file stays traced). For bailOnChanges it fails open: the guardrail is silently disabled with nothing logged anywhere. The over-length guard at :22 has the same shape and is explicitly tested as non-throwing (intelliStory.test.js:269-271).
  • Suggestion:log.warn when a pattern fails to compile — a broken guardrail must be visible:
    }catch(e){log.warn(`IntelliStory: ignoring invalid pattern "${pattern}": ${e.message}`);returnfalse;}
    Better still, validate the patterns once at config-parse time rather than per-path.

5. Stats modules dropped from the graph are logged only at debug, with no threshold

  • File:packages/cli-command/src/intelliStory.js:132-133, :161-166
  • Severity: Medium
  • Reviewer: orchestrator (first pass)
  • Issue:transformModule returns null for any module whose id does not resolve to an absolute path, and readStats counts those into droppedModules and emits a single log.debug. Missing modules mean missing edges in the dependency graph, and a missing edge is exactly what makes the API judge a story unaffected. If a Storybook/webpack version emits a large share of synthetic or virtual module ids, the graph is quietly degraded and the run proceeds — visible only under --verbose. Every other incomplete-data condition in this file bails.
  • Suggestion: Escalate to log.warn and bail above a threshold:
    if(droppedModules){constratio=droppedModules/rawModules.length;if(ratio>0.1)thrownewIntelliStoryBailError(`IntelliStory: ${droppedModules}/${rawModules.length} modules had unresolved ids; graph would be incomplete; running full snapshot set`);log.warn(`IntelliStory: dropped ${droppedModules} module(s) with unresolved (non-absolute) ids`);}

6. Bare-extension globs don't match nested paths

  • File:packages/cli-command/src/intelliStory.js:28 (patternToRegex)
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass); verified
  • Issue: With { globstar: true }, glob-to-regexp('*.css') compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior — minimatch, picomatch, and GitHub Actions paths: filters agree — and the tests assert it deliberately (intelliStory.test.js:266, :286-289), so it is a considered choice rather than a coding error. It is listed because it lands on the same safety valve as findings 1 and 4, and there is currently no documentation for bailOnChanges/untraced anywhere in the repo.
  • Suggestion: Document that patterns are path-segment-scoped and **/*.css is required. If you prefer forgiving semantics, auto-broaden bare-name patterns: const p = pattern.includes('/') ? pattern : '**/' + pattern; — note this must be decided together with finding 1's rebasing.

7. noRequireBinding guard is narrower than the crash class it protects

  • File:packages/cli-command/test/noRequireBinding.test.js:40
  • Severity: Low
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:FORBIDDEN = /\b(?:const|let|var)\s+require\s*=\s*createRequire\b/ only catches a single-line declaration. The same startup crash returns via import { createRequire as require } from 'module', const { default: require } = …, or a line-wrapped const require =\n createRequire(…) — none of which match.
  • Suggestion: Flag any binding of the bare identifier require (import specifier, declaration LHS, or parameter), not just the createRequire-RHS form.

8. mapEntry normalizes source only for type === 'src'

  • File:packages/cli-command/src/intelliStory.js:137
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Fixtures only exercise 'src' and 'module' (the latter a no-op, since package specifiers are never absolute). If the enrichment tool ever emits a third type carrying an absolute path, that path ships unnormalized: it leaks a local or CI-runner path and cannot be correlated against project-relative affectedNodes.
  • Suggestion: Drop the type gate — resolveAndIndex already returns non-absolute values untouched, so running it for every string source is safe.

9. gitDiffNames omits --no-renames while getAffectedFileLocations uses it

  • File:packages/cli-command/src/intelliStory.js:75 vs :84
  • Severity: Low
  • Reviewer: stack:code-reviewer (first pass)
  • Issue: Rename detection is git's default since 2.9, so affectedNodes collapses a rename to the new path alone while getAffectedFileLocations (run with --no-renames) sees a delete + add pair. The two views of one diff disagree for renamed files.
  • Suggestion: Add --no-renames to gitDiffNames for parity.

10. path.relative does not case-fold when computing project-relative module ids

  • File:packages/cli-command/src/intelliStory.js:117-128 (resolveAndIndex)
  • Severity: Low (mechanism demonstrated, real-world trigger unconfirmed)
  • Reviewer: stack:code-reviewer (second pass)
  • Issue:path.relative('/Users/bob/proj', '/Users/Bob/Proj/src/App.js') returns '../../Bob/Proj/src/App.js' — exact segment comparison, no case folding, on every platform. On case-insensitive-but-preserving filesystems (APFS, NTFS), any casing divergence between the bundler's absolute module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that can never match a real repo-relative path. The reviewer could not force an actual divergence, so this is flagged as plausible rather than confirmed.
  • Suggestion: At minimum, warn when the computed relative path escapes projectRoot (starts with ..) rather than silently indexing it; optionally normalize both sides through fs.realpathSync.

11. trace.html loads Drawflow from a public CDN

  • File:packages/cli-command/src/graphTraceTemplate.html:8, :12
  • Severity: Low
  • Reviewer: stack:code-reviewer — reported by both passes
  • Issue: The debug trace viewer pulls Drawflow's CSS and JS from cdn.jsdelivr.net at view time. Opt-in (trace: true) and off the production path, but it is precisely the artifact someone opens after a bail or mis-selection — often inside a locked-down or air-gapped CI runner where it renders blank.
  • Suggestion: Vendor the small Drawflow bundle and inline it into the template.

12. Minor: bail visibility, semgrep scope, stats-shape logging

  • Severity: Low
  • Reviewer: stack:code-reviewer (both passes)
  • Issue: (a) No bail logs at the throw site, so user visibility depends entirely on the @percy/storybook catch — outside this repo and still a follow-up per the PR body. (b) .semgrepignore:49-57 suppresses intelliStory.js file-wide rather than at the flagged joins, so future unsafe joins in this fs/path-heavy file go unscanned; the rationale is documented and the constraint (inline // nosemgrep unsupported by the CI version) is real. (c) readStats's stats.modules || [] (:161) makes a stats file with no modules key indistinguishable from a legitimately empty one.
  • Suggestion: (a) log.warn(message) inside a small bail(message, log) helper. (b) Track narrowing the suppression. (c) log.debug when the key is absent versus empty.

Informational — not counted as defects

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested at lockfileDiff.test.js:85): Storybook addons are typically devDependencies and are imported directly by preview.js/story files. A pure addon version bump with no source diff will not surface through this path. It is explicit and tested, so likely intentional (relying on the server-side module graph instead) — worth a sanity check with the team given how conservative this feature aims to be.
  • stream-json leftover: confirmed absent from packages/cli-command/package.json and unreferenced — correctly removed in 9c525f10.
  • Backward compatibility: the new snapshotSchema fields (intelliStory, storybookPath) are optional, and createSnapshot sends 'intelli-story': intelliStory || null / 'storybook-path': storybookPath || null, so non-IntelliStory callers are unchanged — cli-upload/test/upload.test.js pins exactly that.
  • getStatus response shape:intelli_story_graph returns a top-level { status, data }, unlike the batched-by-id shape used for snapshot/comparison. pollGraphStatus and client.test.js:890-905 agree on it; an API-contract quirk, not a defect.

Verdict: FAIL

Comment threadpackages/cli-command/test/intelliStory.test.js Fixed
@RaghavsBrowserStack

Copy link
Copy Markdown
ContributorAuthor

Claude Code PR Review

PR:#2339Head:91bd770Reviewers:stack:code-reviewer (prior passes), fix verification by the orchestrator against source at this head

Summary

Adds IntelliStory to @percy/cli-command: for a Storybook build it resolves a baseline (explicit or API-predicted), diffs the working tree against it, ships the enriched module graph to the new /intelli_story endpoints, and tags each snapshot with intelliStory/storybookPath so the API performs affected-story selection server-side — bailing to a full snapshot run via IntelliStoryBailError whenever it can't reason safely about a change. Also folds in the packaged-binary crash fix (cjsRequire) and a Node 20 CI leg for the Snyk-backed lockfile-diff path.

This head resolves the High finding from the 01aa1f6 review along with three of the five Mediums. Verdict flips to PASS.

Fix verification (vs. 01aa1f6)

#FindingSevStatus at 91bd770
1bailOnChanges/untraced globs not rebased to the invocation dirHighFixed
2Client calls throw raw Error, not IntelliStoryBailErrorMediumOpen
3.storybook config dir hardcodedMediumFixed
4Malformed glob silently swallowed to "no match"MediumFixed
5Dropped stats modules logged at debug, no thresholdMediumFixed
6Bare-extension globs don't match nested pathsMediumOpen (by design; still undocumented)
7noRequireBinding regex narrower than the crash classLowOpen
8mapEntry normalizes source only for type === 'src'LowOpen
9gitDiffNames omits --no-renamesLowOpen
10path.relative does not case-foldLowOpen (was unconfirmed)
11trace.html loads Drawflow from a CDNLowOpen
12No log.warn at bail throw sitesLowPartially addressed

Finding 1 is fixed properly, not papered over. resolvePattern (intelliStory.js:88-100) rebases each user pattern from the invocation directory onto the repo root — the same basis normalizeImportPath already used for story paths, so the asymmetry that caused the bug is gone. Beyond that, the fix handles the two readings asymmetrically by direction of harm, which is the right call:

  • assertNoBailOnChanges (:394-425) bails when a pattern matches only under the repo-root reading, logging a warning with a concrete rewrite suggestion — a stale root-relative config still protects the user.
  • enforceUntraced (:433-462) declines to apply such a pattern and warns, since applying it would drop snapshots.

A <rootDir>/ prefix is an explicit opt-out, patterns resolving outside the repo bail rather than matching nothing, and resolveConfigDirs (:361-390) additionally checks that a configured directory actually exists — catching a wrong-basis configDir at config time instead of silently never matching. Findings 3, 4 and 5 are each fixed with matching care: isInsideDirs distinguishes single-segment (any depth) from multi-segment (prefix) config dirs so config/storybook can't match config/storybook-old; patternError validates globs up front and routes a compile failure to a bail for bailOnChanges and a warn-skip for untraced; and readStats now separates deliberately-excluded modules from unresolved ones, warns on the latter and bails past a 10% ratio of resolvable candidates — so a stats file heavy in node_modules no longer trips the threshold. intelliStory.test.js grows 364 lines with cwd ≠ projectRoot fixtures (projectRoot: '/repo', invocationDir: '/repo/packages/ui') and a resolveConfigDirs() suite — the gap that let finding 1 through is now covered.

Review Table

PriorityCategoryCheckStatusNotes
HighSecurityNo hardcoded secrets or credentialsPassNo credentials in the diff; auth flows through existing PercyClient token handling.
HighSecurityAuthentication/authorization checks presentN/ACLI-side code; the new /intelli_story calls reuse existing token auth.
HighSecurityInput validation and sanitizationPassassertSafeRef whitelists git refs before spawnSync; statsFile narrowed to a bare .json basename; spawnSync runs without a shell. User globs are now validated up front via patternError. Dedicated security lens is disabled in this orchestrator.
HighSecurityNo IDOR — resource ownership validatedN/AServer-side concern; endpoints are keyed by the caller's own build id under its own token.
HighSecurityNo SQL injection (parameterized queries)N/ANo SQL in this codebase.
HighCorrectnessLogic is correct, handles edge casesPassThe path-basis defect is fixed, with both readings handled by direction of harm. Remaining gaps are Medium/Low.
HighCorrectnessError handling is explicit, no swallowed exceptionsPassGlob compile failures are no longer swallowed, and incomplete graphs now bail past a threshold. Finding 2 (unwrapped client calls) remains, at Medium.
HighCorrectnessNo race conditions or concurrency issuesPasspollGraphStatus is a bounded sequential loop; no shared mutable state beyond the per-client intelliStoryStats counter.
MediumTestingNew code has corresponding testsPass+364 test lines this push, covering resolvePattern, resolveConfigDirs, and the <rootDir>/ opt-out.
MediumTestingError paths and edge cases testedPassThe cwd ≠ projectRoot gap is closed. Still untested: a rejecting client call (finding 2).
MediumTestingExisting tests still pass (no regressions)Pass46 checks green, incl. Lint, Build & verify executable, Regression, all cli-command suites and the Node 20 leg. Test @percy/core still pending at review time — unrelated to the changed packages.
MediumPerformanceNo N+1 queries or unbounded data fetchingPassOne GET, one POST, bounded poll (12 × 5s). In-memory stats parsing.
MediumPerformanceLong-running tasks use background jobsPassGraph generation is enqueued server-side and polled.
MediumQualityFollows existing codebase patternsPassMatches PercyClient conventions, @percy/logger scoping, and the package's exports subpath pattern.
MediumQualityChanges are focused (single concern)PassIntelliStory plus the binary-crash fix it depends on and the CI leg that exercises the lockfile path.
LowQualityMeaningful names, no dead codePassstream-json confirmed absent. New helpers (resolvePattern, patternError, rootReadingHit, suggestRewrite) are well named and each has a stated reason to exist.
LowQualityComments explain why, not whatPassThe new comments are unusually good — they record the direction of harm reasoning behind each asymmetry ("recognising one directory too many only costs snapshots, while missing the real one loses comparisons"), which is exactly what a future maintainer needs.
LowQualityNo unnecessary dependencies addedPassglob-to-regexp used; snyk-nodejs-lockfile-parser correctly optional with a Node 20 CI leg.

Findings

2. API/network failures still escape as raw errors instead of IntelliStoryBailError

  • File:packages/cli-command/src/intelliStory.js:292, :331, :555
  • Severity: Medium
  • Reviewer: stack:code-reviewer (reported independently by both prior passes); re-verified unfixed at this head
  • Issue: Verified still open: getStatus('intelli_story_graph', …) in pollGraphStatus (:292), getIntelliStorySnapshotNameToCommit (:331), and generateIntelliStoryGraph (:555) are all bare awaits with no surrounding try/catch. Every other failure mode in this file — git, fs, config, lockfile, malformed glob, degraded graph — is deliberately converted to IntelliStoryBailError so the caller can degrade to a full snapshot run. A transient 5xx, timeout, or connection reset from these three still propagates as a plain Error, so if @percy/storybook catches IntelliStoryBailError specifically, an API blip hard-fails the storybook command instead of falling back. The graph-job timeout path is handled correctly (:561); only thrown exceptions leak. Not a merge blocker under the verdict rule, but it is now the last inconsistency in an otherwise uniformly fail-safe module.
  • Suggestion:
    letbaseLookup;try{baseLookup=awaitpercy.client.getIntelliStorySnapshotNameToCommit(percy.build?.id);}catch(e){thrownewIntelliStoryBailError(`IntelliStory: base build lookup failed: ${e.message}; running full snapshot set`);}
    Same for generateIntelliStoryGraph; in pollGraphStatus, treat a throw as a non-done status so the existing :561 bail covers it. Add tests where each client method rejects.

6. Bare-extension globs still don't match nested paths, and the contract is undocumented

  • File:packages/cli-command/src/intelliStory.js:28
  • Severity: Medium
  • Reviewer: stack:code-reviewer (first pass)
  • Issue:glob-to-regexp('*.css', { globstar: true }) compiles to ^([^/]*)\.css$, so bailOnChanges: ['*.css'] never matches src/Button.css. This is standard glob behavior (minimatch, picomatch, GitHub Actions paths: all agree) and is deliberately asserted by the tests, so it is not a defect. It is still listed because the new rootReadingHit warning covers the basis mistake but not the depth mistake — a user who writes *.css gets no warning at all — and there remains no documentation for bailOnChanges/untraced/configDir anywhere in the repo.
  • Suggestion: Document the semantics now that the basis rules are non-trivial (invocation-relative, <rootDir>/ opt-out, path-segment-scoped globs). Optionally extend the existing warning: if a pattern has no / and no **, and some affected node matches **/<pattern>, warn the same way the root-reading case does.

7–11. Carried-over Lows, unchanged at this head

  • noRequireBinding.test.js:40FORBIDDEN still only matches a single-line const/let/var require = createRequire. The same crash returns via import { createRequire as require } from 'module' or a line-wrapped declaration. Broaden to flag any binding of the bare require identifier.
  • intelliStory.js:229mapEntry still gates normalization on copy.type === 'src'. Drop the gate; resolveAndIndex already passes non-absolute values through untouched.
  • intelliStory.js:138gitDiffNames still omits --no-renames while getAffectedFileLocations uses it, so the two views of one diff disagree for renamed files.
  • intelliStory.jsresolveAndIndexpath.relative does not case-fold, so a casing divergence between bundler module ids and git rev-parse --show-toplevel yields ..-prefixed index keys that never match. Mechanism is real, real-world trigger still unconfirmed. Cheapest guard: warn when the computed relative path starts with ...
  • graphTraceTemplate.html:8, :12trace.html still loads Drawflow from cdn.jsdelivr.net, so the diagnostic renders blank in an air-gapped or locked-down CI runner. Vendor the bundle.

12. Bail visibility — partially addressed

  • Severity: Low
  • Issue: This push adds ten log.warn calls, so pattern and configDir problems are now visible. The IntelliStoryBailError throw sites themselves still don't log before throwing, so whether a user learns that filtering did not apply continues to depend on the @percy/storybook catch site — outside this repo and still a follow-up per the PR body.
  • Suggestion: A small bail(message, log) helper that warns and throws.

Informational

  • devDependencies excluded from the lockfile gate (lockfileDiff.js:53-63, tested): Storybook addons are usually devDependencies imported directly by preview.js/story files, so a pure addon bump with no source diff won't surface here. Explicit and tested, so presumably intentional — worth a sanity check with the team.
  • .semgrepignore still suppresses intelliStory.js file-wide rather than at the flagged joins. The file grew ~290 lines this push, which widens the unscanned surface; the documented rationale (inline // nosemgrep unsupported by the CI semgrep version) still holds. Worth narrowing when tooling allows.
  • Version strings moved 1.32.6-beta.31.32.6 via the master release merge; no functional change.

Verdict: PASS

@RaghavsBrowserStack
RaghavsBrowserStack merged commit 8311242 into masterAug 10, 2026
48 checks passed
@RaghavsBrowserStack
RaghavsBrowserStack deleted the PPLT-5844 branch August 10, 2026 12:57
RaghavsBrowserStack added a commit that referenced this pull request Aug 11, 2026
* bump version
* remove stray archive test fixtures committed by mistake
packages/core/.test-archive-mixed and .test-archive-symlink were leftovers
from a local unit-test run that got picked up by the version-bump commit.
.test-archive-symlink/linked.json is an absolute symlink into a developer's
home directory, so it dangles on a fresh CI checkout and makes
`babel packages -d build` (build_cjs) fail with ENOENT, breaking the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix unwanted commits
* remove stray archive test fixtures reintroduced by the master merge
d302b09 deleted these on this branch, but master picked up the same
accidental add in d663543 (bump version, #2377), so merging master back in
(2405903) restored them and left PR #2378 with an empty diff.
packages/core/.test-archive-symlink/linked.json is an absolute symlink into a
developer's home directory. It dangles on a fresh CI checkout, so
`babel packages -d build` (build_cjs) fails with ENOENT and takes down the
Build Executables job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* regenerate yarn.lock so `lerna publish` sees a clean tree
The lockfile block added in 8311242 (IntelliStory, #2339) was edited by hand
rather than regenerated, leaving it inconsistent with the dependency graph:
- @types/node@* had no entry, though @types/cacheable-request, @types/keyv,
@types/responselike and @types/yauzl all require it
- get-stream@^5.1.0 had no entry, though cacheable-request@^7.0.2 requires it
- @types/yauzl@^2.9.1 was orphaned — nothing referenced it
So `yarn` on a clean CI checkout resolved the two missing descriptors, pruned
the orphan and rewrote the file. That left the working tree dirty, and
`lerna publish from-package` aborted with EUNCOMMIT (M yarn.lock), failing the
Release job.
Regenerated with a plain `yarn install`. `yarn install --frozen-lockfile` is
now a no-op, so the release checkout stays clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@RaghavsBrowserStack@prklm10@github-advanced-security