From df21d22e28406b46a7f13941d771f9d93cf55208 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Fri, 17 Jul 2026 23:38:05 +0800 Subject: [PATCH] fix(examples): enforce config dependency resolution --- examples/package.json | 1 + pnpm-lock.yaml | 3 + scripts/verify-cordis-config.ts | 102 +++++++++++++++++++++++++++++++- 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/examples/package.json b/examples/package.json index a6b6e97bc0..8d77731a05 100644 --- a/examples/package.json +++ b/examples/package.json @@ -30,6 +30,7 @@ "@deepseek-ai/dsh-subagent-spawn": "workspace:*", "@deepseek-ai/dsh-time-context": "workspace:*", "@deepseek-ai/dsh-timeout-policy": "workspace:*", + "@deepseek-ai/dsh-token-meter": "workspace:*", "@deepseek-ai/dsh-tool-cordis": "workspace:*", "@deepseek-ai/dsh-tool-fs": "workspace:*", "@deepseek-ai/dsh-tool-fs-search": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39b7638b69..9810a9a61e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,6 +167,9 @@ importers: '@deepseek-ai/dsh-timeout-policy': specifier: workspace:* version: link:../packages/timeout/timeout-policy + '@deepseek-ai/dsh-token-meter': + specifier: workspace:* + version: link:../packages/llm/token-meter '@deepseek-ai/dsh-tool-cordis': specifier: workspace:* version: link:../packages/cordis/tool-cordis diff --git a/scripts/verify-cordis-config.ts b/scripts/verify-cordis-config.ts index 131bcf0e58..c4075cb256 100644 --- a/scripts/verify-cordis-config.ts +++ b/scripts/verify-cordis-config.ts @@ -1,18 +1,32 @@ /** - * Reject JavaScript expressions in Cordis Loader entry metadata. + * Validate Cordis Loader entry metadata and example package resolution. * * The Loader interpolates only a plugin entry's `config`; expression objects in * fields such as `disabled` remain truthy data and silently change composition. + * Example configs run from built packages, so every named package must resolve + * from the examples workspace and every local package must be in the root + * TypeScript project graph. */ import { globSync, readFileSync } from 'node:fs' -import { resolve } from 'node:path' +import { dirname, relative, resolve } from 'node:path' import * as yaml from 'js-yaml' +import ts from 'typescript' interface JsExpr { __jsExpr: string } +interface PackageManifest { + name?: string + dependencies?: Record +} + +interface PluginReference { + file: string + name: string +} + const root = resolve(import.meta.dirname, '..') const metadataFields = ['id', 'name', 'group', 'disabled', 'inject', 'intercept', 'isolate'] as const const jsExprType = new yaml.Type('tag:yaml.org,2002:js', { @@ -30,6 +44,7 @@ const files = globSync(['**/*cordis*.yml', '**/*cordis*.yaml'], { exclude: ['.claude/**', 'node_modules/**', 'vendor/**'], }).sort() const errors: string[] = [] +const examplePluginReferences: PluginReference[] = [] for (const file of files) { const document: unknown = yaml.load(readFileSync(resolve(root, file), 'utf8'), { schema }) @@ -42,8 +57,10 @@ for (const file of files) { } } +errors.push(...validateExampleResolution()) + if (errors.length > 0) { - console.error('verify-cordis-config: Loader entry metadata is static; move !!js under plugin config or select an explicit overlay.') + console.error('verify-cordis-config: invalid Loader metadata or example package resolution:') for (const error of errors) console.error(`- ${error}`) process.exitCode = 1 } else { @@ -55,6 +72,7 @@ function validateEntry(value: unknown, file: string, path: string): void { errors.push(`${file}${path}: entry must be an object`) return } + recordExamplePlugin(value, file) validateMetadata(value, file, path) if ((value.group === true || value.name === '@cordisjs/plugin-group') && isUnknownArray(value.config)) { for (let index = 0; index < value.config.length; index++) { @@ -68,6 +86,7 @@ function validateEntry(value: unknown, file: string, path: string): void { const patch = config.patches[index] const patchPath = `${path}.config.patches[${index}]` if (!isRecord(patch)) continue + recordExamplePlugin(patch, file) validateMetadata(patch, file, patchPath) if (!isUnknownArray(patch.insert)) continue for (let insertIndex = 0; insertIndex < patch.insert.length; insertIndex++) { @@ -76,6 +95,83 @@ function validateEntry(value: unknown, file: string, path: string): void { } } +function recordExamplePlugin(entry: Record, file: string): void { + if (file.startsWith('examples/') && typeof entry.name === 'string') { + examplePluginReferences.push({ file, name: entry.name }) + } +} + +function validateExampleResolution(): string[] { + const violations: string[] = [] + const exampleManifest = readManifest('examples/package.json') + const dependencies = exampleManifest.dependencies ?? {} + const localPackages = localPackageDirectories() + const rootReferences = rootProjectReferences() + const requiredPackages = new Map>() + + for (const reference of examplePluginReferences) { + const packageName = packageNameFromSpecifier(reference.name) + if (packageName === undefined) continue + const locations = requiredPackages.get(packageName) ?? new Set() + locations.add(reference.file) + requiredPackages.set(packageName, locations) + } + + for (const [packageName, locations] of requiredPackages) { + if (!(packageName in dependencies)) { + violations.push(`${[...locations].join(', ')}: ${packageName} must be declared in examples/package.json dependencies`) + } + } + + const localExamplePackages = new Set([ + ...Object.keys(dependencies), + ...requiredPackages.keys(), + ]) + for (const packageName of localExamplePackages) { + const packageDirectory = localPackages.get(packageName) + if (packageDirectory === undefined || rootReferences.has(packageDirectory)) continue + const repoPath = relative(root, packageDirectory).replaceAll('\\', '/') + violations.push(`tsconfig.json: missing project reference for ${packageName} (${repoPath})`) + } + + return violations +} + +function readManifest(path: string): PackageManifest { + return JSON.parse(readFileSync(resolve(root, path), 'utf8')) as PackageManifest +} + +function localPackageDirectories(): Map { + const manifests = globSync(['packages/*/*/package.json', 'vendor/*/package.json'], { cwd: root }) + const packages = new Map() + for (const manifestPath of manifests) { + const manifest = readManifest(manifestPath) + if (manifest.name !== undefined) packages.set(manifest.name, resolve(root, dirname(manifestPath))) + } + return packages +} + +function rootProjectReferences(): Set { + const config = ts.readConfigFile(resolve(root, 'tsconfig.json'), ts.sys.readFile) + if (config.error !== undefined) { + throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n')) + } + const references = (config.config as { references?: Array<{ path?: unknown }> }).references ?? [] + return new Set(references.flatMap((reference) => { + if (typeof reference.path !== 'string') return [] + return [resolve(root, reference.path)] + })) +} + +function packageNameFromSpecifier(specifier: string): string | undefined { + if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('file:')) return undefined + const segments = specifier.split('/') + if (specifier.startsWith('@')) { + return segments.length >= 2 ? `${segments[0]}/${segments[1]}` : undefined + } + return segments[0] || undefined +} + function validateMetadata(entry: Record, file: string, path: string): void { for (const field of metadataFields) { if (!(field in entry)) continue