diff --git a/.changeset/stale-reuse-context-and-relpath.md b/.changeset/stale-reuse-context-and-relpath.md new file mode 100644 index 00000000..78b634ba --- /dev/null +++ b/.changeset/stale-reuse-context-and-relpath.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': patch +--- + +Make `intent stale` reuse its per-run filesystem cache across target discovery, workspace enumeration, and skill-policy filtering, and compute each package's relative path to `_artifacts` once per package instead of once per artifact entry; behavior does not change. diff --git a/packages/intent/src/commands/stale.ts b/packages/intent/src/commands/stale.ts index 60507f32..ccdb637a 100644 --- a/packages/intent/src/commands/stale.ts +++ b/packages/intent/src/commands/stale.ts @@ -7,7 +7,7 @@ import { import { resolveProjectContext } from '../core/project-context.js' import { createIntentFsCache } from '../discovery/fs-cache.js' import { - isSourcePermitted, + compileSkillSourcePolicy, readSkillSourcesConfig, } from '../core/source-policy.js' import { isCliFailure } from '../shared/cli-error.js' @@ -15,6 +15,7 @@ import { detectIntentCommandPackageManager, formatIntentCommand, } from '../shared/command-runner.js' +import type { IntentFsCache } from '../discovery/fs-cache.js' import type { StalenessReport } from '../shared/types.js' export interface StaleCommandOptions { @@ -26,7 +27,10 @@ export interface StaleCommandOptions { export async function runStaleCommand( targetDir: string | undefined, options: StaleCommandOptions, - resolveStaleTargets: (targetDir?: string) => Promise<{ + resolveStaleTargets: ( + targetDir?: string, + fsCache?: IntentFsCache, + ) => Promise<{ reports: Array workflowAdvisories?: Array }>, @@ -36,9 +40,10 @@ export async function runStaleCommand( return } + const fsCache = createIntentFsCache() const { reports: unfilteredReports, workflowAdvisories = [] } = - await resolveStaleTargets(targetDir) - const reports = filterStaleReportSkills(unfilteredReports, targetDir) + await resolveStaleTargets(targetDir, fsCache) + const reports = filterStaleReportSkills(unfilteredReports, targetDir, fsCache) if (options.json) { console.log(JSON.stringify(reports, null, 2)) @@ -114,7 +119,10 @@ export async function runStaleCommand( async function runGithubReview( targetDir: string | undefined, options: StaleCommandOptions, - resolveStaleTargets: (targetDir?: string) => Promise<{ + resolveStaleTargets: ( + targetDir?: string, + fsCache?: IntentFsCache, + ) => Promise<{ reports: Array workflowAdvisories?: Array }>, @@ -128,9 +136,14 @@ async function runGithubReview( const packageLabel = options.packageLabel ?? 'workspace' try { + const fsCache = createIntentFsCache() const { reports: unfilteredReports, workflowAdvisories = [] } = - await resolveStaleTargets(targetDir) - const reports = filterStaleReportSkills(unfilteredReports, targetDir) + await resolveStaleTargets(targetDir, fsCache) + const reports = filterStaleReportSkills( + unfilteredReports, + targetDir, + fsCache, + ) const items = [ ...collectStaleReviewItems(reports), ...createWorkflowAdvisoryReviewItems(packageLabel, workflowAdvisories), @@ -157,11 +170,13 @@ async function runGithubReview( function filterStaleReportSkills( reports: Array, targetDir: string | undefined, + fsCache = createIntentFsCache(), ): Array { const cwd = resolve(process.cwd(), targetDir ?? process.cwd()) - const context = resolveProjectContext({ cwd }) - const fsCache = createIntentFsCache() - const config = readSkillSourcesConfig(cwd, context, fsCache) + const context = resolveProjectContext({ cwd, fsCache }) + const sourcePolicy = compileSkillSourcePolicy( + readSkillSourcesConfig(cwd, context, fsCache), + ) const excludeMatchers = compileExcludePatterns( getEffectiveExcludePatterns({}, context, fsCache), ) @@ -170,7 +185,7 @@ function filterStaleReportSkills( ...report, skills: report.skills.filter( (skill) => - isSourcePermitted(config, report.library, undefined, skill.name) && + sourcePolicy.permits(report.library, undefined, skill.name) && !isSkillExcluded(report.library, skill.name, excludeMatchers), ), })) diff --git a/packages/intent/src/commands/support.ts b/packages/intent/src/commands/support.ts index 5ae96d9c..7500d747 100644 --- a/packages/intent/src/commands/support.ts +++ b/packages/intent/src/commands/support.ts @@ -161,14 +161,15 @@ export function printDebugInfo( export async function resolveStaleTargets( targetDir?: string, + fsCache = createIntentFsCache(), ): Promise { - const fsCache = createIntentFsCache() const resolvedRoot = targetDir ? resolve(process.cwd(), targetDir) : process.cwd() const context = resolveProjectContext({ cwd: process.cwd(), targetPath: targetDir, + fsCache, }) const advisoryRoot = context.workspaceRoot ?? context.packageRoot ?? resolvedRoot @@ -196,9 +197,10 @@ export async function resolveStaleTargets( } } - const workspaceRoot = findWorkspaceRoot(resolvedRoot) + const workspaceRoot = + context.workspaceRoot ?? findWorkspaceRoot(resolvedRoot, fsCache) if (workspaceRoot) { - const packageDirs = findWorkspacePackages(workspaceRoot) + const packageDirs = findWorkspacePackages(workspaceRoot, fsCache) const packageDirsWithSkills = packageDirs.filter( (dir) => fsCache.findSkillFiles(join(dir, 'skills')).length > 0, ) diff --git a/packages/intent/src/core/source-policy.ts b/packages/intent/src/core/source-policy.ts index f74c37f0..5d565bd4 100644 --- a/packages/intent/src/core/source-policy.ts +++ b/packages/intent/src/core/source-policy.ts @@ -307,12 +307,11 @@ export function scanForPolicedIntents(params: { context?: ProjectContext }): PolicedScan { const { cwd, scanOptions, coreOptions } = params - const context = params.context ?? resolveProjectContext({ cwd }) - const audience = detectIntentAudience(coreOptions.audience) - const fsCache = (scanOptions as ScanOptions & { fsCache?: IntentFsCache }).fsCache ?? createIntentFsCache() + const context = params.context ?? resolveProjectContext({ cwd, fsCache }) + const audience = detectIntentAudience(coreOptions.audience) const cachedScanOptions = { ...scanOptions, fsCache } const scanResult = scanForIntents(cwd, cachedScanOptions) const config = readSkillSourcesConfig(cwd, context, fsCache) diff --git a/packages/intent/src/staleness/check.ts b/packages/intent/src/staleness/check.ts index 2abea8f7..6a4cd584 100644 --- a/packages/intent/src/staleness/check.ts +++ b/packages/intent/src/staleness/check.ts @@ -211,11 +211,9 @@ function sameStringList( function artifactPackageMatches( artifact: IntentArtifactSkill, - packageDir: string, packageName: string, - artifactRoot: string, + relPackageDir: string, ): boolean { - const relPackageDir = getRelativePackageDir(artifactRoot, packageDir) if (!relPackageDir) return true if (artifact.packages.includes(packageName)) return true @@ -309,8 +307,10 @@ function buildArtifactSignals({ artifactPath: warning.artifactPath, })) + const relPackageDir = getRelativePackageDir(artifactRoot, packageDir) + for (const artifact of artifacts.skills) { - if (!artifactPackageMatches(artifact, packageDir, library, artifactRoot)) { + if (!artifactPackageMatches(artifact, library, relPackageDir)) { continue } @@ -384,11 +384,9 @@ function buildArtifactSignals({ function artifactCoversPackage( artifact: IntentArtifactSkill, - packageDir: string, packageName: string, - artifactRoot: string, + relPackageDir: string, ): boolean { - const relPackageDir = getRelativePackageDir(artifactRoot, packageDir) return ( artifact.packages.includes(packageName) || artifact.packages.includes(relPackageDir) || @@ -400,11 +398,9 @@ function artifactCoversPackage( function artifactIgnoresPackage( artifacts: IntentArtifactSet, - packageDir: string, packageName: string, - artifactRoot: string, + relPackageDir: string, ): boolean { - const relPackageDir = getRelativePackageDir(artifactRoot, packageDir) return artifacts.ignoredPackages.some( (ignored) => ignored.packageName === packageName || @@ -430,19 +426,18 @@ export function buildWorkspaceCoverageSignals({ const packageJson = fsCache.readPackageJson(packageDir) if (packageJson?.private === true) continue - const packageName = readPackageName(packageDir, fsCache) - if ( - artifactIgnoresPackage(artifacts, packageDir, packageName, artifactRoot) - ) { - continue - } - const hasGeneratedSkill = fsCache.findSkillFiles(join(packageDir, 'skills')).length > 0 if (hasGeneratedSkill) continue + const packageName = readPackageName(packageDir, fsCache) + const relPackageDir = getRelativePackageDir(artifactRoot, packageDir) + if (artifactIgnoresPackage(artifacts, packageName, relPackageDir)) { + continue + } + const hasArtifactCoverage = artifacts.skills.some((artifact) => - artifactCoversPackage(artifact, packageDir, packageName, artifactRoot), + artifactCoversPackage(artifact, packageName, relPackageDir), ) if (hasArtifactCoverage) continue @@ -456,7 +451,7 @@ export function buildWorkspaceCoverageSignals({ ], needsReview: true, packageName, - packageRoot: getRelativePackageDir(artifactRoot, packageDir), + packageRoot: relPackageDir, }) } diff --git a/packages/intent/tests/repeated-work.test.ts b/packages/intent/tests/repeated-work.test.ts index 13eb4795..53c50b20 100644 --- a/packages/intent/tests/repeated-work.test.ts +++ b/packages/intent/tests/repeated-work.test.ts @@ -12,7 +12,7 @@ import { writeFileSync, } from 'node:fs' import { tmpdir } from 'node:os' -import { dirname, join, resolve } from 'node:path' +import { dirname, join, relative, resolve } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { main } from '../src/cli.js' import { @@ -38,7 +38,11 @@ vi.mock('node:fs', async (importOriginal) => { vi.mock('node:path', async (importOriginal) => { const path = await importOriginal() - return { ...path, resolve: vi.fn(path.resolve) } + return { + ...path, + relative: vi.fn(path.relative), + resolve: vi.fn(path.resolve), + } }) let root: string @@ -350,6 +354,18 @@ describe('command work budgets', () => { signals: [], }) expect(reports[0].skills).toHaveLength(1) + expect( + vi + .mocked(readFileSync) + .mock.calls.filter(([path]) => path === join(root, 'package.json')), + ).toHaveLength(1) + expect( + vi + .mocked(readFileSync) + .mock.calls.filter( + ([path]) => path === join(root, 'pnpm-workspace.yaml'), + ), + ).toHaveLength(1) expect( vi .mocked(readFileSync) @@ -389,6 +405,23 @@ describe('command work budgets', () => { ) expect(await main(['stale', '--json'])).toBe(0) + expect( + vi + .mocked(readFileSync) + .mock.calls.filter( + ([path]) => path === join(root, 'pnpm-workspace.yaml'), + ), + ).toHaveLength(1) + expect( + vi + .mocked(readFileSync) + .mock.calls.filter(([path]) => path === join(root, 'package.json')), + ).toHaveLength(1) + expect( + vi + .mocked(readdirSync) + .mock.calls.filter(([path]) => path === join(root, 'packages')), + ).toHaveLength(1) expect( vi .mocked(readFileSync) @@ -405,6 +438,11 @@ describe('command work budgets', () => { .mocked(readdirSync) .mock.calls.filter(([path]) => path === join(dir, 'skills')), ).toHaveLength(1) + expect( + vi + .mocked(relative) + .mock.calls.filter(([from, to]) => from === root && to === dir), + ).toHaveLength(1) } const reports = JSON.parse( vi.mocked(console.log).mock.calls.at(-1)![0] as string,