diff --git a/.changeset/cuddly-games-cheer.md b/.changeset/cuddly-games-cheer.md new file mode 100644 index 0000000..eee9d94 --- /dev/null +++ b/.changeset/cuddly-games-cheer.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': patch +--- + +Adds package source-kind metadata to scanner results, tightens scan stats to match the runtime contract, and adds focused markdown destination rewrite coverage while preserving the current name-based allowlist behavior. diff --git a/packages/intent/src/core.ts b/packages/intent/src/core.ts index 1490696..bf593cd 100644 --- a/packages/intent/src/core.ts +++ b/packages/intent/src/core.ts @@ -147,7 +147,7 @@ export function listIntentSkills( warningCount: result.warnings.length, noticeCount: result.notices.length, conflictCount: result.conflicts.length, - scan: scan.stats ?? fsCache.getStats(), + scan: scan.stats, } } @@ -344,7 +344,7 @@ function resolveIntentSkillInCwd( excludes: excludePatterns, resolution: 'full-scan', resolved, - scan: scanResult.stats ?? fsCache.getStats(), + scan: scanResult.stats, scope, }) : undefined, diff --git a/packages/intent/src/discovery/register.ts b/packages/intent/src/discovery/register.ts index b734e95..b606830 100644 --- a/packages/intent/src/discovery/register.ts +++ b/packages/intent/src/discovery/register.ts @@ -23,6 +23,7 @@ export interface CreatePackageRegistrarOptions { deriveIntentConfig: (pkgJson: PackageJson) => IntentConfig | null discoverSkills: (skillsDir: string, packageName: string) => Array getPackageDepth: (packageRoot: string, projectRoot: string) => number + getPackageKind: (packageRoot: string) => IntentPackage['kind'] packageIndexes: Map packages: Array projectRoot: string @@ -120,6 +121,7 @@ export function createPackageRegistrar(opts: CreatePackageRegistrarOptions) { intent, skills, packageRoot: dirPath, + kind: opts.getPackageKind(dirPath), source, } const existingIndex = opts.packageIndexes.get(name) diff --git a/packages/intent/src/scanner.ts b/packages/intent/src/scanner.ts index 51baf42..22ed833 100644 --- a/packages/intent/src/scanner.ts +++ b/packages/intent/src/scanner.ts @@ -19,7 +19,10 @@ import { } from './utils.js' import { createIntentFsCache } from './fs-cache.js' import { detectPackageManager } from './package-manager.js' -import { findWorkspaceRoot } from './workspace-patterns.js' +import { + findWorkspacePackages, + findWorkspaceRoot, +} from './workspace-patterns.js' import type { IntentFsCache } from './fs-cache.js' import type { ReadFs } from './utils.js' import type { @@ -451,6 +454,28 @@ function getScanScope(options: ScanOptions): ScanScope { return options.scope ?? (options.includeGlobal ? 'local-and-global' : 'local') } +function createWorkspacePackageKeySet( + workspaceRoot: string | null, + getFsIdentity: (path: string) => string, +): Set { + if (!workspaceRoot) return new Set() + + return new Set( + findWorkspacePackages(workspaceRoot).map((dir) => getFsIdentity(dir)), + ) +} + +function createPackageKindResolver( + workspacePackageKeys: Set, + getFsIdentity: (path: string) => string, +): (packageRoot: string) => IntentPackage['kind'] { + return (packageRoot: string): IntentPackage['kind'] => { + return workspacePackageKeys.has(getFsIdentity(packageRoot)) + ? 'workspace' + : 'npm' + } +} + export function scanForIntents( root?: string, options: ScanOptions = {}, @@ -495,6 +520,11 @@ export function scanForIntents( >() let pnpApi: PnpApi | null | undefined + const getPackageKind = createPackageKindResolver( + createWorkspacePackageKeySet(workspaceRoot, fsCache.getFsIdentity), + fsCache.getFsIdentity, + ) + function getPnpApi(): PnpApi | null { if (scanScope === 'global') return null if (pnpApi === undefined) { @@ -545,6 +575,7 @@ export function scanForIntents( deriveIntentConfig, discoverSkills: (skillsDir) => discoverSkills(skillsDir, fsCache), getPackageDepth, + getPackageKind, getFsIdentity: fsCache.getFsIdentity, exists: fsCache.exists, packageIndexes, @@ -726,6 +757,13 @@ export function scanIntentPackageAtRoot( const warnings: Array = [] const packageIndexes = new Map() const fsCache = options.fsCache ?? createIntentFsCache() + const getPackageKind = createPackageKindResolver( + createWorkspacePackageKeySet( + findWorkspaceRoot(projectRoot), + fsCache.getFsIdentity, + ), + fsCache.getFsIdentity, + ) function readPkgJson(dirPath: string): Record | null { return fsCache.readPackageJson(dirPath) @@ -744,6 +782,7 @@ export function scanIntentPackageAtRoot( ) : (skillsDir) => discoverSkills(skillsDir, fsCache), getPackageDepth, + getPackageKind, getFsIdentity: fsCache.getFsIdentity, exists: fsCache.exists, packageIndexes, diff --git a/packages/intent/src/types.ts b/packages/intent/src/types.ts index 00e5228..994ed53 100644 --- a/packages/intent/src/types.ts +++ b/packages/intent/src/types.ts @@ -23,7 +23,7 @@ export interface ScanResult { local: NodeModulesScanTarget global: NodeModulesScanTarget } - stats?: ScanStats + stats: ScanStats } export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown' @@ -54,6 +54,7 @@ export interface IntentPackage { intent: IntentConfig skills: Array packageRoot: string + kind: 'npm' | 'workspace' source: 'local' | 'global' } diff --git a/packages/intent/tests/install-writer.test.ts b/packages/intent/tests/install-writer.test.ts index 0a76adb..09539e1 100644 --- a/packages/intent/tests/install-writer.test.ts +++ b/packages/intent/tests/install-writer.test.ts @@ -47,6 +47,7 @@ function pkg(overrides: Partial): IntentPackage { intent: { version: 1, repo: 'test/pkg', docs: 'docs/' }, skills: [], packageRoot: 'node_modules/pkg', + kind: 'npm', source: 'local', ...overrides, } @@ -73,6 +74,10 @@ function scanResult(packages: Array): ScanResult { scanned: false, }, }, + stats: { + packageJsonCacheHits: 0, + packageJsonReadCount: 0, + }, } } diff --git a/packages/intent/tests/markdown.test.ts b/packages/intent/tests/markdown.test.ts new file mode 100644 index 0000000..419d8f8 --- /dev/null +++ b/packages/intent/tests/markdown.test.ts @@ -0,0 +1,40 @@ +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { rewriteLoadedSkillMarkdownDestinations } from '../src/core/markdown.js' + +const cwd = '/repo' +const packageRoot = join(cwd, 'node_modules', 'pkg') +const skillFilePath = join(packageRoot, 'skills', 'core', 'SKILL.md') + +function rewrite(content: string): string { + return rewriteLoadedSkillMarkdownDestinations({ + content, + cwd, + packageRoot, + skillFilePath, + }) +} + +describe('rewriteLoadedSkillMarkdownDestinations', () => { + it('rewrites nested-label links while preserving query and hash suffixes', () => { + expect(rewrite('[API [v1]](docs/api.md?raw=1#setup)')).toBe( + '[API [v1]](node_modules/pkg/skills/core/docs/api.md?raw=1#setup)', + ) + }) + + it('rewrites image destinations with escaped closing parens', () => { + expect(rewrite('![Diagram](assets/flow\\).png)')).toBe( + '![Diagram](node_modules/pkg/skills/core/assets/flow\\).png)', + ) + }) + + it('preserves malformed inline links', () => { + expect(rewrite('[Broken](docs/api.md')).toBe('[Broken](docs/api.md') + }) + + it('does not rewrite links in fenced code blocks', () => { + expect(rewrite('~~~md\n[Keep](docs/api.md)\n~~~')).toBe( + '~~~md\n[Keep](docs/api.md)\n~~~', + ) + }) +}) diff --git a/packages/intent/tests/resolver.test.ts b/packages/intent/tests/resolver.test.ts index 6a6e502..78df895 100644 --- a/packages/intent/tests/resolver.test.ts +++ b/packages/intent/tests/resolver.test.ts @@ -29,6 +29,7 @@ function intentPackage( }, packageRoot: `node_modules/${overrides.name}`, skills: [skill('core')], + kind: 'npm', source: 'local', version: '1.0.0', ...overrides, @@ -64,6 +65,10 @@ function scanResult( }, packageManager: 'npm', packages, + stats: { + packageJsonCacheHits: 0, + packageJsonReadCount: 0, + }, warnings, } } diff --git a/packages/intent/tests/scanner.test.ts b/packages/intent/tests/scanner.test.ts index 735d950..289e86d 100644 --- a/packages/intent/tests/scanner.test.ts +++ b/packages/intent/tests/scanner.test.ts @@ -115,6 +115,7 @@ describe('scanForIntents', () => { const result = scanForIntents(root) expect(result.packages).toHaveLength(1) expect(result.packages[0]!.name).toBe('@tanstack/db') + expect(result.packages[0]!.kind).toBe('npm') expect(result.packages[0]!.version).toBe('0.5.2') expect(result.packages[0]!.packageRoot).toBe(pkgDir) expect(result.packages[0]!.skills).toHaveLength(1) @@ -128,7 +129,7 @@ describe('scanForIntents', () => { packageJsonCacheHits: expect.any(Number), }), ) - expect(result.stats!.packageJsonReadCount).toBeGreaterThan(0) + expect(result.stats.packageJsonReadCount).toBeGreaterThan(0) }) it('does not throw when skills exists but is not a directory', () => { @@ -1556,7 +1557,7 @@ describe('scanForIntents', () => { '@tanstack/query', '@tanstack/store', ]) - expect(result.stats!.packageJsonReadCount).toBeLessThan(10) + expect(result.stats.packageJsonReadCount).toBeLessThan(10) }) it('does not crawl package source trees during nested node_modules discovery', () => { @@ -1591,7 +1592,7 @@ describe('scanForIntents', () => { const result = scanForIntents(root) expect(result.packages).toEqual([]) - expect(result.stats!.packageJsonReadCount).toBeLessThan(4) + expect(result.stats.packageJsonReadCount).toBeLessThan(4) }) it('dedupes recursive workspace symlink paths by real package identity', () => { @@ -1639,7 +1640,8 @@ describe('scanForIntents', () => { expect(result.packages).toHaveLength(1) expect(result.packages[0]!.name).toBe('b') - expect(result.stats!.packageJsonReadCount).toBeLessThan(10) + expect(result.packages[0]!.kind).toBe('workspace') + expect(result.stats.packageJsonReadCount).toBeLessThan(10) }) it('prefers valid semver versions over invalid ones at the same depth', () => { diff --git a/packages/intent/tests/source-policy.test.ts b/packages/intent/tests/source-policy.test.ts index 4948e7c..0c565a4 100644 --- a/packages/intent/tests/source-policy.test.ts +++ b/packages/intent/tests/source-policy.test.ts @@ -32,6 +32,7 @@ function pkg(name: string, skillNames: Array): IntentPackage { intent: { version: 1, repo: 'owner/repo', docs: '' }, skills: skillNames.map(skill), packageRoot: `/root/node_modules/${name}`, + kind: 'npm', source: 'local', } }