From 5a0c430f30d7e41e5a5fd620cc1c5b3f902f01cc Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Fri, 17 Jul 2026 00:14:15 +0530 Subject: [PATCH 1/6] Start IntelliStory --- package.json | 4 +- src/config.js | 33 +++++++++ src/snapshots.js | 49 +++++++++++--- src/storybook.js | 1 + src/utils.js | 67 ++++++++++++++++-- test/snapshots.test.js | 130 +++++++++++++++++++++++++++++++++++ test/utils.test.js | 149 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 417 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index ad5c797b..a98291b5 100644 --- a/package.json +++ b/package.json @@ -87,8 +87,8 @@ "dependencies": { "@amplitude/analytics-browser": "2.35.4", "@amplitude/plugin-session-replay-browser": "1.25.16", - "@percy/cli-command": "^1.31.10", - "@percy/config": "^1.31.10", + "@percy/cli-command": "1.32.4", + "@percy/config": "1.32.4", "axios": "1.17.0", "cross-spawn": "^7.0.3", "glob-to-regexp": "^0.4.1", diff --git a/src/config.js b/src/config.js index 4f523d91..cb0dd439 100644 --- a/src/config.js +++ b/src/config.js @@ -206,6 +206,39 @@ export const configSchema = { } } } + }, + intelliStory: { + type: 'object', + unevaluatedProperties: false, + properties: { + enabled: { + type: 'boolean', + default: false + }, + baseline: { + type: 'string' + }, + untraced: { + type: 'array', + items: { type: 'string' } + }, + trace: { + type: 'boolean', + default: false + }, + bailOnChanges: { + type: 'array', + items: { type: 'string' } + }, + statsFile: { + type: 'string', + default: 'enriched-stats.json' + }, + failBuildOnFailure: { + type: 'boolean', + default: false + } + } } } }, diff --git a/src/snapshots.js b/src/snapshots.js index 5549c8d7..5ca571c4 100644 --- a/src/snapshots.js +++ b/src/snapshots.js @@ -1,4 +1,4 @@ -import { logger, PercyConfig } from '@percy/cli-command'; +import { logger, PercyConfig, applyIntelliStory, IntelliStoryBailError } from '@percy/cli-command'; import { yieldAll } from '@percy/cli-command/utils'; import qs from 'qs'; import { @@ -70,12 +70,12 @@ function shouldSkipStory(name, options, config) { // Returns snapshot config options for a Storybook story merged with global Storybook // options. Validation error messages will be added to the provided validations set. function getSnapshotConfig(story, config, invalid) { - let { id, ...options } = PercyConfig.migrate(story, '/storybook'); + let { id, importPath, ...options } = PercyConfig.migrate(story, '/storybook'); let errors = PercyConfig.validate(options, '/storybook'); for (let e of (errors || [])) invalid.set(e.path, e.message); - return PercyConfig.merge([config, options, { id }], (path, prev, next) => { + return PercyConfig.merge([config, options, { id, importPath }], (path, prev, next) => { // normalize, but do not merge include or exclude options if (path.length === 1 && ['include', 'exclude'].includes(path[0])) { return [path, [].concat(next).filter(Boolean)]; @@ -256,9 +256,10 @@ function needsFreshPage(previousStory) { } // Process a single story and capture its DOM -async function* processStory(page, story, previewResource, percy, flags, log) { - // Extract story details - let { id, args, globals, queryParams, ...options } = story; +export async function* processStory(page, story, previewResource, percy, flags, log) { + // Extract story details. importPath is internal IntelliStory plumbing used only to map a + // snapshot back to its source file — strip it here so it never leaks into the captured snapshot. + let { id, args, globals, queryParams, importPath, ...options } = story; const enableJavaScript = options.enableJavaScript ?? percy.config.snapshot.enableJavaScript; if (flags.dryRun || enableJavaScript) { @@ -284,8 +285,32 @@ async function* processStory(page, story, previewResource, percy, flags, log) { return options; } +// Filters the mapped snapshot set through IntelliStory, which only snapshots stories whose +// dependency graph changed relative to a baseline. IntelliStory is off unless explicitly +// enabled; any IntelliStory error falls back to the full snapshot set, and a bail is logged +// at info level. When failBuildOnFailure is set the error is re-thrown so the build fails +// instead of silently running the full set. The `apply` seam exists so the orchestration +// branches can be exercised in isolation by tests. +export async function applyIntelliStoryFilter( + percy, snapshots, intelliStoryConfig, buildDir, log, apply = applyIntelliStory +) { + if (!intelliStoryConfig?.enabled) return snapshots; + + try { + return await apply(percy, snapshots, intelliStoryConfig, buildDir); + } catch (e) { + if (e instanceof IntelliStoryBailError) { + log.info(e.message); + } else { + log.warn(`IntelliStory failed (${e.message}); running full snapshot set`); + } + if (intelliStoryConfig.failBuildOnFailure) throw e; + return snapshots; + } +} + // Starts the percy instance and collects Storybook snapshots, calling the callback when done -export async function* takeStorybookSnapshots(percy, callback, { baseUrl, flags }) { +export async function* takeStorybookSnapshots(percy, callback, { baseUrl, buildDir, flags }) { try { let aboutUrl = new URL('?path=/settings/about', baseUrl).href; let previewUrl = new URL('iframe.html', baseUrl).href; @@ -314,7 +339,8 @@ export async function* takeStorybookSnapshots(percy, callback, { baseUrl, flags evalStorybookStorySnapshots, { docCapture: isDocDiscoveryEnabled, - autodocCapture: isAutodocDiscoveryEnabled + autodocCapture: isAutodocDiscoveryEnabled, + intelliStory: !!storybookConfig?.intelliStory?.enabled } ), undefined, @@ -322,6 +348,10 @@ export async function* takeStorybookSnapshots(percy, callback, { baseUrl, flags ) ]); + if (stories?.diagnostics) { + log.debug(`Story extraction diagnostics: ${JSON.stringify(stories.diagnostics)}`); + } + // map stories to snapshot options let snapshots = mapStorybookSnapshots(stories, { config: storybookConfig, @@ -333,6 +363,9 @@ export async function* takeStorybookSnapshots(percy, callback, { baseUrl, flags // set storybook environment info percy.client.addEnvironmentInfo(environmentInfo); + // narrow the snapshot set to the stories affected by recent changes (IntelliStory) + snapshots = yield applyIntelliStoryFilter(percy, snapshots, storybookConfig?.intelliStory, buildDir, log); + // Track previous story state to determine when fresh pages are needed let previousStory = null; diff --git a/src/storybook.js b/src/storybook.js index b541f35d..3bfd2bbc 100644 --- a/src/storybook.js +++ b/src/storybook.js @@ -41,6 +41,7 @@ export const storybook = command('storybook', { yield* takeStorybookSnapshots(percy, () => server?.close(), { baseUrl: args.url ?? server?.address(), + buildDir: args.serve, flags }); }); diff --git a/src/utils.js b/src/utils.js index 11701215..595cbb2d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -267,7 +267,7 @@ export function evalStorybookEnvironmentInfo({ waitForXPath }) { // Evaluate and return serialized Storybook stories to snapshot /* istanbul ignore next: no instrumenting injected code */ -export function evalStorybookStorySnapshots({ waitFor }, { docCapture = false, autodocCapture = false } = {}) { +export function evalStorybookStorySnapshots({ waitFor }, { docCapture = false, autodocCapture = false, intelliStory = false } = {}) { let serialize = (what, value, invalid) => { if (what === 'include' || what === 'exclude') { return [].concat(value).filter(Boolean).map(v => v.toString()); @@ -312,14 +312,67 @@ export function evalStorybookStorySnapshots({ waitFor }, { docCapture = false, a }); } + // story path (importPath) extraction is only needed by IntelliStory, so + // skip it entirely unless IntelliStory is enabled + const resolveImportPath = (s) => { + if (!s) return undefined; + if (entries && s.id && entries[s.id]?.importPath) return entries[s.id].importPath; + // older Storybook versions / non-storyIndex sources + return s.parameters?.fileName || s.parameters?.__id || undefined; + }; + const stampImportPath = list => { + if (!intelliStory) return list; + for (const s of list) { + const ip = resolveImportPath(s); + if (ip) s.importPath = ip; + } + return list; + }; + const storiesObj = await (window.__STORYBOOK_PREVIEW__?.extract?.()); + let stories; + let source; if (storiesObj && !Array.isArray(storiesObj)) { - const stories = Object.values(storiesObj); - return stories.concat(docsEntries); + source = '__STORYBOOK_PREVIEW__.extract'; + stories = stampImportPath(Object.values(storiesObj)) + .concat(stampImportPath(docsEntries)); + } else { + source = '__STORYBOOK_STORY_STORE__.raw'; + await window.__STORYBOOK_STORY_STORE__?.extract?.(); + stories = stampImportPath(window.__STORYBOOK_STORY_STORE__.raw()); + } + + if (intelliStory) { + const sampleEntry = entries + ? (() => { + const firstId = Object.keys(entries)[0]; + if (!firstId) return null; + const e = entries[firstId]; + return { id: firstId, keys: Object.keys(e || {}), importPath: e?.importPath }; + })() + : null; + const sampleStory = stories[0] + ? { + id: stories[0].id, + importPath: stories[0].importPath, + parameterKeys: stories[0].parameters ? Object.keys(stories[0].parameters) : [], + fileName: stories[0].parameters?.fileName + } + : null; + const withImportPath = stories.filter(s => s.importPath).length; + + stories.__intelliStoryDiagnostics = { + source, + entriesPresent: !!entries, + entriesCount: entries ? Object.keys(entries).length : 0, + storiesTotal: stories.length, + storiesWithImportPath: withImportPath, + sampleEntry, + sampleStory + }; } - await window.__STORYBOOK_STORY_STORE__?.extract?.(); - return window.__STORYBOOK_STORY_STORE__.raw(); + return stories; }, 5000).catch(() => Promise.reject(new Error( 'Storybook object not found on the window. ' + 'Open Storybook and check the console for errors.' @@ -330,13 +383,15 @@ export function evalStorybookStorySnapshots({ waitFor }, { docCapture = false, a name: story.kind ? `${story.kind}: ${story.name}` : `${story.title}: ${story.name}`, ...story.parameters?.percy, id: story.id, + ...(intelliStory ? { importPath: story.importPath } : {}), type: story.type ? story.type : 'story', tags: story.tags }, invalid)); return { invalid: Array.from(invalid), - data + data, + ...(intelliStory ? { diagnostics: stories.__intelliStoryDiagnostics } : {}) }; }); } diff --git a/test/snapshots.test.js b/test/snapshots.test.js index c89f6a74..8497d96b 100644 --- a/test/snapshots.test.js +++ b/test/snapshots.test.js @@ -1,4 +1,7 @@ import * as utils from '../src/utils.js'; +import { IntelliStoryBailError, PercyConfig } from '@percy/cli-command'; +import * as CoreConfig from '@percy/core/config'; +import { applyIntelliStoryFilter, processStory } from '../src/snapshots.js'; describe('captureDOM behavior', () => { let page, percy, log, previewResource, captureDOM; @@ -290,3 +293,130 @@ describe('takeStorybookSnapshots behaviour', () => { delete process.env.PERCY_STORYBOOK_AUTODOC_CAPTURE; }); }); + +describe('applyIntelliStoryFilter (IntelliStory orchestration)', () => { + let percy, log, snapshots, buildDir; + + beforeEach(() => { + percy = { client: {} }; + log = { + info: jasmine.createSpy('info'), + warn: jasmine.createSpy('warn'), + debug: jasmine.createSpy('debug') + }; + snapshots = [ + { name: 'A', importPath: './a.stories.js' }, + { name: 'B', importPath: './b.stories.js' } + ]; + buildDir = './build'; + }); + + it('returns the full set unchanged when IntelliStory is disabled', async () => { + let apply = jasmine.createSpy('apply'); + let result = await applyIntelliStoryFilter(percy, snapshots, { enabled: false }, buildDir, log, apply); + expect(result).toBe(snapshots); + expect(apply).not.toHaveBeenCalled(); + }); + + it('returns the full set unchanged when there is no IntelliStory config', async () => { + let apply = jasmine.createSpy('apply'); + let result = await applyIntelliStoryFilter(percy, snapshots, undefined, buildDir, log, apply); + expect(result).toBe(snapshots); + expect(apply).not.toHaveBeenCalled(); + }); + + it('reassigns snapshots to the filtered set on success', async () => { + let filtered = [snapshots[0]]; + let config = { enabled: true, baseline: 'main' }; + let apply = jasmine.createSpy('apply').and.returnValue(Promise.resolve(filtered)); + + let result = await applyIntelliStoryFilter(percy, snapshots, config, buildDir, log, apply); + + expect(apply).toHaveBeenCalledWith(percy, snapshots, config, buildDir); + expect(result).toBe(filtered); + expect(log.info).not.toHaveBeenCalled(); + expect(log.warn).not.toHaveBeenCalled(); + }); + + it('logs a bail at info level and falls back to the full set', async () => { + let apply = jasmine.createSpy('apply') + .and.callFake(() => Promise.reject(new IntelliStoryBailError('nothing changed'))); + + let result = await applyIntelliStoryFilter(percy, snapshots, { enabled: true }, buildDir, log, apply); + + expect(log.info).toHaveBeenCalledWith('nothing changed'); + expect(log.warn).not.toHaveBeenCalled(); + expect(result).toBe(snapshots); + }); + + it('warns and falls back to the full set on a generic error', async () => { + let apply = jasmine.createSpy('apply') + .and.callFake(() => Promise.reject(new Error('boom'))); + + let result = await applyIntelliStoryFilter(percy, snapshots, { enabled: true }, buildDir, log, apply); + + expect(log.warn).toHaveBeenCalledWith('IntelliStory failed (boom); running full snapshot set'); + expect(log.info).not.toHaveBeenCalled(); + expect(result).toBe(snapshots); + }); + + it('re-throws a generic error when failBuildOnFailure is set', async () => { + let err = new Error('boom'); + let apply = jasmine.createSpy('apply').and.callFake(() => Promise.reject(err)); + + await expectAsync( + applyIntelliStoryFilter(percy, snapshots, { enabled: true, failBuildOnFailure: true }, buildDir, log, apply) + ).toBeRejectedWith(err); + + expect(log.warn).toHaveBeenCalled(); + }); + + it('re-throws a bail when failBuildOnFailure is set', async () => { + let err = new IntelliStoryBailError('nothing changed'); + let apply = jasmine.createSpy('apply').and.callFake(() => Promise.reject(err)); + + await expectAsync( + applyIntelliStoryFilter(percy, snapshots, { enabled: true, failBuildOnFailure: true }, buildDir, log, apply) + ).toBeRejectedWith(err); + + expect(log.info).toHaveBeenCalledWith('nothing changed'); + }); +}); + +describe('processStory importPath stripping', () => { + let page, percy, log, previewResource; + + beforeEach(() => { + // processStory validates against the core '/snapshot/dom' schema; other suites reset the + // shared schema registry, so (re)register it here to keep this test order-independent. + PercyConfig.addSchema(CoreConfig.schemas); + page = { eval: jasmine.createSpy('eval') }; + percy = { + config: { snapshot: { enableJavaScript: false } }, + snapshot: jasmine.createSpy('snapshot') + }; + log = { debug: jasmine.createSpy('debug'), warn: jasmine.createSpy('warn') }; + previewResource = { content: 'preview' }; + }); + + it('strips importPath so it never reaches the captured snapshot options', async () => { + let story = { + id: 'button--primary', + name: 'Button: Primary', + importPath: './src/Button.stories.js', + url: 'http://localhost:6006/iframe.html?id=button--primary' + }; + + // dry-run path uses the preview DOM and avoids page.eval / captureDOM + let gen = processStory(page, story, previewResource, percy, { dryRun: true }, log); + let { value: options } = await gen.next(); + + expect(Object.prototype.hasOwnProperty.call(options, 'importPath')).toBe(false); + expect(options.name).toBe('Button: Primary'); + expect(options.domSnapshot).toBe('preview'); + // processStory returns the options the caller hands to percy.snapshot(); the absence of + // importPath here guarantees the internal plumbing never leaks into a snapshot + expect(page.eval).not.toHaveBeenCalled(); + expect(percy.snapshot).not.toHaveBeenCalled(); + }); +}); diff --git a/test/utils.test.js b/test/utils.test.js index 5474daac..0a4c2320 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -822,6 +822,155 @@ describe('evalStorybookStorySnapshots', () => { }); }); +describe('evalStorybookStorySnapshots importPath + diagnostics (IntelliStory)', () => { + const waitFor = fn => Promise.resolve(fn()); + + const setPreview = ({ extract, entries }) => { + global.window = { + __STORYBOOK_PREVIEW__: { + ready: () => Promise.resolve(), + extract, + storyStoreValue: entries === undefined + ? undefined + : { storyIndex: { entries } } + } + }; + }; + + afterEach(() => { + delete global.window; + }); + + it('stamps importPath from storyIndex entries onto each story', async () => { + setPreview({ + extract: () => ({ + 'button--primary': { id: 'button--primary', kind: 'Button', name: 'Primary', parameters: {} } + }), + entries: { + 'button--primary': { id: 'button--primary', importPath: './src/Button.stories.js' } + } + }); + + const { data } = await utils.evalStorybookStorySnapshots({ waitFor }, { intelliStory: true }); + expect(data[0].importPath).toBe('./src/Button.stories.js'); + }); + + it('falls back to parameters.fileName when the entry has no importPath', async () => { + setPreview({ + extract: () => ({ + 'button--primary': { + id: 'button--primary', + kind: 'Button', + name: 'Primary', + parameters: { fileName: './fallback/Button.stories.js' } + } + }), + entries: { 'button--primary': { id: 'button--primary' } } + }); + + const { data } = await utils.evalStorybookStorySnapshots({ waitFor }, { intelliStory: true }); + expect(data[0].importPath).toBe('./fallback/Button.stories.js'); + }); + + it('falls back to parameters.__id when neither entry importPath nor fileName exist', async () => { + setPreview({ + extract: () => ({ + 'button--primary': { + id: 'button--primary', + kind: 'Button', + name: 'Primary', + parameters: { __id: './legacy/Button.stories.js' } + } + }), + entries: undefined + }); + + const { data } = await utils.evalStorybookStorySnapshots({ waitFor }, { intelliStory: true }); + expect(data[0].importPath).toBe('./legacy/Button.stories.js'); + }); + + it('leaves importPath undefined when no source resolves one', async () => { + setPreview({ + extract: () => ({ + 'button--primary': { id: 'button--primary', kind: 'Button', name: 'Primary', parameters: {} } + }), + entries: undefined + }); + + const { data, diagnostics } = await utils.evalStorybookStorySnapshots({ waitFor }, { intelliStory: true }); + expect(data[0].importPath).toBeUndefined(); + expect(diagnostics.storiesWithImportPath).toBe(0); + }); + + it('reports diagnostics for the preview-extract source', async () => { + setPreview({ + extract: () => ({ + 'button--primary': { id: 'button--primary', kind: 'Button', name: 'Primary', parameters: {} } + }), + entries: { + 'button--primary': { id: 'button--primary', importPath: './src/Button.stories.js' } + } + }); + + const { diagnostics } = await utils.evalStorybookStorySnapshots({ waitFor }, { intelliStory: true }); + expect(diagnostics).toEqual({ + source: '__STORYBOOK_PREVIEW__.extract', + entriesPresent: true, + entriesCount: 1, + storiesTotal: 1, + storiesWithImportPath: 1, + sampleEntry: { + id: 'button--primary', + keys: ['id', 'importPath'], + importPath: './src/Button.stories.js' + }, + sampleStory: { + id: 'button--primary', + importPath: './src/Button.stories.js', + parameterKeys: [], + fileName: undefined + } + }); + }); + + it('reports the story-store source when extract yields an array', async () => { + global.window = { + __STORYBOOK_PREVIEW__: { + ready: () => Promise.resolve(), + extract: () => [], + storyStoreValue: { storyIndex: { entries: {} } } + }, + __STORYBOOK_STORY_STORE__: { + extract: () => Promise.resolve(), + raw: () => [{ + id: 's--1', kind: 'K', name: 'N', parameters: { fileName: './K.stories.js' } + }] + } + }; + + const { data, diagnostics } = await utils.evalStorybookStorySnapshots({ waitFor }, { intelliStory: true }); + expect(diagnostics.source).toBe('__STORYBOOK_STORY_STORE__.raw'); + expect(diagnostics.sampleEntry).toBeNull(); + expect(data[0].importPath).toBe('./K.stories.js'); + }); + + it('does not extract importPath or diagnostics when IntelliStory is disabled', async () => { + setPreview({ + extract: () => ({ + 'button--primary': { id: 'button--primary', kind: 'Button', name: 'Primary', parameters: {} } + }), + entries: { + 'button--primary': { id: 'button--primary', importPath: './src/Button.stories.js' } + } + }); + + const { data, diagnostics } = await utils.evalStorybookStorySnapshots({ waitFor }); + expect(data[0].importPath).toBeUndefined(); + expect(Object.prototype.hasOwnProperty.call(data[0], 'importPath')).toBe(false); + expect(diagnostics).toBeUndefined(); + }); +}); + describe('evalSetCurrentStory event handling', () => { let channel, waitFor; From a480c608f3b1e80b31ed29a1ad8c101c9824ded6 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Fri, 17 Jul 2026 00:43:31 +0530 Subject: [PATCH 2/6] remove yap --- .harness-docs.json | 65 +++++++++++++++++++++++++++++++++++++++++++ bstack-ai-harness.yml | 41 +++++++++++++++++++++++++++ src/snapshots.js | 9 ------ src/utils.js | 3 -- 4 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 .harness-docs.json create mode 100644 bstack-ai-harness.yml diff --git a/.harness-docs.json b/.harness-docs.json new file mode 100644 index 00000000..d0f4cec2 --- /dev/null +++ b/.harness-docs.json @@ -0,0 +1,65 @@ +{ + "version": 1, + "files": { + ".claude/knowledge/org/SECURITY-POLICY.md": { + "hash": "4c3147c788dd04b5f4b99dddb403927f535bd6bffa9c5aad0a7418998ec044da", + "stack": "stack-org" + }, + ".claude/knowledge/org/coding-guidelines.md": { + "hash": "d40302bc4c084317a12a15ecbe9c106c4432ec1ad37876325058b00147d2bed1", + "stack": "stack-org" + }, + ".claude/knowledge/org/security-rules.md": { + "hash": "36631517cfc53b85d236f324504793280242ea4d34164346678ecab8bf0bf2b8", + "stack": "stack-org" + }, + ".claude/knowledge/domain/DEPENDENCIES.md": { + "hash": "26c9341285b60469da59e08c296735e6c51eccd6dfe984601a767cc1b33b9075", + "stack": "stack-domain-percy-cli" + }, + ".claude/knowledge/domain/DEPLOYMENT.md": { + "hash": "9fa7a63b827c4ff53f5357a2b7d648a0e20eb5ad4a2c81fb03c9ec2b41c9b0bd", + "stack": "stack-domain-percy-cli" + }, + ".claude/knowledge/domain/ERROR-CATALOG.md": { + "hash": "b04a2847dcc6ee02cde4bcbccf1be860bc23f7c285f9c719ade59108be585fd0", + "stack": "stack-domain-percy-cli" + }, + ".claude/knowledge/domain/FEATURE-FLAGS.md": { + "hash": "991d1d789fcbb3d4a7b9d0e662a9531386496e1f6e8dff976297401d9ffa1603", + "stack": "stack-domain-percy-cli" + }, + ".claude/knowledge/domain/GLOSSARY.md": { + "hash": "1d3faf72dbdda6bb9eec4991b3585a6e82c958ac2123badf966fcc2012cc9115", + "stack": "stack-domain-percy-cli" + }, + ".claude/knowledge/domain/PACKAGE-MAP.md": { + "hash": "2414b310e22c4b92033dc168b1db7386b5f946b05068e936da27fb883a61dbef", + "stack": "stack-domain-percy-cli" + }, + ".claude/knowledge/domain/PERFORMANCE.md": { + "hash": "18e8e876cf377b2c875576549ee21fc7515cd8dcfcfbb35bcb88a864ca37519b", + "stack": "stack-domain-percy-cli" + }, + ".claude/knowledge/domain/PRODUCT.md": { + "hash": "47e874139b1bf48c899d204dbb33b51ef592636e9b6181cadb25e8db740aa82b", + "stack": "stack-domain-percy-cli" + }, + ".claude/knowledge/domain/SETUP.md": { + "hash": "08b06b5e7d159a6bba0d1777c3ebd81191809d16e01198cd56f1fdeab5725057", + "stack": "stack-domain-percy-cli" + }, + ".claude/knowledge/domain/TESTING.md": { + "hash": "8d48ad0b562178dc6a9ac203bb2ed73942da89fae9108ca4086c97edcc970bbf", + "stack": "stack-domain-percy-cli" + }, + ".claude/knowledge/domain/learnings.md": { + "hash": "b1b7adfe85c7fae7c9495d876ada349ff10542d8badac1ee55045e631f883c2d", + "stack": "stack-domain-percy-cli" + }, + ".claude/knowledge/lang/nodejs/nodejs-conventions.md": { + "hash": "00e051d6e82975af53728c06aa4602c1e55c6af92a7ef34042b3fc4e70c35565", + "stack": "stack-lang-nodejs" + } + } +} diff --git a/bstack-ai-harness.yml b/bstack-ai-harness.yml new file mode 100644 index 00000000..25341b6c --- /dev/null +++ b/bstack-ai-harness.yml @@ -0,0 +1,41 @@ +schema: 1 +packageVersion: "1.1.182" +stacks: + - stack-org + - stack-domain-percy-cli + - stack-lang-nodejs +managedFiles: + - .claude/skills/stack:pr-review/SKILL.md + - .claude/skills/stack:security-review/SKILL.md + - .claude/agents/stack:code-reviewer.md + - .claude/agents/stack:security-auditor.md + - .claude/skills/stack:percy-cli-code-review/SKILL.md + - .claude/skills/stack:percy-cli-debugging/SKILL.md + - .claude/skills/stack:percy-cli-feature-dev/SKILL.md + - .claude/agents/stack:percy-cli-code-reviewer.md + - .claude/agents/stack:percy-cli-feature-builder.md + - .claude/agents/stack:percy-cli-security-auditor.md + - .claude/agents/stack:percy-cli-version-bump.md + - .claude/rules/api-design.md + - .claude/rules/commit-conventions.md + - .claude/rules/database-migrations.md + - .claude/rules/frontend-components.md + - .claude/rules/security.md + - .claude/knowledge/org/SECURITY-POLICY.md + - .claude/knowledge/org/coding-guidelines.md + - .claude/knowledge/org/security-rules.md + - .claude/knowledge/domain/DEPENDENCIES.md + - .claude/knowledge/domain/DEPLOYMENT.md + - .claude/knowledge/domain/ERROR-CATALOG.md + - .claude/knowledge/domain/FEATURE-FLAGS.md + - .claude/knowledge/domain/GLOSSARY.md + - .claude/knowledge/domain/PACKAGE-MAP.md + - .claude/knowledge/domain/PERFORMANCE.md + - .claude/knowledge/domain/PRODUCT.md + - .claude/knowledge/domain/SETUP.md + - .claude/knowledge/domain/TESTING.md + - .claude/knowledge/domain/learnings.md + - .claude/knowledge/lang/nodejs/nodejs-conventions.md + - CLAUDE.md + - .claude/stack-harness.yml + - .claude/settings.json diff --git a/src/snapshots.js b/src/snapshots.js index 5ca571c4..f1ca9bbd 100644 --- a/src/snapshots.js +++ b/src/snapshots.js @@ -257,8 +257,6 @@ function needsFreshPage(previousStory) { // Process a single story and capture its DOM export async function* processStory(page, story, previewResource, percy, flags, log) { - // Extract story details. importPath is internal IntelliStory plumbing used only to map a - // snapshot back to its source file — strip it here so it never leaks into the captured snapshot. let { id, args, globals, queryParams, importPath, ...options } = story; const enableJavaScript = options.enableJavaScript ?? percy.config.snapshot.enableJavaScript; @@ -285,12 +283,6 @@ export async function* processStory(page, story, previewResource, percy, flags, return options; } -// Filters the mapped snapshot set through IntelliStory, which only snapshots stories whose -// dependency graph changed relative to a baseline. IntelliStory is off unless explicitly -// enabled; any IntelliStory error falls back to the full snapshot set, and a bail is logged -// at info level. When failBuildOnFailure is set the error is re-thrown so the build fails -// instead of silently running the full set. The `apply` seam exists so the orchestration -// branches can be exercised in isolation by tests. export async function applyIntelliStoryFilter( percy, snapshots, intelliStoryConfig, buildDir, log, apply = applyIntelliStory ) { @@ -363,7 +355,6 @@ export async function* takeStorybookSnapshots(percy, callback, { baseUrl, buildD // set storybook environment info percy.client.addEnvironmentInfo(environmentInfo); - // narrow the snapshot set to the stories affected by recent changes (IntelliStory) snapshots = yield applyIntelliStoryFilter(percy, snapshots, storybookConfig?.intelliStory, buildDir, log); // Track previous story state to determine when fresh pages are needed diff --git a/src/utils.js b/src/utils.js index 595cbb2d..2b430d8b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -312,12 +312,9 @@ export function evalStorybookStorySnapshots({ waitFor }, { docCapture = false, a }); } - // story path (importPath) extraction is only needed by IntelliStory, so - // skip it entirely unless IntelliStory is enabled const resolveImportPath = (s) => { if (!s) return undefined; if (entries && s.id && entries[s.id]?.importPath) return entries[s.id].importPath; - // older Storybook versions / non-storyIndex sources return s.parameters?.fileName || s.parameters?.__id || undefined; }; const stampImportPath = list => { From 12d203ae0f4a0e91ee2b535e85fe053f29d41ad2 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Fri, 17 Jul 2026 00:48:42 +0530 Subject: [PATCH 3/6] remove harness --- .harness-docs.json | 65 ------------------------------------------- bstack-ai-harness.yml | 41 --------------------------- 2 files changed, 106 deletions(-) delete mode 100644 .harness-docs.json delete mode 100644 bstack-ai-harness.yml diff --git a/.harness-docs.json b/.harness-docs.json deleted file mode 100644 index d0f4cec2..00000000 --- a/.harness-docs.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "version": 1, - "files": { - ".claude/knowledge/org/SECURITY-POLICY.md": { - "hash": "4c3147c788dd04b5f4b99dddb403927f535bd6bffa9c5aad0a7418998ec044da", - "stack": "stack-org" - }, - ".claude/knowledge/org/coding-guidelines.md": { - "hash": "d40302bc4c084317a12a15ecbe9c106c4432ec1ad37876325058b00147d2bed1", - "stack": "stack-org" - }, - ".claude/knowledge/org/security-rules.md": { - "hash": "36631517cfc53b85d236f324504793280242ea4d34164346678ecab8bf0bf2b8", - "stack": "stack-org" - }, - ".claude/knowledge/domain/DEPENDENCIES.md": { - "hash": "26c9341285b60469da59e08c296735e6c51eccd6dfe984601a767cc1b33b9075", - "stack": "stack-domain-percy-cli" - }, - ".claude/knowledge/domain/DEPLOYMENT.md": { - "hash": "9fa7a63b827c4ff53f5357a2b7d648a0e20eb5ad4a2c81fb03c9ec2b41c9b0bd", - "stack": "stack-domain-percy-cli" - }, - ".claude/knowledge/domain/ERROR-CATALOG.md": { - "hash": "b04a2847dcc6ee02cde4bcbccf1be860bc23f7c285f9c719ade59108be585fd0", - "stack": "stack-domain-percy-cli" - }, - ".claude/knowledge/domain/FEATURE-FLAGS.md": { - "hash": "991d1d789fcbb3d4a7b9d0e662a9531386496e1f6e8dff976297401d9ffa1603", - "stack": "stack-domain-percy-cli" - }, - ".claude/knowledge/domain/GLOSSARY.md": { - "hash": "1d3faf72dbdda6bb9eec4991b3585a6e82c958ac2123badf966fcc2012cc9115", - "stack": "stack-domain-percy-cli" - }, - ".claude/knowledge/domain/PACKAGE-MAP.md": { - "hash": "2414b310e22c4b92033dc168b1db7386b5f946b05068e936da27fb883a61dbef", - "stack": "stack-domain-percy-cli" - }, - ".claude/knowledge/domain/PERFORMANCE.md": { - "hash": "18e8e876cf377b2c875576549ee21fc7515cd8dcfcfbb35bcb88a864ca37519b", - "stack": "stack-domain-percy-cli" - }, - ".claude/knowledge/domain/PRODUCT.md": { - "hash": "47e874139b1bf48c899d204dbb33b51ef592636e9b6181cadb25e8db740aa82b", - "stack": "stack-domain-percy-cli" - }, - ".claude/knowledge/domain/SETUP.md": { - "hash": "08b06b5e7d159a6bba0d1777c3ebd81191809d16e01198cd56f1fdeab5725057", - "stack": "stack-domain-percy-cli" - }, - ".claude/knowledge/domain/TESTING.md": { - "hash": "8d48ad0b562178dc6a9ac203bb2ed73942da89fae9108ca4086c97edcc970bbf", - "stack": "stack-domain-percy-cli" - }, - ".claude/knowledge/domain/learnings.md": { - "hash": "b1b7adfe85c7fae7c9495d876ada349ff10542d8badac1ee55045e631f883c2d", - "stack": "stack-domain-percy-cli" - }, - ".claude/knowledge/lang/nodejs/nodejs-conventions.md": { - "hash": "00e051d6e82975af53728c06aa4602c1e55c6af92a7ef34042b3fc4e70c35565", - "stack": "stack-lang-nodejs" - } - } -} diff --git a/bstack-ai-harness.yml b/bstack-ai-harness.yml deleted file mode 100644 index 25341b6c..00000000 --- a/bstack-ai-harness.yml +++ /dev/null @@ -1,41 +0,0 @@ -schema: 1 -packageVersion: "1.1.182" -stacks: - - stack-org - - stack-domain-percy-cli - - stack-lang-nodejs -managedFiles: - - .claude/skills/stack:pr-review/SKILL.md - - .claude/skills/stack:security-review/SKILL.md - - .claude/agents/stack:code-reviewer.md - - .claude/agents/stack:security-auditor.md - - .claude/skills/stack:percy-cli-code-review/SKILL.md - - .claude/skills/stack:percy-cli-debugging/SKILL.md - - .claude/skills/stack:percy-cli-feature-dev/SKILL.md - - .claude/agents/stack:percy-cli-code-reviewer.md - - .claude/agents/stack:percy-cli-feature-builder.md - - .claude/agents/stack:percy-cli-security-auditor.md - - .claude/agents/stack:percy-cli-version-bump.md - - .claude/rules/api-design.md - - .claude/rules/commit-conventions.md - - .claude/rules/database-migrations.md - - .claude/rules/frontend-components.md - - .claude/rules/security.md - - .claude/knowledge/org/SECURITY-POLICY.md - - .claude/knowledge/org/coding-guidelines.md - - .claude/knowledge/org/security-rules.md - - .claude/knowledge/domain/DEPENDENCIES.md - - .claude/knowledge/domain/DEPLOYMENT.md - - .claude/knowledge/domain/ERROR-CATALOG.md - - .claude/knowledge/domain/FEATURE-FLAGS.md - - .claude/knowledge/domain/GLOSSARY.md - - .claude/knowledge/domain/PACKAGE-MAP.md - - .claude/knowledge/domain/PERFORMANCE.md - - .claude/knowledge/domain/PRODUCT.md - - .claude/knowledge/domain/SETUP.md - - .claude/knowledge/domain/TESTING.md - - .claude/knowledge/domain/learnings.md - - .claude/knowledge/lang/nodejs/nodejs-conventions.md - - CLAUDE.md - - .claude/stack-harness.yml - - .claude/settings.json From 4e7f9e8815c062caaf2ade38dbe54695291aa02a Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Fri, 17 Jul 2026 06:26:21 +0530 Subject: [PATCH 4/6] account for new API requirements --- src/snapshots.js | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/snapshots.js b/src/snapshots.js index f1ca9bbd..49a064e0 100644 --- a/src/snapshots.js +++ b/src/snapshots.js @@ -1,4 +1,4 @@ -import { logger, PercyConfig, applyIntelliStory, IntelliStoryBailError } from '@percy/cli-command'; +import { logger, PercyConfig, applyIntelliStory, writeIntelliStoryTrace, IntelliStoryBailError } from '@percy/cli-command'; import { yieldAll } from '@percy/cli-command/utils'; import qs from 'qs'; import { @@ -320,6 +320,16 @@ export async function* takeStorybookSnapshots(percy, callback, { baseUrl, buildD yield percy.browser.launch(); const storybookConfig = percy.config.storybook; + const intelliStoryEnabled = !!storybookConfig?.intelliStory?.enabled; + + // IntelliStory selects snapshots server-side against the real Percy build, + // so the build must exist before any snapshots are posted. Storybook delays + // uploads (the build is otherwise created lazily on the first flush), so + // create it up front here. Skipped on dry runs, where no build is created. + if (intelliStoryEnabled && !percy.dryRun) { + yield* percy.yield.startBuild(); + } + const { isDocDiscoveryEnabled, isAutodocDiscoveryEnabled, globalDocSettings } = getDocCaptureFlagsWithRules(storybookConfig); let [environmentInfo, stories] = yield* yieldAll([ @@ -440,8 +450,28 @@ export async function* takeStorybookSnapshots(percy, callback, { baseUrl, buildD } } - // Will stop once snapshots are done processing + // Will stop once snapshots are done processing (this finalizes the build) yield* percy.yield.stop(); + + if (intelliStoryEnabled && !percy.dryRun && percy.build?.id) { + // Summarize the server-side selection outcome, derived from each snapshot + // create response code (201 kept / 204 skipped) tallied in @percy/client. + const stats = percy.client.intelliStoryStats; + if (stats) { + const kept = stats.graphKept + stats.forcedKept; + const total = kept + stats.skipped; + log.info(`IntelliStory: ${kept} of ${total} snapshots kept (${stats.graphKept} via affected-graph, ${stats.forcedKept} via missing/failed/rejected baseline)`); + } + + // After finalize, the IntelliStory graph job's data is available from job + // status, so fetch it once more and write the trace. Never let a trace + // failure fail an otherwise-successful build. + try { + yield writeIntelliStoryTrace(percy, storybookConfig.intelliStory, log); + } catch (e) { + log.debug(`IntelliStory: failed to write trace after finalize: ${e.message}`); + } + } } catch (error) { // force stop and re-throw await percy.stop(true); From de97c06052b77e5d9d32de2bd0174c10d302566f Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Mon, 20 Jul 2026 17:34:02 +0530 Subject: [PATCH 5/6] update storybook message --- src/snapshots.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/snapshots.js b/src/snapshots.js index 49a064e0..258d98cb 100644 --- a/src/snapshots.js +++ b/src/snapshots.js @@ -455,12 +455,11 @@ export async function* takeStorybookSnapshots(percy, callback, { baseUrl, buildD if (intelliStoryEnabled && !percy.dryRun && percy.build?.id) { // Summarize the server-side selection outcome, derived from each snapshot - // create response code (201 kept / 204 skipped) tallied in @percy/client. + // create response (skipped-via-smartsnap) tallied in @percy/client. const stats = percy.client.intelliStoryStats; if (stats) { - const kept = stats.graphKept + stats.forcedKept; - const total = kept + stats.skipped; - log.info(`IntelliStory: ${kept} of ${total} snapshots kept (${stats.graphKept} via affected-graph, ${stats.forcedKept} via missing/failed/rejected baseline)`); + const total = stats.kept + stats.skipped; + log.info(`IntelliStory: ${stats.kept} of ${total} snapshots kept`); } // After finalize, the IntelliStory graph job's data is available from job From 8602e88869811c79f76e6f9e8236fd0da1f40232 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Fri, 24 Jul 2026 16:29:37 +0530 Subject: [PATCH 6/6] improve the message --- src/snapshots.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snapshots.js b/src/snapshots.js index 258d98cb..db444472 100644 --- a/src/snapshots.js +++ b/src/snapshots.js @@ -459,7 +459,7 @@ export async function* takeStorybookSnapshots(percy, callback, { baseUrl, buildD const stats = percy.client.intelliStoryStats; if (stats) { const total = stats.kept + stats.skipped; - log.info(`IntelliStory: ${stats.kept} of ${total} snapshots kept`); + log.info(`IntelliStory: filtered out ${stats.skipped} of ${total} snapshots with no detected changes; the remaining ${stats.kept} were processed.`); } // After finalize, the IntelliStory graph job's data is available from job