11import { existsSync , readFileSync } from 'node:fs'
2- import { isAbsolute , join , relative , resolve , sep } from 'node:path'
2+ import { isAbsolute , join , relative , resolve } from 'node:path'
3+ import semver from 'semver'
34import { readIntentArtifacts } from './artifact-coverage.js'
4- import { findSkillFiles , parseFrontmatter } from './utils.js'
5+ import { findSkillFiles , parseFrontmatter , toPosixPath } from './utils.js'
56import type {
67 IntentArtifactSet ,
78 IntentArtifactSkill ,
@@ -26,19 +27,48 @@ function classifyVersionDrift(
2627 oldVer : string ,
2728 newVer : string ,
2829) : 'major' | 'minor' | 'patch' | null {
29- if ( oldVer === newVer ) return null
30- const oldParts = oldVer
31- . replace ( / [ ^ 0 - 9 . ] / g, '' )
32- . split ( '.' )
33- . map ( Number )
34- const newParts = newVer
35- . replace ( / [ ^ 0 - 9 . ] / g, '' )
36- . split ( '.' )
37- . map ( Number )
38- if ( ( newParts [ 0 ] ?? 0 ) > ( oldParts [ 0 ] ?? 0 ) ) return 'major'
39- if ( ( newParts [ 1 ] ?? 0 ) > ( oldParts [ 1 ] ?? 0 ) ) return 'minor'
40- if ( ( newParts [ 2 ] ?? 0 ) > ( oldParts [ 2 ] ?? 0 ) ) return 'patch'
41- return null
30+ const oldVersion = normalizeVersion ( oldVer )
31+ const newVersion = normalizeVersion ( newVer )
32+
33+ if ( ! oldVersion || ! newVersion ) return null
34+ if ( semver . eq ( oldVersion , newVersion ) ) return null
35+ if ( ! semver . gt ( newVersion , oldVersion ) ) return null
36+
37+ const oldParsed = semver . parse ( oldVersion )
38+ const newParsed = semver . parse ( newVersion )
39+ if (
40+ oldParsed &&
41+ newParsed &&
42+ oldParsed . major === newParsed . major &&
43+ oldParsed . minor === newParsed . minor &&
44+ oldParsed . patch === newParsed . patch &&
45+ oldParsed . prerelease . length > 0
46+ ) {
47+ return 'patch'
48+ }
49+
50+ const drift = semver . diff ( oldVersion , newVersion )
51+ switch ( drift ) {
52+ case 'major' :
53+ case 'premajor' :
54+ return 'major'
55+ case 'minor' :
56+ case 'preminor' :
57+ return 'minor'
58+ case 'patch' :
59+ case 'prepatch' :
60+ case 'prerelease' :
61+ return 'patch'
62+ default :
63+ return null
64+ }
65+ }
66+
67+ function normalizeVersion ( version : string ) : string | null {
68+ const validVersion = semver . valid ( version )
69+ if ( validVersion ) return validVersion
70+
71+ return semver . coerce ( version ) ?. version ?? null
4272}
4373
4474// ---------------------------------------------------------------------------
@@ -156,7 +186,14 @@ function readPackageJson(packageDir: string): Record<string, unknown> | null {
156186// ---------------------------------------------------------------------------
157187
158188function normalizeFilePath ( path : string ) : string {
159- return resolve ( path ) . split ( sep ) . join ( '/' )
189+ return toPosixPath ( resolve ( path ) )
190+ }
191+
192+ function getRelativePackageDir (
193+ artifactRoot : string ,
194+ packageDir : string ,
195+ ) : string {
196+ return toPosixPath ( relative ( artifactRoot , packageDir ) )
160197}
161198
162199function normalizeList ( values : Array < string > | undefined ) : Array < string > {
@@ -181,7 +218,7 @@ function artifactPackageMatches(
181218 packageName : string ,
182219 artifactRoot : string ,
183220) : boolean {
184- const relPackageDir = relative ( artifactRoot , packageDir ) . split ( sep ) . join ( '/' )
221+ const relPackageDir = getRelativePackageDir ( artifactRoot , packageDir )
185222 if ( ! relPackageDir ) return true
186223
187224 if ( artifact . packages . includes ( packageName ) ) return true
@@ -352,7 +389,7 @@ function artifactCoversPackage(
352389 packageName : string ,
353390 artifactRoot : string ,
354391) : boolean {
355- const relPackageDir = relative ( artifactRoot , packageDir ) . split ( sep ) . join ( '/' )
392+ const relPackageDir = getRelativePackageDir ( artifactRoot , packageDir )
356393 return (
357394 artifact . packages . includes ( packageName ) ||
358395 artifact . packages . includes ( relPackageDir ) ||
@@ -368,7 +405,7 @@ function artifactIgnoresPackage(
368405 packageName : string ,
369406 artifactRoot : string ,
370407) : boolean {
371- const relPackageDir = relative ( artifactRoot , packageDir ) . split ( sep ) . join ( '/' )
408+ const relPackageDir = getRelativePackageDir ( artifactRoot , packageDir )
372409 return artifacts . ignoredPackages . some (
373410 ( ignored ) =>
374411 ignored . packageName === packageName ||
@@ -416,7 +453,7 @@ export function buildWorkspaceCoverageSignals({
416453 ] ,
417454 needsReview : true ,
418455 packageName,
419- packageRoot : relative ( artifactRoot , packageDir ) . split ( sep ) . join ( '/' ) ,
456+ packageRoot : getRelativePackageDir ( artifactRoot , packageDir ) ,
420457 } )
421458 }
422459
@@ -433,16 +470,16 @@ export async function checkStaleness(
433470 artifactRoot = packageDir ,
434471) : Promise < StalenessReport > {
435472 const skillsDir = join ( packageDir , 'skills' )
436- const library = packageName ?? 'unknown'
473+ const library = packageName ?? readPackageName ( packageDir )
437474
438475 // Find all skills
439476 const skillFiles = findSkillFiles ( skillsDir )
440477 const skillMetas : Array < SkillMeta > = skillFiles . map ( ( filePath ) => {
441478 const fm = parseFrontmatter ( filePath )
442- const relName = relative ( skillsDir , filePath )
443- . replace ( / [ / \\ ] S K I L L \. m d $ / , '' )
444- . split ( sep )
445- . join ( '/' )
479+ const relName = toPosixPath ( relative ( skillsDir , filePath ) ) . replace (
480+ / [ / \\ ] S K I L L \. m d $ / ,
481+ '' ,
482+ )
446483 return {
447484 name : typeof fm ?. name === 'string' ? fm . name : relName ,
448485 relName,
@@ -482,7 +519,7 @@ export async function checkStaleness(
482519 if (
483520 currentVersion &&
484521 skill . libraryVersion &&
485- skill . libraryVersion !== currentVersion
522+ classifyVersionDrift ( skill . libraryVersion , currentVersion ) !== null
486523 ) {
487524 reasons . push (
488525 `version drift (${ skill . libraryVersion } → ${ currentVersion } )` ,
0 commit comments