diff --git a/.changeset/stale-per-package-version.md b/.changeset/stale-per-package-version.md new file mode 100644 index 0000000..7f20327 --- /dev/null +++ b/.changeset/stale-per-package-version.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': patch +--- + +Stop `intent stale` from reporting artifact library version drift for a skill whose `library_version` matches the current version of its own package. Monorepos that version packages independently no longer get this false warning, and a review signal raised by more than one artifact file now prints once in the text output. diff --git a/docs/cli/intent-stale.md b/docs/cli/intent-stale.md index 218ce72..9047f84 100644 --- a/docs/cli/intent-stale.md +++ b/docs/cli/intent-stale.md @@ -115,6 +115,7 @@ Reason generation: - Report header format: ` ( → ) [ drift]` - When no skill reasons exist: `All skills up-to-date` - Otherwise: one warning line per stale skill or review signal (`⚠ : , , ...`) +- Review signals that produce the same line, such as one raised by both `domain_map.yaml` and `skill_tree.yaml`, print once; JSON output keeps each signal with its `artifactPath` ## Generated review files diff --git a/packages/intent/src/commands/stale.ts b/packages/intent/src/commands/stale.ts index 0a2a230..60507f3 100644 --- a/packages/intent/src/commands/stale.ts +++ b/packages/intent/src/commands/stale.ts @@ -75,6 +75,7 @@ export async function runStaleCommand( for (const skill of stale) { console.log(` ⚠ ${skill.name}: ${skill.reasons.join(', ')}`) } + const printed = new Set() for (const signal of signals) { const subject = signal.packageName ?? @@ -83,7 +84,10 @@ export async function runStaleCommand( signal.artifactPath ?? signal.subject ?? report.library - console.log(` ⚠ ${subject}: ${signal.reasons.join(', ')}`) + const line = ` ⚠ ${subject}: ${signal.reasons.join(', ')}` + if (printed.has(line)) continue + printed.add(line) + console.log(line) } } diff --git a/packages/intent/src/staleness/check.ts b/packages/intent/src/staleness/check.ts index 628d391..81bd247 100644 --- a/packages/intent/src/staleness/check.ts +++ b/packages/intent/src/staleness/check.ts @@ -267,12 +267,14 @@ function findMatchingSkill( function buildArtifactSignals({ artifactRoot, artifacts, + currentVersion, library, packageDir, skillMetas, }: { artifactRoot: string artifacts: IntentArtifactSet | null + currentVersion: string | null library: string packageDir: string skillMetas: Array @@ -355,7 +357,8 @@ function buildArtifactSignals({ if ( artifactVersion && matchingSkill.libraryVersion && - artifactVersion !== matchingSkill.libraryVersion + artifactVersion !== matchingSkill.libraryVersion && + matchingSkill.libraryVersion !== currentVersion ) { signals.push({ type: 'artifact-library-version-drift', @@ -550,6 +553,7 @@ export async function checkStaleness( signals: buildArtifactSignals({ artifactRoot, artifacts, + currentVersion, library, packageDir, skillMetas, diff --git a/packages/intent/tests/stale-command.test.ts b/packages/intent/tests/stale-command.test.ts index 20db52a..deaa7c7 100644 --- a/packages/intent/tests/stale-command.test.ts +++ b/packages/intent/tests/stale-command.test.ts @@ -60,6 +60,38 @@ describe('runStaleCommand', () => { expect(output).not.toContain('All skills up-to-date') }) + it('prints a review signal raised by two artifact files once in non-json output', async () => { + const reason = + 'artifact library.version (1.2.0) differs from SKILL.md library_version (1.0.0)' + await runStaleCommand(undefined, {}, () => + Promise.resolve({ + reports: [ + { + library: '@tanstack/router', + currentVersion: '1.1.0', + skillVersion: '1.0.0', + versionDrift: 'minor', + skills: [], + signals: ['domain_map.yaml', 'skill_tree.yaml'].map((file) => ({ + type: 'artifact-library-version-drift', + library: '@tanstack/router', + subject: 'routing', + reasons: [reason], + needsReview: true, + artifactPath: `_artifacts/${file}`, + skill: 'routing', + })), + }, + ], + }), + ) + + const lines = logSpy.mock.calls.map((call) => String(call[0])) + expect(lines.filter((line) => line.includes(reason))).toEqual([ + ` ⚠ routing: ${reason}`, + ]) + }) + it('prints workflow update advisories in non-json output', async () => { await runStaleCommand(undefined, {}, () => Promise.resolve({ diff --git a/packages/intent/tests/staleness.test.ts b/packages/intent/tests/staleness.test.ts index b85954d..58c9332 100644 --- a/packages/intent/tests/staleness.test.ts +++ b/packages/intent/tests/staleness.test.ts @@ -542,6 +542,100 @@ skills: ]) }) + it('does not flag artifact library version drift when the skill matches its independently versioned package', async () => { + const tablePackageDir = join(tmpDir, 'packages', 'table') + const utilsPackageDir = join(tmpDir, 'packages', 'utils') + mkdirSync(tablePackageDir, { recursive: true }) + mkdirSync(utilsPackageDir, { recursive: true }) + writeFileSync( + join(tablePackageDir, 'package.json'), + JSON.stringify({ name: '@example/table', version: '9.2.5' }), + ) + writeFileSync( + join(utilsPackageDir, 'package.json'), + JSON.stringify({ name: '@example/utils', version: '9.1.2' }), + ) + writeSkill(tablePackageDir, 'core', { + name: 'core', + description: 'Core', + library_version: '9.2.5', + }) + writeSkill(utilsPackageDir, 'ranking', { + name: 'ranking', + description: 'Ranking', + library_version: '9.1.2', + }) + const artifact = ` +library: + name: '@example/table' + version: '9.2.5' +skills: + - name: 'Core' + slug: 'core' + path: 'packages/table/skills/core/SKILL.md' + package: 'packages/table' + - name: 'Ranking' + slug: 'ranking' + path: 'packages/utils/skills/ranking/SKILL.md' + package: 'packages/utils' +` + writeArtifact(tmpDir, 'skill_tree.yaml', artifact) + writeArtifact(tmpDir, 'domain_map.yaml', artifact) + mockFetchNotOk() + + const tableReport = await checkStaleness( + tablePackageDir, + '@example/table', + tmpDir, + ) + const utilsReport = await checkStaleness( + utilsPackageDir, + '@example/utils', + tmpDir, + ) + + expect(tableReport.signals).toEqual([]) + expect(utilsReport.currentVersion).toBe('9.1.2') + expect(utilsReport.signals).toEqual([]) + }) + + it('flags artifact library version drift when the skill differs from its package and the artifact', async () => { + writeFileSync( + join(tmpDir, 'package.json'), + JSON.stringify({ name: '@example/lib', version: '1.1.0' }), + ) + writeSkill(tmpDir, 'core', { + name: 'core', + description: 'Core', + library_version: '1.0.0', + }) + writeArtifact( + tmpDir, + 'skill_tree.yaml', + ` +library: + name: '@example/lib' + version: '1.2.0' +skills: + - name: 'Core' + slug: 'core' + path: 'skills/core/SKILL.md' + package: '@example/lib' +`, + ) + mockFetchNotOk() + + const report = await checkStaleness(tmpDir, '@example/lib') + + expect(report.signals).toEqual([ + expect.objectContaining({ + type: 'artifact-library-version-drift', + skill: 'core', + needsReview: true, + }), + ]) + }) + it('flags artifact parse warnings as review signals', async () => { writeArtifact(tmpDir, 'skill_tree.yaml', 'skills:\n - name: [broken\n') mockFetchNotOk()