2026-07-14 21:25:58 +08:00
/ * *
2026-07-28 23:05:20 +08:00
* Validate Cordis Loader entry metadata and package resolution .
2026-07-14 21:25:58 +08:00
*
2026-08-11 18:52:52 +08:00
* The Loader interpolates a plugin entry ' s ` config ` ( after declared injections
* activate , against that plugin context ) and the entry ` disabled ` field ( at
* every mount decision , against the loader context ) . Every other entry
* metadata field stays static , so an expression there remains truthy data and
2026-08-24 10:00:15 +08:00
* silently changes composition . Shipped and test - only dsh overlays resolve
2026-08-24 10:37:43 +08:00
* named plugins from the CLI application ' s owning manifest ; package - owned
* Loader fixtures resolve from their package manifest .
2026-07-14 21:25:58 +08:00
* /
import { globSync , readFileSync } from 'node:fs'
2026-07-17 23:38:05 +08:00
import { dirname , relative , resolve } from 'node:path'
2026-08-14 14:12:23 +08:00
import { Script } from 'node:vm'
2026-07-17 23:38:05 +08:00
import ts from 'typescript'
2026-07-23 00:39:55 +08:00
import { cordisConfigFiles } from './cordis-config-files.ts'
2026-08-18 14:38:42 +08:00
import { isCordisGroupEntry , isJsExpr , loadCordisYaml } from './cordis-yaml.ts'
2026-07-14 21:25:58 +08:00
2026-08-12 19:28:31 +08:00
export interface PackageManifest {
2026-07-17 23:38:05 +08:00
name? : string
dependencies? : Record < string , string >
2026-08-24 10:00:15 +08:00
devDependencies? : Record < string , string >
2026-08-12 19:28:31 +08:00
optionalDependencies? : Record < string , string >
dsh ? : { bundle ? : { patch? : string } }
2026-07-17 23:38:05 +08:00
}
2026-08-12 19:28:31 +08:00
export interface PluginReference {
2026-07-17 23:38:05 +08:00
file : string
name : string
}
2026-07-14 21:25:58 +08:00
const root = resolve ( import . meta . dirname , '..' )
2026-08-24 10:00:15 +08:00
// These overlays are consumed by the built dsh app, so their bare specifiers
// resolve from apps/cli.
2026-07-31 01:57:19 -07:00
const appOverlayFiles = new Set ( [
2026-08-24 10:00:15 +08:00
. . . globSync ( 'apps/cli/config/examples/**/*.yml' , { cwd : root } ) ,
2026-07-31 01:57:19 -07:00
] )
2026-08-11 11:33:53 +08:00
const metadataFields = [ 'id' , 'name' , 'group' , 'inject' , 'intercept' , 'isolate' ] as const
2026-07-29 21:09:02 +08:00
/** The adaptive directory-picker chooser package (mounts a backend row at boot). */
const CHOOSER_PACKAGE = '@deepseek-ai/dsh-host-directory-picker-auto'
/ * *
2026-08-11 19:10:50 +08:00
* The packages the chooser mounts by runtime string ( mirror of its exported
* ` BACKEND_PACKAGES ` and ` SURFACE_PACKAGES ` ) , invisible to yml - row scanning : a
* composition mounting the chooser must resolve every one , or keyless Linux CI
* ( which only ever resolves ` browse ` ) hides a dropped ` -native ` dependency
* until a macOS boot .
2026-07-29 21:09:02 +08:00
* /
const CHOOSER_BACKEND_PACKAGES = [
'@deepseek-ai/dsh-host-directory-picker-native' ,
'@deepseek-ai/dsh-host-directory-picker-browse' ,
2026-08-13 00:36:22 +08:00
'@deepseek-ai/dsh-client-ui-directory-picker-browse' ,
2026-08-11 19:10:50 +08:00
'@deepseek-ai/dsh-client-ui-directory-picker-native' ,
2026-07-29 21:09:02 +08:00
]
2026-07-14 21:25:58 +08:00
const errors : string [ ] = [ ]
2026-07-28 23:05:20 +08:00
const pluginReferences : PluginReference [ ] = [ ]
2026-07-14 21:25:58 +08:00
2026-08-11 11:33:53 +08:00
if ( import . meta . main ) {
const files = cordisConfigFiles ( root )
for ( const file of files ) {
2026-08-14 13:59:49 +08:00
const document = loadCordisYaml ( readFileSync ( resolve ( root , file ) , 'utf8' ) )
2026-08-11 11:33:53 +08:00
if ( ! isUnknownArray ( document ) ) {
errors . push ( ` ${ file } : root must be a Loader entry array ` )
continue
}
for ( let index = 0 ; index < document . length ; index ++ ) {
validateEntry ( document [ index ] , file , ` [ ${ index } ] ` )
}
2026-07-14 21:25:58 +08:00
}
2026-08-11 11:33:53 +08:00
errors . push ( . . . validateAppResolution ( ) )
2026-08-24 10:37:43 +08:00
errors . push ( . . . validatePackageTestResolution ( ) )
errors . push ( . . . packageTestFixtureDependencyErrors ( ) )
2026-08-11 11:33:53 +08:00
errors . push ( . . . validateSourcePlaneResolution ( ) )
errors . push ( . . . validatePresetPlaneSeparation ( ) )
2026-08-11 21:04:49 +08:00
errors . push ( . . . validateClientHalvesDeclared ( ) )
2026-08-11 11:33:53 +08:00
if ( errors . length > 0 ) {
console . error ( 'verify-cordis-config: invalid Loader metadata or plugin package resolution:' )
for ( const error of errors ) console . error ( ` - ${ error } ` )
process . exitCode = 1
} else {
console . log ( ` verify-cordis-config: ${ files . length } config files passed. ` )
}
2026-07-14 21:25:58 +08:00
}
2026-08-10 23:56:12 +08:00
/ * *
* A browser plugin must declare the browser half it ships .
*
* The browser roster is discovered by scanning composed packages for a
* ` dsh.client ` block , and the node half of a surface plugin is an empty
* ` apply ` . A ` packages/client ` package that exports ` ./client ` without that
* block therefore composes , activates , and contributes nothing — its bundle is
* never served and no error is raised anywhere . The mismatch is invisible in
* the composition file , so it is checked against the manifests instead . Only
* this group is checked : a Host package ' s ` ./client ` export is the typed wire
* face its browser consumers import , not a plugin the roster serves .
* @returns one violation per client package whose ` ./client ` export and
* ` dsh.client ` declaration disagree .
* /
function validateClientHalvesDeclared ( ) : string [ ] {
return globSync ( 'packages/client/*/package.json' , { cwd : root } ) . flatMap ( ( manifestPath ) = > {
const manifest = readManifest ( manifestPath ) as PackageManifest & {
exports? : Record < string , unknown >
dsh ? : { client? : unknown }
}
const shipsClient = manifest . exports !== undefined && Object . hasOwn ( manifest . exports , './client' )
const declaresClient = manifest . dsh ? . client !== undefined
if ( shipsClient === declaresClient ) return [ ]
return [ shipsClient
? ` ${ manifestPath } : exports "./client" but declares no dsh.client, so its browser half is never served `
: ` ${ manifestPath } : declares dsh.client but exports no "./client" entry to serve ` ]
} )
}
fix(agent-presets): keep the code preset on one plane, and check that
`code` still carried `bash-env` behind its own `isolate` realm and its own
`tool-subagent-report` row — the two the other three presets had already given
back to the host. It was added a layer above the fix, so the rebase carried it
forward untouched, and the shipped deployment ran a preset whose sessions get
no `DSH_WEB_URL` in their shell and hand every subagent a second `report`
registration on the host registry.
Nothing caught it. A tool-catalog assertion cannot: neither row contributes a
tool. The web lane cannot: no scenario composes `code` beside another preset,
which is when the second `report` throws. The presets are near-copies of one
another, so "fixed in three of four" is the shape this failure takes, and it
will take it again.
So the invariant is checked rather than described. `verify-cordis-config` now
rejects any shipped preset row that is also active on the host plane, which is
the property both defects violated: a row active on both planes is mounted once
per process and once per session, and what that costs depends on the row — a
provider behind an `isolate` realm shadows the host's for its own consumers, so
a host contributor reaches nobody; a row registering into a host singleton
registers once per live session, so the second collides.
2026-08-06 19:58:26 +08:00
/ * *
* No shipped agent preset may repeat a row the host composition still runs .
*
* A preset contributes what ONE session adds to the host ' s registries . A row
* active on both planes is therefore mounted twice — once per process and once
* per session — and what that costs depends on what the row does : a provider
* behind an ` isolate ` realm shadows the host ' s for its own consumers , so a host
* contributor to that service reaches nobody ; a row that registers into a host
* singleton registers once per live session , so the second one collides .
*
2026-08-30 14:14:46 +08:00
* Both failure modes have occurred . A preset - local provider once shadowed the
* host route that its consumer needed , and a host - registry contribution once
* registered again for every live session until the second registration threw .
* Neither changes a tool catalog , so no catalog assertion can see them — and the
* shipped presets are near - copies of each other , so a fix applied to three of
* four is the normal failure .
fix(agent-presets): keep the code preset on one plane, and check that
`code` still carried `bash-env` behind its own `isolate` realm and its own
`tool-subagent-report` row — the two the other three presets had already given
back to the host. It was added a layer above the fix, so the rebase carried it
forward untouched, and the shipped deployment ran a preset whose sessions get
no `DSH_WEB_URL` in their shell and hand every subagent a second `report`
registration on the host registry.
Nothing caught it. A tool-catalog assertion cannot: neither row contributes a
tool. The web lane cannot: no scenario composes `code` beside another preset,
which is when the second `report` throws. The presets are near-copies of one
another, so "fixed in three of four" is the shape this failure takes, and it
will take it again.
So the invariant is checked rather than described. `verify-cordis-config` now
rejects any shipped preset row that is also active on the host plane, which is
the property both defects violated: a row active on both planes is mounted once
per process and once per session, and what that costs depends on the row — a
provider behind an `isolate` realm shadows the host's for its own consumers, so
a host contributor reaches nobody; a row registering into a host singleton
registers once per live session, so the second collides.
2026-08-06 19:58:26 +08:00
* @returns one diagnostic per preset row that is also active on the host plane .
* /
function validatePresetPlaneSeparation ( ) : string [ ] {
const problems : string [ ] = [ ]
2026-08-06 21:23:30 +08:00
// The shipped Web surface is two bundle patch layers over an empty root.
const hostFile = 'packages/bundle/base/cordis.patch.yml'
const overlayFile = 'packages/bundle/web-app/cordis.patch.yml'
fix(agent-presets): keep the code preset on one plane, and check that
`code` still carried `bash-env` behind its own `isolate` realm and its own
`tool-subagent-report` row — the two the other three presets had already given
back to the host. It was added a layer above the fix, so the rebase carried it
forward untouched, and the shipped deployment ran a preset whose sessions get
no `DSH_WEB_URL` in their shell and hand every subagent a second `report`
registration on the host registry.
Nothing caught it. A tool-catalog assertion cannot: neither row contributes a
tool. The web lane cannot: no scenario composes `code` beside another preset,
which is when the second `report` throws. The presets are near-copies of one
another, so "fixed in three of four" is the shape this failure takes, and it
will take it again.
So the invariant is checked rather than described. `verify-cordis-config` now
rejects any shipped preset row that is also active on the host plane, which is
the property both defects violated: a row active on both planes is mounted once
per process and once per session, and what that costs depends on the row — a
provider behind an `isolate` realm shadows the host's for its own consumers, so
a host contributor reaches nobody; a row registering into a host singleton
registers once per live session, so the second collides.
2026-08-06 19:58:26 +08:00
const hostRows = rowIds ( hostFile )
const overlay = loadEntries ( overlayFile )
const disabled = new Set < string > ( )
for ( const entry of overlay ) {
if ( ! isRecord ( entry ) ) continue
if ( entry . disabled === true && typeof entry . id === 'string' ) disabled . add ( entry . id )
}
// The overlay's own inserts are host-plane too; its disables take them back out.
const active = new Set ( [ . . . hostRows , . . . rowIds ( overlayFile ) ] . filter ( id = > ! disabled . has ( id ) ) )
refactor(preset): bundle the shipped presets inside dsh-agent-presets
Review asked why the launcher special-cases one plugin's row. It no
longer does: the four shipped compositions move into the package
(presets/, in files), dsh-agent-presets resolves its own shipped root
and prepends it before configured roots (includeShippedRoot, default
true, opt-out for bare-machinery embedders), and the per-composition
derived patch, its spec, and the dump layer are deleted — profile-boot
and dump-config return to plain layer stacking. The always-load
guarantee now rides the schema default instead of patch ordering, so a
whole-config replacement keeps the shipped set and the squash, reload
freeze, and dump divergence stop being possible.
Gate globs, the web scaffold, and both preset browser lanes drop their
hand-fed shipped roots; the roster e2e keeps asserting configured roots
beside the shipped four against the built lib.
Fixes #2863.
2026-08-21 12:37:57 +08:00
for ( const file of globSync ( 'packages/preset/agent-presets/presets/*/agent.cordis.yml' , { cwd : root } ) ) {
fix(agent-presets): keep the code preset on one plane, and check that
`code` still carried `bash-env` behind its own `isolate` realm and its own
`tool-subagent-report` row — the two the other three presets had already given
back to the host. It was added a layer above the fix, so the rebase carried it
forward untouched, and the shipped deployment ran a preset whose sessions get
no `DSH_WEB_URL` in their shell and hand every subagent a second `report`
registration on the host registry.
Nothing caught it. A tool-catalog assertion cannot: neither row contributes a
tool. The web lane cannot: no scenario composes `code` beside another preset,
which is when the second `report` throws. The presets are near-copies of one
another, so "fixed in three of four" is the shape this failure takes, and it
will take it again.
So the invariant is checked rather than described. `verify-cordis-config` now
rejects any shipped preset row that is also active on the host plane, which is
the property both defects violated: a row active on both planes is mounted once
per process and once per session, and what that costs depends on the row — a
provider behind an `isolate` realm shadows the host's for its own consumers, so
a host contributor reaches nobody; a row registering into a host singleton
registers once per live session, so the second collides.
2026-08-06 19:58:26 +08:00
for ( const id of rowIds ( file ) ) {
if ( ! active . has ( id ) ) continue
problems . push (
` ${ file } : row " ${ id } " is also active in the host composition; `
+ 'a row belongs to exactly one plane' ,
)
}
}
return problems
}
/** Every entry of one config file, or an empty list when it is not an entry array. */
function loadEntries ( file : string ) : unknown [ ] {
2026-08-14 13:59:49 +08:00
const document = loadCordisYaml ( readFileSync ( resolve ( root , file ) , 'utf8' ) )
fix(agent-presets): keep the code preset on one plane, and check that
`code` still carried `bash-env` behind its own `isolate` realm and its own
`tool-subagent-report` row — the two the other three presets had already given
back to the host. It was added a layer above the fix, so the rebase carried it
forward untouched, and the shipped deployment ran a preset whose sessions get
no `DSH_WEB_URL` in their shell and hand every subagent a second `report`
registration on the host registry.
Nothing caught it. A tool-catalog assertion cannot: neither row contributes a
tool. The web lane cannot: no scenario composes `code` beside another preset,
which is when the second `report` throws. The presets are near-copies of one
another, so "fixed in three of four" is the shape this failure takes, and it
will take it again.
So the invariant is checked rather than described. `verify-cordis-config` now
rejects any shipped preset row that is also active on the host plane, which is
the property both defects violated: a row active on both planes is mounted once
per process and once per session, and what that costs depends on the row — a
provider behind an `isolate` realm shadows the host's for its own consumers, so
a host contributor reaches nobody; a row registering into a host singleton
registers once per live session, so the second collides.
2026-08-06 19:58:26 +08:00
return isUnknownArray ( document ) ? document : [ ]
}
/ * *
* Row ids declared anywhere in one config file , including inside group ` config `
* lists — a preset nests most of its rows in ` isolate ` groups .
* @param file - repository - relative config path .
* @returns the declared ids .
* /
function rowIds ( file : string ) : Set < string > {
const ids = new Set < string > ( )
const walk = ( value : unknown ) : void = > {
if ( isUnknownArray ( value ) ) {
for ( const item of value ) walk ( item )
return
}
if ( ! isRecord ( value ) ) return
if ( typeof value . id === 'string' && typeof value . name === 'string' ) ids . add ( value . id )
for ( const child of Object . values ( value ) ) walk ( child )
}
walk ( loadEntries ( file ) )
return ids
}
2026-07-14 21:25:58 +08:00
function validateEntry ( value : unknown , file : string , path : string ) : void {
if ( ! isRecord ( value ) ) {
errors . push ( ` ${ file } ${ path } : entry must be an object ` )
return
}
2026-07-28 23:05:20 +08:00
recordPlugin ( value , file )
2026-07-14 21:25:58 +08:00
validateMetadata ( value , file , path )
2026-08-18 14:38:42 +08:00
if ( isCordisGroupEntry ( value ) ) {
2026-07-14 21:25:58 +08:00
for ( let index = 0 ; index < value . config . length ; index ++ ) {
validateEntry ( value . config [ index ] , file , ` ${ path } .config[ ${ index } ] ` )
}
}
2026-07-29 17:34:50 +08:00
if ( isUnknownArray ( value . insert ) ) {
for ( let index = 0 ; index < value . insert . length ; index ++ ) {
validateEntry ( value . insert [ index ] , file , ` ${ path } .insert[ ${ index } ] ` )
}
}
build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.
Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.
The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.
Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:06 +08:00
if ( value . name !== '@deepseek-ai/cordis-plugin-include' ) return
2026-07-14 21:25:58 +08:00
const config = value . config
if ( ! isRecord ( config ) || ! isUnknownArray ( config . patches ) ) return
for ( let index = 0 ; index < config . patches . length ; index ++ ) {
const patch = config . patches [ index ]
const patchPath = ` ${ path } .config.patches[ ${ index } ] `
if ( ! isRecord ( patch ) ) continue
2026-07-28 23:05:20 +08:00
recordPlugin ( patch , file )
2026-07-14 21:25:58 +08:00
validateMetadata ( patch , file , patchPath )
if ( ! isUnknownArray ( patch . insert ) ) continue
for ( let insertIndex = 0 ; insertIndex < patch . insert . length ; insertIndex ++ ) {
validateEntry ( patch . insert [ insertIndex ] , file , ` ${ patchPath } .insert[ ${ insertIndex } ] ` )
}
}
}
2026-07-28 23:05:20 +08:00
function recordPlugin ( entry : Record < string , unknown > , file : string ) : void {
if ( typeof entry . name === 'string' ) pluginReferences . push ( { file , name : entry.name } )
2026-07-17 23:38:05 +08:00
}
2026-07-28 23:05:20 +08:00
function validateAppResolution ( ) : string [ ] {
2026-08-06 04:40:32 +08:00
const violations : string [ ] = [ ]
2026-08-12 19:28:31 +08:00
const bundleManifests = bundleManifestPaths ( )
2026-08-06 04:40:32 +08:00
// App overlays (and any config left under apps/cli/config) resolve from the
// dsh app's own dependency surface — the profile module fallback mirrors it.
2026-08-24 10:00:15 +08:00
const appManifest = readManifest ( 'apps/cli/package.json' )
2026-08-06 04:40:32 +08:00
const appDependencies = {
2026-08-24 10:00:15 +08:00
. . . appManifest . dependencies ,
2026-08-12 19:28:31 +08:00
// The fallback also links every in-box bundle's own dependencies
// (healProfilesModuleFallback). Optional Profile bundles stay outside the
// app installation until that Profile installs them.
2026-08-06 04:40:32 +08:00
. . . Object . fromEntries ( globSync ( 'packages/bundle/*/package.json' , { cwd : root } )
. flatMap ( file = > Object . entries ( readManifest ( file ) . dependencies ? ? { } ) ) ) ,
}
2026-07-30 14:56:39 +08:00
const shipped = new Set ( globSync ( '*.cordis.yml' , { cwd : resolve ( root , 'apps/cli/config' ) } )
. map ( file = > ` apps/cli/config/ ${ file } ` ) )
2026-08-06 04:40:32 +08:00
const appReferences = pluginReferences . filter ( reference = > shipped . has ( reference . file ) || appOverlayFiles . has ( reference . file ) )
2026-08-24 10:37:43 +08:00
violations . push ( . . . missingPluginDependencies (
appReferences ,
appDependencies ,
'apps/cli/package.json dependencies or a bundle manifest' ,
) )
2026-08-24 10:00:15 +08:00
const appTestReferences = pluginReferences . filter ( reference = > reference . file . startsWith ( 'apps/cli/tests/' ) )
violations . push ( . . . missingPluginDependencies (
appTestReferences ,
{ . . . appManifest . dependencies , . . . appManifest . devDependencies } ,
'apps/cli/package.json dependencies or devDependencies' ,
) )
2026-08-06 04:40:32 +08:00
// Each bundle's patch rows must resolve from that bundle's own dependencies:
// per-layer resolution anchors on the bundle package directory.
2026-08-12 19:28:31 +08:00
for ( const manifestPath of bundleManifests ) {
2026-08-06 04:40:32 +08:00
const bundleDir = manifestPath . replace ( /\/package\.json$/ , '' )
2026-08-06 09:53:49 +08:00
const manifest = readManifest ( manifestPath )
2026-08-12 19:28:31 +08:00
const patch = manifest . dsh ? . bundle ? . patch
if ( typeof patch !== 'string' ) continue
const patchFile = relative ( root , resolve ( root , bundleDir , patch ) ) . replaceAll ( '\\' , '/' )
const references = pluginReferences . filter ( reference = > reference . file === patchFile )
violations . push ( . . . bundlePluginDependencyErrors ( manifestPath , manifest , references ) )
2026-08-06 04:40:32 +08:00
}
return violations
2026-07-28 23:05:20 +08:00
}
2026-08-24 10:37:43 +08:00
/ * *
* Package - owned Loader fixtures resolve named plugins from their package ' s
* dependency surface , not from a repository - level test umbrella .
* @returns one violation per configured package absent from the owner manifest .
* /
function validatePackageTestResolution ( ) : string [ ] {
const referencesByManifest = new Map < string , PluginReference [ ] > ( )
for ( const reference of pluginReferences ) {
const manifestPath = packageTestManifestPath ( reference . file )
if ( manifestPath === undefined ) continue
const references = referencesByManifest . get ( manifestPath ) ? ? [ ]
references . push ( reference )
referencesByManifest . set ( manifestPath , references )
}
return [ . . . referencesByManifest ] . flatMap ( ( [ manifestPath , references ] ) = >
packageTestPluginDependencyErrors ( manifestPath , readManifest ( manifestPath ) , references ) )
}
/ * *
* Validate the named plugins one package - owned Loader fixture resolves .
* Self - references use Node package self - resolution ; every other package must
* be an ordinary production or test dependency of the owner .
* @param manifestPath Repository - relative owner manifest path .
* @param manifest Parsed owner manifest .
* @param references Named plugin references from owner - local test configs .
* @returns Missing dependency diagnostics .
* /
export function packageTestPluginDependencyErrors (
manifestPath : string ,
manifest : PackageManifest ,
references : readonly PluginReference [ ] ,
) : string [ ] {
return missingPluginDependencies (
references . filter ( reference = > packageNameFromSpecifier ( reference . name ) !== manifest . name ) ,
{ . . . manifest . dependencies , . . . manifest . devDependencies } ,
` ${ manifestPath } dependencies or devDependencies ` ,
)
}
/ * *
* Validate imports made by fixture modules adjacent to package - owned Loader
* configs . These files execute as plain Node / tsx children , so a stale root
* ` node_modules ` link must not hide an undeclared dependency .
* @param repoRoot Repository root to scan .
* @returns Missing dependency diagnostics .
* /
export function packageTestFixtureDependencyErrors ( repoRoot : string = root ) : string [ ] {
const fixtureDirectories = new Set ( cordisConfigFiles ( repoRoot )
. filter ( file = > packageTestManifestPath ( file ) !== undefined )
. map ( file = > dirname ( file ) . replaceAll ( '\\' , '/' ) ) )
if ( fixtureDirectories . size === 0 ) {
return [ 'package test fixture dependency scan found no package-owned Loader configs' ]
}
const referencesByManifest = new Map < string , PluginReference [ ] > ( )
let fixtureModuleCount = 0
for ( const fixtureDirectory of fixtureDirectories ) {
const files = globSync ( [
` ${ fixtureDirectory } /**/*.ts ` ,
` ${ fixtureDirectory } /**/*.mjs ` ,
] , { cwd : repoRoot } )
fixtureModuleCount += files . length
for ( const file of files ) {
const manifestPath = packageTestManifestPath ( file )
if ( manifestPath === undefined ) continue
const references = referencesByManifest . get ( manifestPath ) ? ? [ ]
const source = readFileSync ( resolve ( repoRoot , file ) , 'utf8' )
for ( const imported of ts . preProcessFile ( source , true , true ) . importedFiles ) {
references . push ( { file : file.replaceAll ( '\\' , '/' ) , name : imported.fileName } )
}
referencesByManifest . set ( manifestPath , references )
}
}
if ( fixtureModuleCount === 0 ) {
return [ 'package test fixture dependency scan found no fixture modules beside Loader configs' ]
}
return [ . . . referencesByManifest ] . flatMap ( ( [ manifestPath , references ] ) = >
packageTestPluginDependencyErrors (
manifestPath ,
readManifest ( manifestPath , repoRoot ) ,
references ,
) )
}
/** Owner manifest for a package-local test path. */
function packageTestManifestPath ( file : string ) : string | undefined {
const match = /^(packages\/[^/]+\/[^/]+)\/tests(?:\/|$)/ . exec ( file . replaceAll ( '\\' , '/' ) )
return match ? . [ 1 ] === undefined ? undefined : ` ${ match [ 1 ] } /package.json `
}
2026-08-12 19:28:31 +08:00
/ * *
* Discover workspace Bundle packages from their manifest declaration .
* @param repoRoot Repository root to scan .
2026-08-18 21:19:06 +08:00
* @returns Sorted slash - normalized repository - relative package manifest paths .
2026-08-12 19:28:31 +08:00
* /
export function bundleManifestPaths ( repoRoot : string = root ) : string [ ] {
return globSync ( 'packages/*/*/package.json' , { cwd : repoRoot } )
. filter ( path = > typeof readManifest ( path , repoRoot ) . dsh ? . bundle ? . patch === 'string' )
2026-08-18 21:19:06 +08:00
. map ( path = > path . replaceAll ( '\\' , '/' ) )
2026-08-12 19:28:31 +08:00
. sort ( )
}
/ * *
* Validate plugin packages referenced by one Bundle patch .
* @param manifestPath Repository - relative Bundle manifest path .
* @param manifest Parsed Bundle manifest .
* @param references Plugin rows read from the Bundle package directory .
* @returns Missing production dependency diagnostics .
* /
export function bundlePluginDependencyErrors (
manifestPath : string ,
manifest : PackageManifest ,
references : readonly PluginReference [ ] ,
) : string [ ] {
return missingPluginDependencies (
// A Bundle may mount its own package (for example, its provider or runtime row).
references . filter ( reference = > packageNameFromSpecifier ( reference . name ) !== manifest . name ) ,
manifest . dependencies ? ? { } ,
2026-08-24 10:37:43 +08:00
` ${ manifestPath } dependencies ` ,
2026-08-12 19:28:31 +08:00
)
}
2026-07-30 21:34:23 +08:00
/ * *
* Every configured specifier of a local workspace package must resolve through
* the tsconfig ` paths ` facade to a ` .ts ` / ` .tsx ` source file . The ` dsh ` source
* launch ( tsx ) and vitest resolve in the source plane ; without a ` paths ` match
* they fall back to package ` exports ` , which reach built ` lib/ ` — present on a
* built dev tree , absent on a clean one — so a missing mapping boots locally
* yet breaks every clean checkout . Anything but a ` .ts ` / ` .tsx ` hit ( a ` .d.ts `
* or ` .js ` under built ` lib/ ` ) is that artifact - plane fallback , not source .
* /
function validateSourcePlaneResolution ( ) : string [ ] {
const violations : string [ ] = [ ]
const localPackages = localPackageDirectories ( )
const config = ts . readConfigFile ( resolve ( root , 'tsconfig.base.json' ) , path = > ts . sys . readFile ( path ) )
if ( config . error !== undefined ) {
throw new Error ( ts . flattenDiagnosticMessageText ( config . error . messageText , '\n' ) )
}
const { options , errors : optionErrors } = ts . convertCompilerOptionsFromJson (
( config . config as { compilerOptions? : unknown } ) . compilerOptions ,
root ,
'tsconfig.base.json' ,
)
if ( optionErrors . length > 0 ) {
throw new Error ( optionErrors . map ( error = > ts . flattenDiagnosticMessageText ( error . messageText , '\n' ) ) . join ( '\n' ) )
}
// convertCompilerOptionsFromJson leaves `pathsBasePath` unset, so relative
// `paths` targets resolve against the host's current directory; anchor it to
// the repository root to keep the gate cwd-independent.
const host : ts.ModuleResolutionHost = {
fileExists : path = > ts . sys . fileExists ( path ) ,
readFile : path = > ts . sys . readFile ( path ) ,
directoryExists : path = > ts . sys . directoryExists ( path ) ,
getCurrentDirectory : ( ) = > root ,
}
const sourceExtensions = new Set < string > ( [ ts . Extension . Ts , ts . Extension . Tsx ] )
const containingFile = resolve ( root , 'scripts/verify-cordis-config.ts' )
const locationsBySpecifier = new Map < string , Set < string > > ( )
for ( const reference of pluginReferences ) {
const packageName = packageNameFromSpecifier ( reference . name )
if ( packageName === undefined || ! localPackages . has ( packageName ) ) continue
const locations = locationsBySpecifier . get ( reference . name ) ? ? new Set < string > ( )
locations . add ( reference . file )
locationsBySpecifier . set ( reference . name , locations )
}
for ( const [ specifier , locations ] of locationsBySpecifier ) {
const resolved = ts . resolveModuleName ( specifier , containingFile , options , host ) . resolvedModule
if ( resolved !== undefined && sourceExtensions . has ( resolved . extension ) ) continue
violations . push ( ` ${ [ . . . locations ] . join ( ', ' ) } : ${ specifier } does not resolve to workspace source through tsconfig.base.json paths (add a mapping so the tsx source launch does not depend on built lib/) ` )
}
return violations
}
2026-07-28 23:05:20 +08:00
function missingPluginDependencies (
references : readonly PluginReference [ ] ,
dependencies : Readonly < Record < string , string > > ,
2026-08-24 10:37:43 +08:00
dependencyOwner : string ,
2026-07-28 23:05:20 +08:00
) : string [ ] {
const requiredPackages = new Map < string , Set < string > > ( )
2026-07-29 21:09:02 +08:00
const require = ( packageName : string , file : string ) : void = > {
const locations = requiredPackages . get ( packageName ) ? ? new Set < string > ( )
locations . add ( file )
requiredPackages . set ( packageName , locations )
}
2026-07-28 23:05:20 +08:00
for ( const reference of references ) {
const packageName = packageNameFromSpecifier ( reference . name )
if ( packageName === undefined ) continue
2026-07-29 21:09:02 +08:00
require ( packageName , reference . file )
if ( packageName === CHOOSER_PACKAGE ) {
for ( const backend of CHOOSER_BACKEND_PACKAGES ) require ( backend , reference . file )
}
2026-07-28 23:05:20 +08:00
}
return [ . . . requiredPackages ] . flatMap ( ( [ packageName , locations ] ) = > packageName in dependencies
? [ ]
2026-08-24 10:37:43 +08:00
: ` ${ [ . . . locations ] . join ( ', ' ) } : ${ packageName } must be declared in ${ dependencyOwner } ` )
2026-07-28 23:05:20 +08:00
}
2026-08-12 19:28:31 +08:00
function readManifest ( path : string , repoRoot : string = root ) : PackageManifest {
return JSON . parse ( readFileSync ( resolve ( repoRoot , path ) , 'utf8' ) ) as PackageManifest
2026-07-17 23:38:05 +08:00
}
function localPackageDirectories ( ) : Map < string , string > {
const manifests = globSync ( [ 'packages/*/*/package.json' , 'vendor/*/package.json' ] , { cwd : root } )
const packages = new Map < string , string > ( )
for ( const manifestPath of manifests ) {
const manifest = readManifest ( manifestPath )
if ( manifest . name !== undefined ) packages . set ( manifest . name , resolve ( root , dirname ( manifestPath ) ) )
}
return packages
}
function packageNameFromSpecifier ( specifier : string ) : string | undefined {
2026-07-28 23:05:20 +08:00
if ( specifier . startsWith ( '.' ) || specifier . startsWith ( '/' ) || /^[a-z][a-z+.-]*:/i . test ( specifier ) ) return undefined
2026-07-17 23:38:05 +08:00
const segments = specifier . split ( '/' )
if ( specifier . startsWith ( '@' ) ) {
return segments . length >= 2 ? ` ${ segments [ 0 ] } / ${ segments [ 1 ] } ` : undefined
}
return segments [ 0 ] || undefined
}
2026-07-14 21:25:58 +08:00
function validateMetadata ( entry : Record < string , unknown > , file : string , path : string ) : void {
2026-08-11 11:33:53 +08:00
for ( const problem of metadataExpressionErrors ( entry , path ) ) {
errors . push ( ` ${ file } ${ problem } ` )
}
}
/ * *
* Expression - node diagnostics for one entry . ` disabled ` is the single
2026-08-11 18:52:52 +08:00
* interpolated metadata field : its own ` !!js ` expression node is allowed and
* must parse , while expressions nested below it stay truthy data ; every other
* metadata field must stay fully static .
2026-08-11 11:33:53 +08:00
* @param entry - one loader entry ( or patch row ) .
* @param path - the entry ' s diagnostic path prefix .
* @returns one diagnostic per offending expression .
* /
export function metadataExpressionErrors ( entry : Record < string , unknown > , path : string ) : string [ ] {
const problems : string [ ] = [ ]
2026-07-14 21:25:58 +08:00
for ( const field of metadataFields ) {
if ( ! ( field in entry ) ) continue
const expressionPaths : string [ ] = [ ]
collectExpressionPaths ( entry [ field ] , ` ${ path } . ${ field } ` , expressionPaths )
2026-08-11 11:33:53 +08:00
for ( const expressionPath of expressionPaths ) problems . push ( ` ${ expressionPath } : !!js is not interpolated here ` )
2026-07-14 21:25:58 +08:00
}
2026-08-11 18:52:52 +08:00
const disabled = entry . disabled
if ( disabled !== undefined ) {
if ( isJsExpr ( disabled ) ) {
const detail = disabledExpressionProblem ( disabled . __jsExpr )
if ( detail !== undefined ) problems . push ( ` ${ path } .disabled ${ detail } ` )
} else {
// A non-expression value gates on Boolean() at mount; an expression
// nested anywhere below it never evaluates, so it must stay literal.
const expressionPaths : string [ ] = [ ]
collectExpressionPaths ( disabled , ` ${ path } .disabled ` , expressionPaths )
for ( const expressionPath of expressionPaths ) problems . push ( ` ${ expressionPath } : !!js is not interpolated here ` )
}
}
2026-08-11 11:33:53 +08:00
return problems
2026-07-14 21:25:58 +08:00
}
2026-08-11 18:52:52 +08:00
/ * *
* Parse - only validation of a ` disabled ` expression : the Loader evaluates it
* at every mount decision , and a syntax error would fail the boot — rejecting
* it here moves that failure to the earliest resolvable point .
* @param expression - the ` !!js ` expression text .
* @returns the diagnostic suffix , or ` undefined ` when the expression parses .
* /
function disabledExpressionProblem ( expression : string ) : string | undefined {
try {
2026-08-14 14:12:23 +08:00
// Compilation only — constructing a Script does not execute its source.
new Script ( ` ( ${ expression } ) ` )
2026-08-11 18:52:52 +08:00
return undefined
} catch ( error ) {
const detail = error instanceof Error ? error.message : String ( error )
return ` : disabled expression does not parse: ${ detail } `
2026-07-14 21:25:58 +08:00
}
}
function collectExpressionPaths ( value : unknown , path : string , output : string [ ] ) : void {
if ( isJsExpr ( value ) ) {
output . push ( path )
return
}
if ( isUnknownArray ( value ) ) {
for ( let index = 0 ; index < value . length ; index ++ ) collectExpressionPaths ( value [ index ] , ` ${ path } [ ${ index } ] ` , output )
return
}
if ( ! isRecord ( value ) ) return
for ( const [ key , child ] of Object . entries ( value ) ) collectExpressionPaths ( child , ` ${ path } . ${ key } ` , output )
}
function isRecord ( value : unknown ) : value is Record < string , unknown > {
return value !== null && typeof value === 'object'
}
function isUnknownArray ( value : unknown ) : value is unknown [ ] {
return Array . isArray ( value )
}