2026-07-13 16:34:09 +08:00
/ * *
2026-08-14 13:06:21 +08:00
* Verify that the executable deploy manifest supplies every plugin referenced
* by a shipped agent preset and every required workspace peer in its dependency
* graph . With auto peer installation disabled , either omission can otherwise
* fail only when Cordis loads the packaged plugin .
2026-07-13 16:34:09 +08:00
* /
2026-07-26 23:14:28 +08:00
import { globSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
2026-08-14 13:06:21 +08:00
import { basename , dirname , resolve } from 'node:path'
2026-07-13 16:34:09 +08:00
import { parseArgs } from 'node:util'
2026-08-18 14:38:42 +08:00
import { isCordisGroupEntry , loadCordisYaml } from './cordis-yaml.ts'
2026-07-13 16:34:09 +08:00
interface PackageManifest {
name? : string
dependencies? : Record < string , string >
optionalDependencies? : Record < string , string >
peerDependencies? : Record < string , string >
peerDependenciesMeta? : Record < string , { optional ? : boolean } >
}
interface WorkspacePackage {
path : string
manifest : PackageManifest
}
2026-08-14 13:06:21 +08:00
interface RuntimePlatform {
tag : string
executable : string
}
type RuntimePlatformManifest = Record < string , RuntimePlatform >
2026-08-18 14:38:42 +08:00
const AGENT_PRESET_GLOB = 'apps/cli/config/agent-presets/*/agent.cordis.yml'
2026-08-17 20:43:08 +08:00
export interface RuntimeClosureResult {
failures : string [ ]
presetCount : number
workspacePackageCount : number
}
/ * *
* Check that the runtime manifest contains every shipped - preset plugin and workspace peer .
* @param root repository root containing the runtime manifest and shipped presets .
* @param manifestPath runtime manifest path relative to { @link root } .
* @returns the discovered preset count , reachable workspace package count , and violations .
* /
export async function verifyRuntimeClosure (
root : string ,
manifestPath = 'python/sdk-runtime/package.json' ,
) : Promise < RuntimeClosureResult > {
const runtimeManifest = await loadManifest ( resolve ( root , manifestPath ) )
const runtimeName = runtimeManifest . name ? ? manifestPath
const workspace = await loadWorkspacePackages ( root )
const runtimeDependencies = runtimeManifest . dependencies ? ? { }
const platforms = await loadJson < RuntimePlatformManifest > ( resolve ( root , 'python/sdk-runtime/platforms.json' ) )
2026-08-18 14:38:42 +08:00
const presetPaths = globSync ( AGENT_PRESET_GLOB , { cwd : root } ) . sort ( )
const targets = Object . keys ( platforms ) . sort ( )
2026-08-17 20:43:08 +08:00
const parents = new Map < string , string | undefined > ( )
const queue : string [ ] = [ ]
for ( const dependency of Object . keys ( runtimeDependencies ) . sort ( ) ) {
if ( ! workspace . has ( dependency ) ) continue
parents . set ( dependency , undefined )
queue . push ( dependency )
2026-07-13 16:34:09 +08:00
}
2026-08-17 20:43:08 +08:00
2026-08-18 14:38:42 +08:00
const failures : string [ ] = [ ]
if ( presetPaths . length === 0 ) failures . push ( ` no agent presets matched ${ AGENT_PRESET_GLOB } ` )
if ( targets . length === 0 ) failures . push ( 'python/sdk-runtime/platforms.json defines no runtime targets' )
failures . push ( . . . await missingPresetPlugins ( root , runtimeDependencies , presetPaths , targets ) )
2026-08-17 20:43:08 +08:00
for ( let index = 0 ; index < queue . length ; index += 1 ) {
const packageName = queue [ index ]
if ( packageName === undefined ) continue
const current = workspace . get ( packageName )
if ( current === undefined ) continue
const peers = current . manifest . peerDependencies ? ? { }
const peerMeta = current . manifest . peerDependenciesMeta ? ? { }
for ( const peer of Object . keys ( peers ) . sort ( ) ) {
if ( ! workspace . has ( peer ) || peerMeta [ peer ] ? . optional === true ) continue
if ( runtimeDependencies [ peer ] ? . startsWith ( 'workspace:' ) === true ) continue
failures . push ( ` ${ formatChain ( runtimeName , packageName , parents ) } -> ${ peer } ` )
}
const dependencies = {
. . . current . manifest . dependencies ,
. . . current . manifest . optionalDependencies ,
}
for ( const dependency of Object . keys ( dependencies ) . sort ( ) ) {
if ( ! workspace . has ( dependency ) || parents . has ( dependency ) ) continue
parents . set ( dependency , packageName )
queue . push ( dependency )
}
2026-07-13 16:34:09 +08:00
}
2026-08-17 20:43:08 +08:00
return {
failures ,
2026-08-18 14:38:42 +08:00
presetCount : presetPaths.length ,
2026-08-17 20:43:08 +08:00
workspacePackageCount : queue.length ,
2026-07-13 16:34:09 +08:00
}
}
2026-08-17 20:43:08 +08:00
if ( import . meta . main ) {
const root = resolve ( import . meta . dirname , '..' )
const { values } = parseArgs ( {
args : process.argv.slice ( 2 ) ,
options : { manifest : { type : 'string' } } ,
} )
const result = await verifyRuntimeClosure ( root , values . manifest )
if ( result . failures . length > 0 ) {
console . error ( 'verify-runtime-closure: preset plugins or required workspace peers are missing from python/sdk-runtime dependencies:' )
for ( const failure of result . failures ) console . error ( ` ${ failure } ` )
process . exitCode = 1
} else {
console . log (
` verify-runtime-closure: ${ result . presetCount } agent presets and ${ result . workspacePackageCount } workspace packages form a closed runtime dependency graph. ` ,
)
}
2026-07-13 16:34:09 +08:00
}
2026-08-14 13:06:21 +08:00
async function missingPresetPlugins (
2026-08-17 20:43:08 +08:00
root : string ,
2026-08-14 13:06:21 +08:00
runtimeDependencies : Readonly < Record < string , string > > ,
2026-08-18 14:38:42 +08:00
presetPaths : readonly string [ ] ,
targets : readonly string [ ] ,
2026-08-14 13:06:21 +08:00
) : Promise < string [ ] > {
const missing = new Map < string , Set < string > > ( )
const failures : string [ ] = [ ]
for ( const presetPath of presetPaths ) {
2026-08-14 13:59:49 +08:00
const document = loadCordisYaml ( await readFile ( resolve ( root , presetPath ) , 'utf8' ) )
2026-08-14 13:06:21 +08:00
if ( ! Array . isArray ( document ) ) {
failures . push ( ` ${ presetPath } : preset root must be a Loader entry array ` )
continue
}
2026-08-18 14:38:42 +08:00
for ( const target of targets ) {
2026-08-14 13:06:21 +08:00
const processPlatform = processPlatformForTarget ( target )
for ( const plugin of activeBarePluginPackages ( document , processPlatform ) ) {
2026-08-18 17:36:59 +08:00
const version = runtimeDependencies [ plugin ]
if ( version ? . startsWith ( 'workspace:' ) === true ) continue
2026-08-14 13:06:21 +08:00
const preset = basename ( dirname ( presetPath ) )
2026-08-18 17:36:59 +08:00
const declaration = version === undefined
? ''
: ` [runtime dependency is ${ JSON . stringify ( version ) } ; expected workspace:] `
const key = ` ${ preset } preset -> ${ plugin } ${ declaration } `
2026-08-14 13:06:21 +08:00
const targets = missing . get ( key ) ? ? new Set < string > ( )
targets . add ( target )
missing . set ( key , targets )
}
}
}
failures . push ( . . . [ . . . missing . entries ( ) ] . map ( ( [ chain , targets ] ) = >
` ${ chain } ( ${ [ . . . targets ] . sort ( ) . join ( ', ' ) } ) ` ) )
return failures
}
function activeBarePluginPackages ( entries : unknown [ ] , processPlatform : string ) : Set < string > {
const packages = new Set < string > ( )
const visit = ( value : unknown , parentDisabled : boolean ) : void = > {
if ( ! isRecord ( value ) ) return
const disabled = parentDisabled || disabledOnPlatform ( value . disabled , processPlatform )
if ( disabled ) return
if ( typeof value . name === 'string' ) {
const packageName = barePackageName ( value . name )
if ( packageName !== undefined ) packages . add ( packageName )
}
2026-08-18 14:38:42 +08:00
if ( isCordisGroupEntry ( value ) ) {
2026-08-14 13:06:21 +08:00
for ( const child of value . config ) visit ( child , disabled )
}
}
for ( const entry of entries ) visit ( entry , false )
return packages
}
function disabledOnPlatform ( value : unknown , processPlatform : string ) : boolean {
if ( typeof value === 'boolean' ) return value
if ( ! isRecord ( value ) || typeof value . __jsExpr !== 'string' ) return false
const match = /^process\.platform\s*(===|!==)\s*(['"])(win32|linux|darwin)\2$/ . exec ( value . __jsExpr . trim ( ) )
if ( match === null ) return false
const [ , operator , , expected ] = match
return operator === '===' ? processPlatform === expected : processPlatform !== expected
}
function processPlatformForTarget ( target : string ) : string {
if ( target . startsWith ( 'linux-' ) ) return 'linux'
if ( target . startsWith ( 'macos-' ) ) return 'darwin'
throw new Error ( ` verify-runtime-closure: unsupported runtime target ${ JSON . stringify ( target ) } ` )
}
function barePackageName ( specifier : string ) : string | undefined {
if ( specifier . startsWith ( '.' ) || specifier . startsWith ( '/' ) || specifier . includes ( ':' ) ) return undefined
const parts = specifier . split ( '/' )
if ( specifier . startsWith ( '@' ) ) {
return parts . length >= 2 ? ` ${ parts [ 0 ] } / ${ parts [ 1 ] } ` : undefined
}
return parts [ 0 ] || undefined
}
function isRecord ( value : unknown ) : value is Record < string , unknown > {
return typeof value === 'object' && value !== null && ! Array . isArray ( value )
}
2026-07-13 16:34:09 +08:00
2026-08-17 20:43:08 +08:00
async function loadWorkspacePackages ( root : string ) : Promise < Map < string , WorkspacePackage > > {
2026-07-26 23:14:28 +08:00
const paths = globSync ( [ 'packages/*/*/package.json' , 'vendor/*/package.json' ] , { cwd : root } )
. sort ( )
. map ( relative = > resolve ( root , relative ) )
2026-07-13 16:34:09 +08:00
const result = new Map < string , WorkspacePackage > ( )
for ( const path of paths ) {
const manifest = await loadManifest ( path )
if ( manifest . name !== undefined ) result . set ( manifest . name , { path , manifest } )
}
return result
}
async function loadManifest ( path : string ) : Promise < PackageManifest > {
2026-08-14 13:06:21 +08:00
return loadJson < PackageManifest > ( path )
}
async function loadJson < T > ( path : string ) : Promise < T > {
return JSON . parse ( await readFile ( path , 'utf8' ) ) as T
2026-07-13 16:34:09 +08:00
}
function formatChain (
runtimeName : string ,
packageName : string ,
parents : ReadonlyMap < string , string | undefined > ,
) : string {
const chain = [ packageName ]
let parent = parents . get ( packageName )
while ( parent !== undefined ) {
chain . unshift ( parent )
parent = parents . get ( parent )
}
return [ runtimeName , . . . chain ] . join ( ' -> ' )
}