* feat(release): publish package source, declaration maps, and documentation Every release member publishes its TypeScript source and the declaration maps that resolve into it, and @deepseek-ai/dsh publishes the repository Markdown documentation staged from docs/ at prepack time. The ./src/* export most members already declared now resolves for an installed consumer, and a consumer editor follows a declaration to the implementation. Bundled JavaScript maps stay unpublished; the vendored family and the Desktop runtime file policy are unchanged. Rationale: .agents/notes/implemented/process/2026-10-08-published-source-and-docs.md * feat(release): publish JavaScript maps and compress bundles Every emitted JavaScript map ships beside the bundle it describes, and the workspace build and the client preset compress bundles without mangling identifiers. The documentation payload now mirrors the website publication manifest and is removed again by `postpack`; Desktop and the Python executable exclude the published source, documentation, and maps; `clean` removes a staged copy. Typert's publication contract gains the Remote declaration map. * fix(release): keep node bundles readable for the worker image The browser worker image lowers `yield*` only as a whole statement and rewrites `for await` loops by their source layout, so compressed node-side entries either fail that lowering or lower to unparseable code. Only the browser bundles keep compression, which removes about a third of their text. The bundle-purity assertion now matches the module-loader call regardless of the quote style compression picks, and the release-pack fixture exercises the installed package's new documentation requirement. * feat(release): publish the whole documentation tree `@deepseek-ai/dsh` copies `docs/` in full instead of the website's page set: the maintainer documents, the generated migration records, the translation guidelines, and the `*.i18n.yaml` consistency records ship with the installed package, so the documentation's own links resolve without a checkout. * feat(release): publish the upgrade guides with the documentation The staged documentation keeps the website's page set and adds every `docs/upgrade-guide/` document, which is what an installed reader consults when a released change breaks something. The rest of the maintainer tree stays out. * test(release): expect source and maps in master's new payload cases * feat(release): keep installed docs and source readable in Desktop Desktop keeps the published src/ and docs/ trees inside app.asar, and every model shell call receives DSH_INSTALL_DIR and DSH_NODE_EXECUTABLE so an agent can read them through Electron's Node mode. Staging also generates docs/upgrade-guide/README.md, an index of the upgrade guides grouped by the release they upgrade from. Minified client bundles print string literals with backticks: the worker image packer now recognizes those chunk requests, client license banners are prepended after minification so their text survives, and the affected artifact tests accept either quote. * test(desktop): read archived docs through the Windows PowerShell form Regenerate the config catalog for the shifted shell-env source line. * test(desktop): wait for the GUI-subsystem Electron process from PowerShell * build(release): publish source and maps from the message-quotes package * build(release): publish source and maps from packages new on master * build(release): publish source and maps from the tool-office package
737 lines
37 KiB
TypeScript
737 lines
37 KiB
TypeScript
/**
|
|
* Workspace package invariant checks for package-manager-independent quality
|
|
* gates.
|
|
*
|
|
* Run: `tsx scripts/check-workspace-constraints.ts`.
|
|
*/
|
|
|
|
import { existsSync, globSync, readdirSync, readFileSync } from 'node:fs'
|
|
import { dirname, join, relative, resolve } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { load as loadYaml } from 'js-yaml'
|
|
import {
|
|
isPublicExperimentalPackageDirectory,
|
|
isExperimentalPackageName,
|
|
hasExperimentalPackageReference,
|
|
EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS,
|
|
EXPERIMENTAL_PACKAGE_NAME_PREFIX,
|
|
LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES,
|
|
PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
|
|
} from './experimental-package-policy.ts'
|
|
import { hasTypertRemoteNavigation } from './publication-payload.ts'
|
|
import type { DshBundleManifest } from '../packages/util/package-manifest/src/types.ts'
|
|
import { detailPath } from '../packages/util/plugin-detail/src/document.ts'
|
|
import { bundlePatchFiles } from '../packages/boot/app-boot/src/profile.ts'
|
|
import { OPTIONAL_BUNDLES } from '../packages/util/official-bundles/src/bundles.ts'
|
|
import {
|
|
PACKAGE_ASSET_EXPORT,
|
|
PACKAGE_ASSET_EXPORT_TARGET,
|
|
PACKAGE_ASSET_FILES_ENTRY,
|
|
} from '../packages/client/modules/src/assets.ts'
|
|
import { collectProjectReferenceFaceViolations } from './project-reference-faces.ts'
|
|
|
|
const root = resolve(import.meta.dirname, '..')
|
|
// Publication rules cover these package trees; dependency rules read all pnpm members.
|
|
const workspaceGlobs = [
|
|
{ dir: 'vendor', depth: 1 },
|
|
{ dir: 'packages', depth: 2 },
|
|
{ dir: 'native', depth: 1 },
|
|
{ dir: 'native/system/packages', depth: 1 },
|
|
{ dir: 'apps', depth: 1 },
|
|
] as const
|
|
const vendoredPackages = new Set([
|
|
'@deepseek-ai/cordis',
|
|
'@deepseek-ai/cosmokit',
|
|
'@deepseek-ai/schemastery',
|
|
'@deepseek-ai/cordis-plugin-loader',
|
|
'@deepseek-ai/cordis-plugin-include',
|
|
'@deepseek-ai/cordis-plugin-group',
|
|
'@deepseek-ai/cordis-plugin-timer',
|
|
'@deepseek-ai/cordis-plugin-hmr',
|
|
'@deepseek-ai/cordis-plugin-logger-console',
|
|
])
|
|
const publicNativePackages = new Set([
|
|
'@deepseek-ai/node-addon-system',
|
|
'@deepseek-ai/node-addon-system-darwin-arm64',
|
|
'@deepseek-ai/node-addon-system-darwin-x64',
|
|
'@deepseek-ai/node-addon-system-linux-arm64',
|
|
'@deepseek-ai/node-addon-system-linux-x64',
|
|
])
|
|
/** Public source home recorded in maintained package manifests. */
|
|
const publishedRepositoryUrl = 'git+https://github.com/deepseek-ai/deepseek-harness.git'
|
|
/** Packages that participate in the experimental policy. */
|
|
const experimentalPackageDirectory = /^packages\/experimental\/[^/]+$/
|
|
/** Private reference tool run from the repository; it publishes no files. */
|
|
const pluginMarketGeneratorDirectory = 'apps/plugin-market-generator'
|
|
/**
|
|
* Ordinary directories whose packages this repository publishes: one release member each. The reference plugin
|
|
* market generator is a private repository tool, not a published application.
|
|
*/
|
|
const standardReleaseMemberDirectory = new RegExp(
|
|
`^(?:packages/(?!experimental/)[^/]+/[^/]+|apps/(?!desktop(?:-host)?$|${pluginMarketGeneratorDirectory.slice('apps/'.length)}$)[^/]+|vendor/[^/]+)$`,
|
|
'u',
|
|
)
|
|
/** Installable application assembled by electron-builder rather than published to npm. */
|
|
const desktopApplicationDirectory = 'apps/desktop'
|
|
const localArtifactDirs = new Set(['node_modules'])
|
|
const appPackageFiles: Readonly<Record<string, readonly string[]>> = {
|
|
// Only `prepack` stages `docs`; every other reader of this directory sees
|
|
// whatever the checkout holds.
|
|
'@deepseek-ai/dsh': ['lib/*.js', 'lib/*.js.map', 'lib/types/*.d.ts', 'lib/types/*.d.ts.map', 'docs', 'src'],
|
|
'@deepseek-ai/dsh-desktop-host': [
|
|
'lib/index.js', 'lib/cli.js',
|
|
],
|
|
// The worker-preview surface (dist/preview.html and dist/preview/) backs
|
|
// opt-in experimental packages and is not published.
|
|
'@deepseek-ai/dsh-web-frontend': ['dist', '!dist/preview.html', '!dist/preview', 'src'],
|
|
}
|
|
|
|
/** The subset of package.json fields this constraint check cares about. */
|
|
export interface PackageManifest {
|
|
name?: string
|
|
version?: string
|
|
private?: boolean
|
|
type?: string
|
|
main?: string
|
|
types?: string
|
|
bin?: string | Record<string, string>
|
|
exports?: Record<string, ExportTarget | undefined>
|
|
files?: string[]
|
|
icon?: string
|
|
publishConfig?: { access?: string }
|
|
repository?: { type?: string; url?: string; directory?: string }
|
|
peerDependencies?: Record<string, string>
|
|
devDependencies?: Record<string, string>
|
|
dependencies?: Record<string, string>
|
|
optionalDependencies?: Record<string, string>
|
|
dsh?: {
|
|
bundle?: DshBundleManifest
|
|
}
|
|
}
|
|
|
|
/** Node package export target: a path, a fallback list, a conditional map, or an exclusion. */
|
|
export type ExportTarget = string | readonly ExportTarget[] | { readonly [condition: string]: ExportTarget | undefined } | null
|
|
|
|
/** One workspace manifest and its repo-relative path. */
|
|
export interface WorkspaceManifest {
|
|
dir: string
|
|
manifest: PackageManifest
|
|
}
|
|
|
|
function readJson(path: string): PackageManifest {
|
|
return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
|
|
}
|
|
|
|
const rootManifest = readJson(join(root, 'package.json'))
|
|
const repositoryVersion = rootManifest.version
|
|
const nativeWorkspaceManifest = readJson(join(root, 'native/system/package.json'))
|
|
const nativeVersion = nativeWorkspaceManifest.version
|
|
|
|
/** Repo-relative dirs holding a package.json, walked to the configured depth. */
|
|
function packageDirs(base: string, depth: number): string[] {
|
|
if (depth === 1) {
|
|
return readdirSync(join(root, base), { withFileTypes: true })
|
|
.filter(entry => entry.isDirectory())
|
|
.filter(entry => !localArtifactDirs.has(entry.name))
|
|
.filter(entry => existsSync(join(root, base, entry.name, 'package.json')))
|
|
.map(entry => `${base}/${entry.name}`)
|
|
}
|
|
return readdirSync(join(root, base), { withFileTypes: true })
|
|
.filter(entry => entry.isDirectory())
|
|
.filter(entry => !localArtifactDirs.has(entry.name))
|
|
.flatMap(group => packageDirs(`${base}/${group.name}`, depth - 1))
|
|
}
|
|
|
|
function workspaceManifests(): WorkspaceManifest[] {
|
|
const manifests: WorkspaceManifest[] = [
|
|
{ dir: '.', manifest: rootManifest },
|
|
]
|
|
|
|
for (const { dir: base, depth } of workspaceGlobs) {
|
|
for (const dir of packageDirs(base, depth)) {
|
|
manifests.push({ dir, manifest: readJson(join(root, dir, 'package.json')) })
|
|
}
|
|
}
|
|
|
|
return manifests
|
|
}
|
|
|
|
/**
|
|
* Read the root manifest and every member declared by pnpm-workspace.yaml.
|
|
* @param repositoryRoot - repository or fixture root containing the workspace declaration.
|
|
* @returns Manifests with normalized repository-relative directories.
|
|
* @throws When the workspace declaration is invalid or matches no member manifests.
|
|
*/
|
|
export function readWorkspaceManifests(repositoryRoot: string): WorkspaceManifest[] {
|
|
const config = loadYaml(readFileSync(join(repositoryRoot, 'pnpm-workspace.yaml'), 'utf8'))
|
|
if (typeof config !== 'object' || config === null || !('packages' in config)
|
|
|| !Array.isArray(config.packages) || config.packages.length === 0
|
|
|| !config.packages.every((member: unknown): member is string => typeof member === 'string' && member.length > 0)) {
|
|
throw new Error('pnpm-workspace.yaml: packages must be a non-empty list of workspace patterns')
|
|
}
|
|
const patterns = config.packages.filter(member => !member.startsWith('!')).map(member => `${member}/package.json`)
|
|
const exclude = config.packages.filter(member => member.startsWith('!')).map(member => `${member.slice(1)}/package.json`)
|
|
const paths = globSync(patterns, {
|
|
cwd: repositoryRoot,
|
|
exclude: ['**/node_modules/**', '**/.git/**', ...exclude],
|
|
}).map(path => path.replaceAll('\\', '/'))
|
|
if (paths.length === 0) throw new Error('pnpm-workspace.yaml: packages matched no workspace manifests')
|
|
return [...new Set(['package.json', ...paths])].sort().map(path => ({
|
|
dir: dirname(path).replaceAll('\\', '/'),
|
|
manifest: readJson(join(repositoryRoot, path)),
|
|
}))
|
|
}
|
|
|
|
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
|
// Tools entry points share error classes and invocation stages through runtime chunks.
|
|
'@deepseek-ai/dsh-tools': ['lib/pipeline.js', 'lib/shared-*.js'],
|
|
// The embedded OAR implementation retains its upstream license.
|
|
'@deepseek-ai/dsh-experimental-subagent-oar': ['LICENSE-OAR'],
|
|
// CUA publishes its client API and attributed native source for local macOS builds.
|
|
'@deepseek-ai/dsh-cua': ['lib/remote-types.js', 'lib/api.js', 'lib/bridge.js', 'lib/native-client.js', 'lib/protocol.js'],
|
|
'@deepseek-ai/dsh-experimental-cua-macos': ['native/bin', 'cua-helper.json', 'native/Package.swift', 'native/Info.plist', 'native/DSHComputerUse.icns', 'native/build_app.py', 'native/Sources', 'native/tests/swift', 'native/tests/test_swift.py', 'native/LICENSE.cua-driver', 'native/LICENSE.computer-use-icon', 'native/upstream.json', 'native/README.md', 'native/README.zh.md', 'native/protocol.schema.json'],
|
|
'@deepseek-ai/dsh-webhook-github': ['examples/github-review/cordis.yml', 'examples/github-review/github-ready-review-rule.mjs'],
|
|
// Liveshare publishes its built Relay gateway entry and the shared runtime
|
|
// chunk that entry imports. Its `./types` export is a type-only module, so
|
|
// the emitted tree contributes that one runtime file; the declarations still
|
|
// ship as the whole tree.
|
|
'@deepseek-ai/dsh-experimental-liveshare': [
|
|
'lib/owner-gateway/cli.js',
|
|
'lib/retry-backoff-*.js',
|
|
],
|
|
// Owned Worker bundles import this public bootstrap before their business entry.
|
|
'@deepseek-ai/dsh-app-boot': ['lib/worker/profile-resolution-bootstrap.js'],
|
|
// Statically linked client libraries keep their stylesheets next to the emitted
|
|
// JavaScript, which imports them by relative path: the compile shell runs
|
|
// them through its own CSS pipeline, so the sheets are published artifacts.
|
|
// The glob covers whichever sheets a package emits; sourcemaps stay
|
|
// unpublished, as everywhere else in the repository.
|
|
'@deepseek-ai/dsh-client-ui-primitives': ['lib/**/*.css'],
|
|
'@deepseek-ai/dsh-client-ui-dockkit': ['lib/**/*.css'],
|
|
'@deepseek-ai/dsh-client-ui-sidebar-documentpreview': ['lib/client.*.js'],
|
|
'@deepseek-ai/dsh-client-ui-sidebar-terminal': ['lib/client.*.js'],
|
|
'@deepseek-ai/dsh-client-web': ['lib/**/*.css', 'lib/apply-injections.js'],
|
|
'@deepseek-ai/dsh-client-ui-theme': ['lib/styles'],
|
|
// The physical-key protocol is a public entry usable without the browser service.
|
|
'@deepseek-ai/dsh-client-shortcuts': ['lib/protocol.js'],
|
|
// The CPython side ships as source .py files, published as-is rather than built.
|
|
'@deepseek-ai/dsh-experimental-ptc-runtime-python': ['py/**/*.py'],
|
|
'@deepseek-ai/dsh-experimental-speech-to-text-sensevoice': ['runtime/assets.json'],
|
|
// Node profile configuration and process bootstraps have separate bundles.
|
|
'@deepseek-ai/dsh-ptc-runtime-node': ['lib/configuration.js', 'lib/process.js', 'lib/persistent-process.js'],
|
|
// The Inspector owns a Worker and a mirrored frontend outside package export paths.
|
|
'@deepseek-ai/dsh-experimental-inspector': ['lib/client.*.js', 'lib/worker.js', 'lib/devtools/**'],
|
|
// Creator's composition guidance travels with the declaration package.
|
|
'@deepseek-ai/dsh-agent-preset': ['skills'],
|
|
// Personalized pages ship the location guidance used by their Creator.
|
|
'@deepseek-ai/dsh-creation-recommendations': ['references/*.md', 'skills/*.md'],
|
|
// Official-plugin recommendations ship the skill text their provider loads.
|
|
'@deepseek-ai/dsh-plugin-recommendations': ['skills/*.md'],
|
|
// The Web Host mounts the default-off settings owner independently of the
|
|
// delegation-tool instances.
|
|
'@deepseek-ai/dsh-tool-subagent': ['lib/model-selection-settings.js'],
|
|
// The JSONL backend resolves its private verification Worker relative to
|
|
// import.meta.url; it is shipped without a public package subpath.
|
|
'@deepseek-ai/dsh-session-persistence-jsonl': ['lib/worker.cjs'],
|
|
// The plugin market parses fetched indexes in a private Worker resolved
|
|
// relative to import.meta.url; it is shipped without a public package subpath.
|
|
'@deepseek-ai/dsh-plugin-market': ['lib/worker.cjs'],
|
|
// The argv-prefix runner entry ships beside the lib as its own bundle;
|
|
// sandbox-local resolves it through the package's ./runner export. tsdown
|
|
// also shares its generated FFI code through a hashed runtime chunk.
|
|
'@deepseek-ai/dsh-sandbox-windows-acl': ['lib/runner.js', 'lib/types-*.js', 'assets'],
|
|
'@deepseek-ai/dsh-skill-badge': ['assets'],
|
|
'@deepseek-ai/dsh-skill-office': ['assets'],
|
|
'@deepseek-ai/dsh-subprocess': ['lib/control.js'],
|
|
// SSH launches a private helper and shares wire definitions and TLS setup
|
|
// between that helper and the connection owner.
|
|
'@deepseek-ai/dsh-ssh': [
|
|
'lib/helper.js', 'lib/protocol.js', 'lib/schemas.js',
|
|
'lib/protocol-*.js', 'lib/schemas-*.js', 'lib/stream-security-*.js',
|
|
],
|
|
// Ordinary native containment ships a path-loaded runner and its shared
|
|
// runner chunk beside the existing node-pty permission repair.
|
|
'@deepseek-ai/dsh-subprocess-local': [
|
|
'lib/runner.js',
|
|
'lib/runner-*.js',
|
|
'lib/output.js',
|
|
'scripts/ensure-spawn-helper.mjs',
|
|
],
|
|
// tsdown shares the repository/pack code between the lib entry and the bin
|
|
// through a hashed chunk. The committed bin.js is the link target pnpm can
|
|
// resolve at install time, before the build produces lib/bin.js.
|
|
'@deepseek-ai/dsh-experimental-webworker-packer': ['bin.js', 'lib/repository-*.js'],
|
|
// Startup and runtime share the advertised URL parser.
|
|
'@deepseek-ai/dsh-web-app': ['lib/public-url-*.js'],
|
|
// The headless entry and its startup row share the JSON projection code
|
|
// through a hashed tsdown chunk; both import it by relative path.
|
|
'@deepseek-ai/dsh-headless': ['lib/json-stream-*.js'],
|
|
}
|
|
|
|
function sameStringList(actual: readonly string[] | undefined, expected: readonly string[]): boolean {
|
|
return !!actual && actual.length === expected.length && actual.every((value, index) => value === expected[index])
|
|
}
|
|
|
|
/**
|
|
* List the publication entries that locale `meta.detail` documents need: the top-level directory holding
|
|
* each document and its images.
|
|
* @param packageDir - absolute package directory.
|
|
* @returns sorted, deduplicated entries.
|
|
* @throws Error when a declared document is not a package-relative Markdown file in a subdirectory or does not exist,
|
|
* so a typo cannot publish a directory that holds no document.
|
|
*/
|
|
export function detailDocumentFiles(packageDir: string): readonly string[] {
|
|
const entries = new Set<string>()
|
|
for (const file of globSync('locale/*.json', { cwd: packageDir })) {
|
|
let meta: unknown
|
|
try {
|
|
meta = (JSON.parse(readFileSync(join(packageDir, file), 'utf8')) as { meta?: unknown } | null)?.meta
|
|
} catch (error: unknown) {
|
|
// SyntaxError: verify-package-meta reports malformed locale files; this check only adds publication entries.
|
|
void error
|
|
continue
|
|
}
|
|
const label = `${join(packageDir, file)}: meta.detail`
|
|
const detail = detailPath(typeof meta === 'object' && meta !== null ? (meta as { detail?: unknown }).detail : undefined, label)
|
|
if (detail === undefined) continue
|
|
const slash = detail.indexOf('/')
|
|
if (slash < 0) throw new Error(`${label} ${JSON.stringify(detail)} must name a file in a subdirectory such as docs/`)
|
|
if (!existsSync(join(packageDir, detail))) throw new Error(`${label} ${JSON.stringify(detail)} does not exist`)
|
|
entries.add(detail.slice(0, slash))
|
|
}
|
|
return [...entries].sort()
|
|
}
|
|
|
|
/**
|
|
* Compute canonical publication patterns, including declared and exported icon paths, exported locale JSON resources,
|
|
* detail documents, and the built package-asset directory when `exports["./assets/*"]` targets it.
|
|
* @param manifest - workspace package manifest.
|
|
* @param detailFiles - entries from {@link detailDocumentFiles}.
|
|
* @returns the icon, deduplicated locale targets, and detail documents followed by runtime, package-asset, and declaration payloads.
|
|
*/
|
|
export function expectedDshPackageFiles(manifest: PackageManifest, detailFiles: readonly string[] = []): readonly string[] {
|
|
const localeFiles = new Set<string>()
|
|
for (const resource of Object.keys(manifest.exports ?? {})) {
|
|
if (!/^\.\/(?:.+\/)?locale\/[^/]+\.json$/u.test(resource)) continue
|
|
const target = exportDefault(manifest, resource)
|
|
if (target?.startsWith('./') && target.endsWith('.json')) localeFiles.add(target.slice(2))
|
|
}
|
|
const browserClient = exportDefault(manifest, './client')
|
|
const bundle = manifest.dsh?.bundle
|
|
const bundleFiles = bundle === undefined ? [] : bundlePatchFiles(bundle).map(file => file.replace(/^\.\//, ''))
|
|
const extras = [
|
|
...bundleFiles,
|
|
...(manifest.name ? packageFileExtras[manifest.name] ?? [] : []),
|
|
]
|
|
const targets = (value: ExportTarget | undefined): string[] => typeof value === 'string' ? [value]
|
|
: typeof value === 'object' && value !== null ? Object.values(value).flatMap(targets) : []
|
|
const icons = [
|
|
...typeof manifest.icon === 'string' ? [manifest.icon.replace(/^\.\//u, '')] : [],
|
|
...Object.entries(manifest.exports ?? {}).filter(([key]) => /^\.\/(?:.+\/)?icon$/u.test(key))
|
|
.flatMap(([, target]) => targets(target)).filter(icon => icon.startsWith('./')).map(icon => icon.slice(2)),
|
|
]
|
|
return [
|
|
...new Set(icons),
|
|
...[...localeFiles].sort(),
|
|
...detailFiles,
|
|
'lib/index.js',
|
|
...manifest.bin ? ['lib/bin.js'] : [],
|
|
// Worker-thread packages ship a CJS worker entry; the browser worker
|
|
// bundle is an ES module a page loads with `new Worker(type: 'module')`.
|
|
// Keyed on the artifact path, like ./client below.
|
|
...exportDefault(manifest, './worker') === './lib/worker.cjs' ? ['lib/worker.cjs'] : [],
|
|
...exportDefault(manifest, './worker') === './lib/worker.js' ? ['lib/worker.js'] : [],
|
|
// Only physical browser bundles need a publication entry; a ./client
|
|
// subpath may instead expose a browser-safe emitted source tree.
|
|
...browserClient === './lib/client.js' ? ['lib/client.js'] : [],
|
|
...browserClient === './lib/client.browser.js' ? ['lib/client.browser.js'] : [],
|
|
...exportCondition(manifest, './client', 'node') === './lib/client.node.js' ? ['lib/client.node.js'] : [],
|
|
...exportDefault(manifest, PACKAGE_ASSET_EXPORT) === PACKAGE_ASSET_EXPORT_TARGET ? [PACKAGE_ASSET_FILES_ENTRY] : [],
|
|
// runtime's shell-held loader subpath ships as its own bundle beside the client half.
|
|
...exportDefault(manifest, './loader') === './lib/loader.js' ? ['lib/loader.js'] : [],
|
|
// A store subpath ships its own bundle (single-entry builds; no shared chunk).
|
|
...exportDefault(manifest, './store') === './lib/store/index.js' ? ['lib/store/index.js'] : [],
|
|
// A surface bundle's startup row is its own bundle: the Loader imports it
|
|
// as a row module, so it cannot ride inside the package entry.
|
|
...exportDefault(manifest, './startup') === './lib/startup.js' ? ['lib/startup.js'] : [],
|
|
...extras,
|
|
// Subpaths whose runtime default is the tsc-emitted tree (lib/types/*.js —
|
|
// browser-safe source channels rehomed off src so plain Node can import
|
|
// them without type stripping) publish the emitted JS alongside the
|
|
// declarations.
|
|
...usesEmittedTreeDefaults(manifest) ? ['lib/types/**/*.js'] : [],
|
|
'lib/types/**/*.d.ts',
|
|
// A published declaration map plus `src` lets a consumer editor open the
|
|
// implementation behind an exported declaration, and lets a plugin author
|
|
// read it without a repository checkout.
|
|
'lib/types/**/*.d.ts.map',
|
|
// Every emitted map ships beside the JavaScript it describes, so a stack
|
|
// frame or a debugger session resolves into the published `src`.
|
|
'lib/**/*.js.map',
|
|
...hasExportPair(manifest, './typert', './lib/typert.host.d.ts', './lib/typert.host.js')
|
|
? ['lib/typert.host.js', 'lib/typert.host.d.ts']
|
|
: [],
|
|
...hasExportPair(manifest, './client/typert', './lib/typert.client.d.ts', './lib/typert.client.js')
|
|
? ['lib/typert.client.js', 'lib/typert.client.d.ts']
|
|
: [],
|
|
...hasTypertRemoteNavigation(manifest)
|
|
// The generated Remote declaration carries a `sourceMappingURL` its map
|
|
// resolves, and that map resolves the Host method through `src`.
|
|
? ['lib/typert.remote-client.js', 'lib/typert.remote-client.d.ts', 'lib/typert.remote-client.d.ts.map']
|
|
: [],
|
|
'src',
|
|
]
|
|
}
|
|
|
|
/** Fields of a conditional export; scalar and list targets have no named conditions. */
|
|
function exportFields(value: ExportTarget | undefined): { readonly [condition: string]: ExportTarget | undefined } | undefined {
|
|
return typeof value !== 'object' || value === null || isExportList(value) ? undefined : value
|
|
}
|
|
|
|
function isExportList(value: ExportTarget): value is readonly ExportTarget[] {
|
|
return Array.isArray(value)
|
|
}
|
|
|
|
/** Whether one conditional export exactly names the generated runtime and declaration pair. */
|
|
function hasExportPair(
|
|
manifest: PackageManifest,
|
|
subpath: string,
|
|
types: string,
|
|
runtime: string,
|
|
): boolean {
|
|
const entry = exportFields(manifest.exports?.[subpath])
|
|
return entry?.types === types && entry.default === runtime
|
|
}
|
|
|
|
/** Runtime target of an export entry: conditional `default`, or the bare-string shorthand. */
|
|
function exportDefault(manifest: PackageManifest, subpath: string): string | undefined {
|
|
const entry = manifest.exports?.[subpath]
|
|
if (typeof entry === 'string') return entry
|
|
const target = exportFields(entry)?.default
|
|
return typeof target === 'string' ? target : undefined
|
|
}
|
|
|
|
/** Exact named condition target of an export entry. */
|
|
function exportCondition(manifest: PackageManifest, subpath: string, condition: string): string | undefined {
|
|
const target = exportFields(manifest.exports?.[subpath])?.[condition]
|
|
return typeof target === 'string' ? target : undefined
|
|
}
|
|
|
|
/** Whether any export's runtime default points into the tsc-emitted lib/types tree. */
|
|
function usesEmittedTreeDefaults(manifest: PackageManifest): boolean {
|
|
return Object.keys(manifest.exports ?? {}).some(subpath =>
|
|
exportDefault(manifest, subpath)?.startsWith('./lib/types/') === true)
|
|
}
|
|
|
|
/** Experimental manifest requirements, including explicit private exceptions. */
|
|
export function checkExperimentalManifest(
|
|
{ dir, manifest }: WorkspaceManifest,
|
|
privateDirectories: readonly string[] = PRIVATE_EXPERIMENTAL_PACKAGE_DIRECTORIES,
|
|
): string[] {
|
|
if (!experimentalPackageDirectory.test(dir)) return []
|
|
const label = manifest.name ?? dir
|
|
const errors: string[] = []
|
|
if (manifest.name === undefined) {
|
|
errors.push(`${label}: experimental package must declare a name`)
|
|
} else if (manifest.name.startsWith(EXPERIMENTAL_PACKAGE_NAME_PREFIX)) {
|
|
if (!LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES.includes(dir)) {
|
|
errors.push(`${label}: new experimental packages must not use the ${JSON.stringify(EXPERIMENTAL_PACKAGE_NAME_PREFIX)} prefix; name it @deepseek-ai/dsh-<name> and declare it in EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS (scripts/experimental-package-policy.ts)`)
|
|
}
|
|
} else if (manifest.name !== EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS[dir]) {
|
|
errors.push(`${label}: experimental package name must be declared for this directory in EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS (scripts/experimental-package-policy.ts)`)
|
|
}
|
|
if (isPublicExperimentalPackageDirectory(dir, privateDirectories)) {
|
|
if (manifest.private === true) errors.push(`${label}: public experimental package must not set "private": true`)
|
|
if (manifest.publishConfig?.access !== 'public') {
|
|
errors.push(`${label}: public experimental package must set publishConfig.access to "public"`)
|
|
}
|
|
} else {
|
|
if (manifest.private !== true) errors.push(`${label}: experimental package must set "private": true`)
|
|
if (manifest.publishConfig !== undefined) errors.push(`${label}: experimental package must omit publishConfig`)
|
|
}
|
|
return errors
|
|
}
|
|
|
|
/**
|
|
* Reject missing, renamed, moved, or duplicated unprefixed-name declarations and stale legacy prefixed directories.
|
|
* @param manifests - every workspace manifest, with repository-relative directories.
|
|
* @returns Violations of the exact directory/name ownership declarations.
|
|
*/
|
|
export function checkExperimentalNameExceptions(manifests: readonly WorkspaceManifest[]): string[] {
|
|
const errors: string[] = []
|
|
for (const [dir, name] of Object.entries(EXPERIMENTAL_PACKAGE_NAME_EXCEPTIONS)) {
|
|
const owners = manifests.filter(entry => entry.manifest.name === name)
|
|
if (owners.length !== 1 || owners[0]?.dir !== dir) {
|
|
errors.push(`${dir}: unprefixed experimental name ${name} must have exactly one workspace owner at this directory`)
|
|
}
|
|
}
|
|
for (const dir of LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES) {
|
|
if (!manifests.some(entry => entry.dir === dir && entry.manifest.name?.startsWith(EXPERIMENTAL_PACKAGE_NAME_PREFIX) === true)) {
|
|
errors.push(`${dir}: remove this directory from LEGACY_PREFIXED_EXPERIMENTAL_PACKAGE_DIRECTORIES; no package with the legacy prefix lives here`)
|
|
}
|
|
}
|
|
return errors
|
|
}
|
|
|
|
function isReleaseMemberDirectory(dir: string): boolean {
|
|
// The SSH carrier is distributed as executable archives, never as an npm application.
|
|
if (dir === 'packages/ssh/ssh-helper-runtime') return false
|
|
return standardReleaseMemberDirectory.test(dir) || isPublicExperimentalPackageDirectory(dir)
|
|
}
|
|
|
|
/**
|
|
* Require a dsh-family manifest to carry the workspace version.
|
|
*
|
|
* The dsh release sequence publishes packages/ and apps/ members and every
|
|
* private dsh package on one shared version, written by `release:dsh` and
|
|
* shared with the workspace root. This name test is that boundary: it covers
|
|
* the family wherever the manifest lives, so apps/ members cannot drift with
|
|
* only the release lane noticing.
|
|
* @param manifest - the workspace package manifest.
|
|
* @param expected - the version every dsh-family manifest must carry (the root's).
|
|
* @returns one violation naming the manifest and the expected version, or
|
|
* undefined when the manifest is compliant or not in the family.
|
|
*/
|
|
export function checkDshFamilyVersion(manifest: PackageManifest, expected: string | undefined): string | undefined {
|
|
const name = manifest.name
|
|
if (name !== '@deepseek-ai/dsh' && name?.startsWith('@deepseek-ai/dsh-') !== true) return undefined
|
|
if (manifest.version !== expected) {
|
|
return `${name}: package.json version must match root version ${expected ?? '(missing)'}`
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Check one workspace manifest against publication and dsh-package policy.
|
|
* @param workspace - package directory and parsed manifest.
|
|
* @returns path-qualified policy violations.
|
|
*/
|
|
export function checkWorkspaceManifest({ dir, manifest }: WorkspaceManifest): string[] {
|
|
const errors = checkExperimentalManifest({ dir, manifest })
|
|
const label = manifest.name ?? dir
|
|
const familyVersionError = checkDshFamilyVersion(manifest, repositoryVersion)
|
|
if (familyVersionError !== undefined) errors.push(familyVersionError)
|
|
const isNativePackageDir = dir.startsWith('native/system/packages/')
|
|
const isPublicNativePackage = isNativePackageDir
|
|
&& manifest.name !== undefined
|
|
&& publicNativePackages.has(manifest.name)
|
|
|
|
if (isPublicNativePackage) {
|
|
if (manifest.private === true) {
|
|
errors.push(`${label}: published Landlock package must not set "private": true`)
|
|
}
|
|
if (manifest.publishConfig?.access !== 'public') {
|
|
errors.push(`${label}: published Landlock package must set publishConfig.access to "public"`)
|
|
}
|
|
const expectedDirectory = dir
|
|
if (manifest.repository?.type !== 'git'
|
|
|| manifest.repository.url !== publishedRepositoryUrl
|
|
|| manifest.repository.directory !== expectedDirectory) {
|
|
errors.push(`${label}: published Landlock package repository must use ${publishedRepositoryUrl} with directory ${expectedDirectory}`)
|
|
}
|
|
} else if (isReleaseMemberDirectory(dir)) {
|
|
// Release members state that they are publishable: npm refuses a private
|
|
// package, and the repository field is how a consumer finds the source of
|
|
// the package it installed.
|
|
//
|
|
// Access is per release sequence, not per scope: the vendored framework and
|
|
// the Landlock packages publish publicly because outside consumers install
|
|
// them, and the dsh family published publicly with its own sequence on
|
|
// 2026-08-13. No publish path passes `--access`; each packed manifest declares
|
|
// it, and this gate requires every release member to be public.
|
|
if (manifest.private === true) {
|
|
errors.push(`${label}: release member must not set "private": true`)
|
|
}
|
|
if (manifest.publishConfig?.access !== 'public') {
|
|
errors.push(`${label}: release member must set publishConfig.access to "public"`)
|
|
}
|
|
if (manifest.repository?.type !== 'git'
|
|
|| manifest.repository.url !== publishedRepositoryUrl
|
|
|| manifest.repository.directory !== dir) {
|
|
errors.push(`${label}: release member repository must use ${publishedRepositoryUrl} with directory ${dir}`)
|
|
}
|
|
} else if (!experimentalPackageDirectory.test(dir) && manifest.private !== true) {
|
|
errors.push(`${label}: package.json must set "private": true`)
|
|
}
|
|
|
|
if (manifest.name && vendoredPackages.has(manifest.name)) {
|
|
return errors
|
|
}
|
|
|
|
if (dir.startsWith('apps/') && dir !== desktopApplicationDirectory && dir !== pluginMarketGeneratorDirectory && manifest.name?.startsWith('@deepseek-ai/')) {
|
|
const expectedFiles = appPackageFiles[manifest.name]
|
|
if (expectedFiles === undefined) {
|
|
errors.push(`${label}: app package has no publication files policy`)
|
|
} else if (!sameStringList(manifest.files, expectedFiles)) {
|
|
errors.push(`${label}: package.json files must be ${JSON.stringify(expectedFiles)}`)
|
|
}
|
|
}
|
|
|
|
if (isNativePackageDir) {
|
|
if (!isPublicNativePackage) {
|
|
errors.push(`${label}: unexpected package in the public Landlock package family`)
|
|
}
|
|
if (manifest.version !== nativeVersion) {
|
|
errors.push(`${label}: package.json version must match native workspace version ${nativeVersion ?? '(missing)'}`)
|
|
}
|
|
}
|
|
|
|
if (dir.startsWith('packages/') && manifest.name?.startsWith('@deepseek-ai/dsh-')) {
|
|
const peer = manifest.peerDependencies?.['@deepseek-ai/cordis']
|
|
const dev = manifest.devDependencies?.['@deepseek-ai/cordis']
|
|
|
|
if (!peer) errors.push(`${label}: @deepseek-ai/cordis must be a peerDependency`)
|
|
if (!dev) errors.push(`${label}: @deepseek-ai/cordis must also be a devDependency`)
|
|
if (peer && dev && peer !== dev) {
|
|
errors.push(`${label}: @deepseek-ai/cordis peer (${peer}) and dev (${dev}) ranges must match`)
|
|
}
|
|
if (manifest.type !== 'module') {
|
|
errors.push(`${label}: package.json must set "type": "module"`)
|
|
}
|
|
if (manifest.main !== 'lib/index.js') {
|
|
errors.push(`${label}: package.json must set "main": "lib/index.js"`)
|
|
}
|
|
if (manifest.types !== 'lib/types/index.d.ts') {
|
|
errors.push(`${label}: package.json must set "types": "lib/types/index.d.ts"`)
|
|
}
|
|
const rootEntry = exportFields(manifest.exports?.['.'])
|
|
if (rootEntry?.types !== './lib/types/index.d.ts') {
|
|
errors.push(`${label}: package.json exports["."].types must be "./lib/types/index.d.ts"`)
|
|
}
|
|
if (rootEntry?.default !== './lib/index.js') {
|
|
errors.push(`${label}: package.json exports["."].default must be "./lib/index.js"`)
|
|
}
|
|
if (manifest.name === '@deepseek-ai/dsh-tools'
|
|
&& !hasExportPair(manifest, './pipeline', './lib/types/pipeline.d.ts', './lib/pipeline.js')) {
|
|
errors.push(`${label}: exports["./pipeline"] must expose lib/types/pipeline.d.ts and lib/pipeline.js`)
|
|
}
|
|
const expectedFiles = expectedDshPackageFiles(manifest, detailDocumentFiles(join(root, dir)))
|
|
if (!sameStringList(manifest.files, expectedFiles)) {
|
|
errors.push(`${label}: package.json files must be ${JSON.stringify(expectedFiles)}`)
|
|
}
|
|
}
|
|
|
|
return errors.map(error => `${relative(root, join(root, dir, 'package.json'))}: ${error}`)
|
|
}
|
|
|
|
/**
|
|
* Enforce `packages/<group>/<pkg>`: groups are open-named containers without a
|
|
* package.json, and packages may be neither flat nor more deeply nested.
|
|
*/
|
|
function checkHierarchyShape(): string[] {
|
|
const errors: string[] = []
|
|
const packagesRoot = join(root, 'packages')
|
|
for (const group of readdirSync(packagesRoot, { withFileTypes: true })) {
|
|
if (!group.isDirectory()) continue
|
|
const groupRel = join('packages', group.name)
|
|
if (existsSync(join(packagesRoot, group.name, 'package.json'))) {
|
|
errors.push(`${groupRel}: a group dir must not contain a package.json — packages live at packages/<group>/<pkg>, not directly under packages/`)
|
|
continue
|
|
}
|
|
for (const pkg of readdirSync(join(packagesRoot, group.name), { withFileTypes: true })) {
|
|
if (!pkg.isDirectory()) continue
|
|
if (localArtifactDirs.has(pkg.name)) continue
|
|
const pkgRel = join(groupRel, pkg.name)
|
|
if (!existsSync(join(packagesRoot, group.name, pkg.name, 'package.json'))) {
|
|
errors.push(`${pkgRel}: expected a package here (no package.json found) — the hierarchy is exactly packages/<group>/<pkg>, no deeper nesting`)
|
|
}
|
|
}
|
|
}
|
|
return errors
|
|
}
|
|
|
|
function checkRepositoryVersion(): string[] {
|
|
// The root carries the dsh release family's version, so a prerelease such as
|
|
// 0.0.1-rc.1 is a valid state between `release:dsh` and its publication.
|
|
if (repositoryVersion && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(repositoryVersion)) return []
|
|
return ['package.json: version must be X.Y.Z with an optional prerelease segment']
|
|
}
|
|
|
|
/** Dependency sections whose ranges reach a published tarball or a local install. */
|
|
const dependencySections = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] as const
|
|
/** Dependency sections present in an installed runtime. */
|
|
const runtimeDependencySections = ['dependencies', 'optionalDependencies', 'peerDependencies'] as const
|
|
|
|
/**
|
|
* Prevent an official runtime from requiring an experimental package. The dsh installation's `dependencies`
|
|
* may hold the bundles the launcher's `OPTIONAL_BUNDLES` names: shipped switched off, they are not a requirement
|
|
* ([rationale](../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md)).
|
|
* @param manifests - release, private experimental, and deployment-root manifests.
|
|
* @param optionalBundles - the bundles the installation ships switched off; the launcher's list by default.
|
|
* @returns One error for each forbidden runtime dependency.
|
|
*/
|
|
export function checkExperimentalDependencyIsolation(
|
|
manifests: readonly WorkspaceManifest[], optionalBundles: readonly string[] = OPTIONAL_BUNDLES,
|
|
): string[] {
|
|
const experimentalNames = new Set(manifests
|
|
.filter(entry => experimentalPackageDirectory.test(entry.dir))
|
|
.map(entry => entry.manifest.name)
|
|
.filter(name => name !== undefined))
|
|
const errors: string[] = []
|
|
for (const { dir, manifest } of manifests) {
|
|
if (!standardReleaseMemberDirectory.test(dir) && dir !== 'python/sdk-runtime') continue
|
|
const offered = manifest.name === '@deepseek-ai/dsh' ? new Set(optionalBundles) : new Set<string>()
|
|
for (const section of runtimeDependencySections) {
|
|
for (const [name, range] of Object.entries(manifest[section] ?? {})) {
|
|
const experimental = experimentalNames.has(name) || isExperimentalPackageName(name)
|
|
|| /^(?:npm:|workspace:)/.test(range) && hasExperimentalPackageReference(range)
|
|
if (!experimental) continue
|
|
if (section === 'dependencies' && offered.has(name)) continue
|
|
errors.push(`${manifest.name ?? dir}: ${section}.${name} must not reference an experimental package`)
|
|
}
|
|
}
|
|
}
|
|
return errors
|
|
}
|
|
|
|
/**
|
|
* Require exact DSH ranges, tilde vendor/native ranges, and the workspace protocol elsewhere.
|
|
*
|
|
* A hand-written range says nothing about the version the workspace actually
|
|
* carries, and `pnpm pack` leaves it alone: `^0.0.1` published from version
|
|
* `0.0.2` names a version that does not exist. The protocol makes pack
|
|
* substitute the member's real version. Dependency targets determine the range,
|
|
* regardless of the consuming manifest's name or directory.
|
|
* @param manifests - every workspace manifest.
|
|
* @returns One error per workspace reference with a disallowed range.
|
|
*/
|
|
export function checkWorkspaceProtocol(manifests: readonly WorkspaceManifest[]): string[] {
|
|
const members = new Set(manifests.map(entry => entry.manifest.name).filter(name => name !== undefined))
|
|
const vendors = new Set(manifests.filter(entry => entry.dir.startsWith('vendor/')
|
|
|| entry.dir === 'native/system' || entry.dir.startsWith('native/system/packages/')).map(entry => entry.manifest.name))
|
|
const errors: string[] = []
|
|
for (const { dir, manifest } of manifests) {
|
|
for (const section of dependencySections) {
|
|
for (const [name, range] of Object.entries(manifest[section] ?? {})) {
|
|
if (!members.has(name)) continue
|
|
const expected = name === '@deepseek-ai/dsh' || name.startsWith('@deepseek-ai/dsh-')
|
|
? 'workspace:*'
|
|
: vendors.has(name) ? 'workspace:~' : undefined
|
|
if (expected !== undefined ? range === expected : range.startsWith('workspace:')) continue
|
|
errors.push(`${manifest.name ?? dir}: ${section}.${name} must use ${expected ?? 'the workspace: protocol'}, got ${range}`)
|
|
}
|
|
}
|
|
}
|
|
return errors
|
|
}
|
|
|
|
/** Run the repository constraint gate. */
|
|
export function main(): void {
|
|
const manifests = readWorkspaceManifests(root)
|
|
const errors = [
|
|
...checkRepositoryVersion(),
|
|
...workspaceManifests().flatMap(checkWorkspaceManifest),
|
|
...checkWorkspaceProtocol(manifests),
|
|
...checkExperimentalNameExceptions(manifests),
|
|
...checkExperimentalDependencyIsolation(manifests),
|
|
...checkHierarchyShape(),
|
|
...collectProjectReferenceFaceViolations(root),
|
|
]
|
|
if (errors.length > 0) {
|
|
console.error(errors.join('\n'))
|
|
process.exitCode = 1
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) main()
|