Merge commit 'refs/codex/pr885/master-20260730' into worktree/retarget-pr885-20260729
# Conflicts: # eslint.config.mjs
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/typert/registry/README.md
|
||||
README.md: 83c03ab284abf2b7cab4dd1ee70d7e855184a1e0
|
||||
README.zh.md: 6ef8b22805e21a379b4c2fac4bf9fbae447c41a1
|
||||
@@ -0,0 +1,31 @@
|
||||
# @deepseek-ai/dsh-typert-registry
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Runtime registry for generated Typert artifacts. A contribution carries one package face's business reflection and optional live Zod schemas; `ctx.typert` registers both atomically and withdraws them with the calling Cordis fiber. TypeScript analysis and code generation live in [`dsh-typert-generator`](../generator/README.md).
|
||||
|
||||
Package reflection is keyed by `<package>#<face>`. Schemas are keyed by `<package>#<name>` and retain the producer's Zod instance. JSON Schema is computed on demand at the consumer edge.
|
||||
|
||||
## Public API
|
||||
|
||||
- `TypertRegistry` is the default plugin and provides `ctx.typert`.
|
||||
- `register(contribution)` rejects malformed identities and duplicate package-face or schema keys before committing anything, then returns the exact Cordis effect disposer.
|
||||
- `get(key)`, `resolve(key)`, and `list(filter?)` query live schemas. `resolve()` distinguishes a malformed key, an absent package, and a package that contributes no schema under that name.
|
||||
- `getPackage(packageName, face?)` and `listPackages(filter?)` query generated service, event, and object reflection; the default face is `host`.
|
||||
- `toJSONSchema(key, params?)` projects a live schema with `z.toJSONSchema()` without caching the result.
|
||||
- `typertKey()` and `typertPackageKey()` compose the two stable identity forms.
|
||||
|
||||
The `@deepseek-ai/dsh-typert-registry/types` subpath contains the pure contribution and record contracts. [`dsh-typert-loader`](../loader/README.md) discovers and registers generated host artifacts in Loader compositions; direct `ctx.typert.register()` supports other composition owners.
|
||||
|
||||
## Model Experience
|
||||
|
||||
None, as the registry contributes no prompt, tool, or session event; consumers such as `cordis_inspect` own any model-visible projection.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
No direct effect. A consumer that places reflection in a request owns the resulting prefix change.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- The registry stores generated reflection but does not merge host and client graphs or resolve TypeScript references. Those are analyzer and emitter concerns.
|
||||
- Schema keys omit the face because host and client run in separate contexts. Registering same-named schemas from both faces into one context is rejected as a duplicate.
|
||||
@@ -0,0 +1,31 @@
|
||||
# @deepseek-ai/dsh-typert-registry
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
生成的 Typert 产物所用的运行时注册表。每个注册项包含某个包(package)在一个 face 上的业务反射信息,以及可选的运行时 Zod schema;`ctx.typert` 会以原子方式同时注册两者,并在发起调用的 Cordis fiber 释放时一并移除它们。TypeScript 分析和代码生成由 [`dsh-typert-generator`](../generator/README.md) 负责。
|
||||
|
||||
包反射信息以 `<package>#<face>` 为键。schema 以 `<package>#<name>` 为键,并保留生成方的 Zod 实例。系统按需在消费方边界计算 JSON Schema。
|
||||
|
||||
## 公开 API
|
||||
|
||||
- `TypertRegistry` 是默认插件,并提供 `ctx.typert`。
|
||||
- `register(contribution)` 会在提交任何内容之前拒绝格式错误的标识,以及重复的包与 face 组合键或 schema 键,随后返回 Cordis effect 提供的同一资源释放函数。
|
||||
- `get(key)`、`resolve(key)` 和 `list(filter?)` 查询当前有效的 schema。`resolve()` 能区分格式错误的键、未注册的包,以及已注册但未以该名称提供 schema 的包。
|
||||
- `getPackage(packageName, face?)` 和 `listPackages(filter?)` 查询生成的服务、事件和对象反射信息;默认 face 为 `host`。
|
||||
- `toJSONSchema(key, params?)` 使用 `z.toJSONSchema()` 投影当前有效的 schema,且不缓存结果。
|
||||
- `typertKey()` 和 `typertPackageKey()` 构造两种稳定的标识形式。
|
||||
|
||||
`@deepseek-ai/dsh-typert-registry/types` 子路径包含注册项和记录的纯类型契约。[`dsh-typert-loader`](../loader/README.md) 会在 Loader 组合中发现并注册生成的宿主侧产物;其他组合所有者可以直接调用 `ctx.typert.register()`。
|
||||
|
||||
## 模型体验
|
||||
|
||||
无。注册表不会提供提示词、工具或会话事件;所有模型可见投影均由 `cordis_inspect` 等消费方负责。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
无直接影响。将反射信息放入请求的消费方负责由此产生的前缀变化。
|
||||
|
||||
## 已知限制与暂缓工作
|
||||
|
||||
- 注册表存储生成的反射信息,但不会合并宿主侧与客户端侧的图,也不会解析 TypeScript 引用;这些由分析器和产物输出器负责。
|
||||
- schema 键不包含 face,因为宿主侧和客户端侧在不同的上下文中运行。若在同一上下文中注册来自两个 face 的同名 schema,系统会将其作为重复项拒绝。
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-typert-registry",
|
||||
"description": "Runtime registry for generated package reflection and Zod schemas",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./lib/types/types.d.ts",
|
||||
"default": "./lib/types/types.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Runtime registry for generated Typert contributions. It owns live Zod
|
||||
* instances and generated package reflection, but performs no TypeScript
|
||||
* analysis or schema generation.
|
||||
* @module @deepseek-ai/dsh-typert-registry
|
||||
*/
|
||||
|
||||
import { Context, Service } from 'cordis'
|
||||
import { z } from 'zod'
|
||||
import type {
|
||||
TypertContribution,
|
||||
TypertFace,
|
||||
TypertPackageFilter,
|
||||
TypertPackageRecord,
|
||||
TypertSchemaFilter,
|
||||
TypertSchemaRecord,
|
||||
} from './types.ts'
|
||||
|
||||
export type {
|
||||
TypertContribution,
|
||||
TypertDocTag,
|
||||
TypertDocumentation,
|
||||
TypertEventModel,
|
||||
TypertFace,
|
||||
TypertMemberModel,
|
||||
TypertObjectModel,
|
||||
TypertPackageFilter,
|
||||
TypertPackageModel,
|
||||
TypertPackageRecord,
|
||||
TypertSchema,
|
||||
TypertSchemaFilter,
|
||||
TypertSchemaRecord,
|
||||
TypertServiceModel,
|
||||
TypertTypeModel,
|
||||
} from './types.ts'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
typert: TypertRegistry
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the global key of one generated schema.
|
||||
* @param packageName - contributing npm package.
|
||||
* @param name - schema export name.
|
||||
* @returns `<package>#<name>`.
|
||||
*/
|
||||
export function typertKey(packageName: string, name: string): string {
|
||||
return `${packageName}#${name}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the identity of one package-face model.
|
||||
* @param packageName - contributing npm package.
|
||||
* @param face - independently compiled face.
|
||||
* @returns `<package>#<face>`.
|
||||
*/
|
||||
export function typertPackageKey(packageName: string, face: TypertFace): string {
|
||||
return `${packageName}#${face}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry of generated schemas and package reflection.
|
||||
* @typert service
|
||||
*/
|
||||
export class TypertRegistry extends Service {
|
||||
private readonly schemas = new Map<string, TypertSchemaRecord>()
|
||||
private readonly packages = new Map<string, TypertPackageRecord>()
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'typert')
|
||||
}
|
||||
|
||||
/**
|
||||
* Register one generated contribution atomically for the calling fiber.
|
||||
* Duplicate package-face identities or schema keys reject the whole batch.
|
||||
* @param contribution - generated schemas and package metadata.
|
||||
* @returns the exact effect disposer that removes this contribution.
|
||||
*/
|
||||
register(contribution: TypertContribution): () => void {
|
||||
const packageRecord = this.validatePackage(contribution)
|
||||
const schemaRecords = this.validateSchemas(contribution)
|
||||
const { schemas, packages } = this
|
||||
const dispose = this.ctx.effect(function* () {
|
||||
packages.set(packageRecord.key, packageRecord)
|
||||
for (const record of schemaRecords) schemas.set(record.key, record)
|
||||
yield () => {
|
||||
packages.delete(packageRecord.key)
|
||||
for (const record of schemaRecords) schemas.delete(record.key)
|
||||
}
|
||||
}, 'typert.register()')
|
||||
// oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; preserve Cordis disposer identity
|
||||
return dispose
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up one schema by `<package>#<name>`.
|
||||
* @param key - global schema key.
|
||||
* @returns the live schema record, or `undefined` when absent.
|
||||
*/
|
||||
get(key: string): TypertSchemaRecord | undefined {
|
||||
return this.schemas.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one required schema.
|
||||
* @param key - global schema key.
|
||||
* @returns the live schema record.
|
||||
* @throws when the key is malformed, the package face is absent, or the schema is not contributed.
|
||||
*/
|
||||
resolve(key: string): TypertSchemaRecord {
|
||||
const record = this.schemas.get(key)
|
||||
if (record !== undefined) return record
|
||||
const hash = key.indexOf('#')
|
||||
if (hash <= 0 || hash === key.length - 1) {
|
||||
throw new Error(`typert: invalid schema key "${key}" — expected "<package>#<name>"`)
|
||||
}
|
||||
const packageName = key.slice(0, hash)
|
||||
if ([...this.packages.values()].some(candidate => candidate.package === packageName)) {
|
||||
throw new Error(
|
||||
`typert: cannot resolve "${key}" — package "${packageName}" is registered but contributes no schema named "${key.slice(hash + 1)}"`,
|
||||
)
|
||||
}
|
||||
throw new Error(`typert: cannot resolve "${key}" — package "${packageName}" has no registered contribution`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate live schemas in registration order.
|
||||
* @param filter - optional package and face restriction.
|
||||
* @returns matching schema records.
|
||||
*/
|
||||
list(filter: TypertSchemaFilter = {}): TypertSchemaRecord[] {
|
||||
return [...this.schemas.values()].filter(record => matches(record, filter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up generated reflection for one package face.
|
||||
* @param packageName - exact npm package name.
|
||||
* @param face - face to query; defaults to the host runtime.
|
||||
* @returns the live package record, or `undefined` when absent.
|
||||
*/
|
||||
getPackage(packageName: string, face: TypertFace = 'host'): TypertPackageRecord | undefined {
|
||||
return this.packages.get(typertPackageKey(packageName, face))
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate generated package reflection in registration order.
|
||||
* @param filter - optional package and face restriction.
|
||||
* @returns matching package records.
|
||||
*/
|
||||
listPackages(filter: TypertPackageFilter = {}): TypertPackageRecord[] {
|
||||
return [...this.packages.values()].filter(record => matches(record, filter))
|
||||
}
|
||||
|
||||
/**
|
||||
* Project a live Zod schema to JSON Schema without caching the result.
|
||||
* @param key - global schema key.
|
||||
* @param params - Zod projection parameters.
|
||||
* @returns a fresh JSON Schema document.
|
||||
*/
|
||||
toJSONSchema(key: string, params?: z.core.ToJSONSchemaParams): z.core.JSONSchema.BaseSchema {
|
||||
return z.toJSONSchema(this.resolve(key).schema, params)
|
||||
}
|
||||
|
||||
private validatePackage(contribution: TypertContribution): TypertPackageRecord {
|
||||
validateSegment('package name', contribution.package)
|
||||
const face: unknown = contribution.face
|
||||
if (face !== 'host' && face !== 'client') {
|
||||
throw new Error(`typert: invalid face ${JSON.stringify(face)} — expected "host" or "client"`)
|
||||
}
|
||||
const key = typertPackageKey(contribution.package, contribution.face)
|
||||
if (this.packages.has(key)) {
|
||||
throw new Error(`typert: package face "${key}" is already registered`)
|
||||
}
|
||||
return {
|
||||
package: contribution.package,
|
||||
face,
|
||||
key,
|
||||
model: contribution.model,
|
||||
}
|
||||
}
|
||||
|
||||
private validateSchemas(contribution: TypertContribution): TypertSchemaRecord[] {
|
||||
const records: TypertSchemaRecord[] = []
|
||||
const batch = new Set<string>()
|
||||
for (const schema of contribution.schemas) {
|
||||
validateSegment('schema name', schema.name)
|
||||
const key = typertKey(contribution.package, schema.name)
|
||||
if (batch.has(key) || this.schemas.has(key)) {
|
||||
throw new Error(`typert: schema "${key}" is already registered`)
|
||||
}
|
||||
batch.add(key)
|
||||
records.push({
|
||||
...schema,
|
||||
package: contribution.package,
|
||||
face: contribution.face,
|
||||
key,
|
||||
})
|
||||
}
|
||||
return records
|
||||
}
|
||||
}
|
||||
|
||||
function matches(
|
||||
record: { readonly package: string; readonly face: TypertFace },
|
||||
filter: { readonly package?: string; readonly face?: TypertFace },
|
||||
): boolean {
|
||||
return (filter.package === undefined || record.package === filter.package)
|
||||
&& (filter.face === undefined || record.face === filter.face)
|
||||
}
|
||||
|
||||
function validateSegment(subject: string, value: string): void {
|
||||
if (value.length === 0 || value.includes('#')) {
|
||||
throw new Error(`typert: invalid ${subject} "${value}" — must be nonempty and must not contain "#"`)
|
||||
}
|
||||
}
|
||||
|
||||
export default TypertRegistry
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-typert-registry`.
|
||||
* @module @deepseek-ai/dsh-typert-registry/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-typert-registry'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'typert-registry-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: schema and package-reflection records mutate together
|
||||
* inside register/dispose, with no independent event or second data source to
|
||||
* cross-check; duplicate identities fail at the owning operation boundary.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Pure generated-artifact and runtime-registry types. The registry stores Zod
|
||||
* schemas separately from generated package reflection metadata.
|
||||
* @module @deepseek-ai/dsh-typert-registry/types
|
||||
*/
|
||||
|
||||
import type { z } from 'zod'
|
||||
|
||||
/** Independently compiled side that produced a contribution. */
|
||||
export type TypertFace = 'host' | 'client'
|
||||
|
||||
/** Structured JSDoc tag retained by generated runtime metadata. */
|
||||
export interface TypertDocTag {
|
||||
readonly name: string
|
||||
readonly argument?: string
|
||||
readonly comment?: string
|
||||
readonly text: string
|
||||
}
|
||||
|
||||
/** Source documentation retained on reflected package elements. */
|
||||
export interface TypertDocumentation {
|
||||
readonly description?: string
|
||||
readonly summary?: string
|
||||
readonly tags: readonly TypertDocTag[]
|
||||
readonly jsDoc?: string
|
||||
}
|
||||
|
||||
/** One generated public member signature. */
|
||||
export interface TypertMemberModel {
|
||||
readonly kind: 'property' | 'method' | 'getter' | 'setter' | 'call' | 'construct' | 'index'
|
||||
readonly name: string
|
||||
readonly signature: string
|
||||
readonly summary?: string
|
||||
readonly jsDoc?: string
|
||||
}
|
||||
|
||||
/** One named type declaration referenced by a reflected business surface. */
|
||||
export interface TypertTypeModel {
|
||||
readonly name: string
|
||||
readonly declaration: string
|
||||
}
|
||||
|
||||
/** Runtime reflection metadata for one Cordis service. */
|
||||
export interface TypertServiceModel extends TypertDocumentation {
|
||||
readonly key: string
|
||||
readonly exportName: string
|
||||
readonly members: readonly TypertMemberModel[]
|
||||
readonly types: readonly TypertTypeModel[]
|
||||
}
|
||||
|
||||
/** Runtime reflection metadata for one Cordis event. */
|
||||
export interface TypertEventModel extends TypertDocumentation {
|
||||
readonly name: string
|
||||
readonly mode?: string
|
||||
readonly signature: string
|
||||
}
|
||||
|
||||
/** Runtime reflection metadata for one explicitly exported reference object. */
|
||||
export interface TypertObjectModel extends TypertDocumentation {
|
||||
readonly name: string
|
||||
readonly exportName: string
|
||||
readonly members: readonly TypertMemberModel[]
|
||||
readonly types: readonly TypertTypeModel[]
|
||||
}
|
||||
|
||||
/** Generated business reflection for one package on one face. */
|
||||
export interface TypertPackageModel {
|
||||
readonly services: readonly TypertServiceModel[]
|
||||
readonly events: readonly TypertEventModel[]
|
||||
readonly objects: readonly TypertObjectModel[]
|
||||
}
|
||||
|
||||
/** One generated live Zod schema. */
|
||||
export interface TypertSchema {
|
||||
readonly name: string
|
||||
readonly schema: z.ZodType
|
||||
}
|
||||
|
||||
/** One generated package contribution registered and withdrawn atomically. */
|
||||
export interface TypertContribution {
|
||||
readonly package: string
|
||||
readonly face: TypertFace
|
||||
readonly schemas: readonly TypertSchema[]
|
||||
readonly model: TypertPackageModel
|
||||
}
|
||||
|
||||
/** A live schema plus its contribution identity. */
|
||||
export interface TypertSchemaRecord extends TypertSchema {
|
||||
readonly package: string
|
||||
readonly face: TypertFace
|
||||
readonly key: string
|
||||
}
|
||||
|
||||
/** A live generated package model plus its stable identity. */
|
||||
export interface TypertPackageRecord {
|
||||
readonly package: string
|
||||
readonly face: TypertFace
|
||||
readonly key: string
|
||||
readonly model: TypertPackageModel
|
||||
}
|
||||
|
||||
/** Filter for schema enumeration. */
|
||||
export interface TypertSchemaFilter {
|
||||
readonly package?: string
|
||||
readonly face?: TypertFace
|
||||
}
|
||||
|
||||
/** Filter for package-model enumeration. */
|
||||
export interface TypertPackageFilter {
|
||||
readonly package?: string
|
||||
readonly face?: TypertFace
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { z } from 'zod'
|
||||
import TypertRegistry, {
|
||||
typertKey,
|
||||
typertPackageKey,
|
||||
type TypertContribution,
|
||||
} from '@deepseek-ai/dsh-typert-registry'
|
||||
|
||||
async function makeCtx(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(TypertRegistry)
|
||||
return ctx
|
||||
}
|
||||
|
||||
function toolsContribution(schema: z.ZodType = z.object({ name: z.string() })): TypertContribution {
|
||||
return {
|
||||
package: '@deepseek-ai/dsh-tools',
|
||||
face: 'host',
|
||||
schemas: [{ name: 'ToolInput', schema }],
|
||||
model: {
|
||||
services: [{
|
||||
key: 'tools',
|
||||
exportName: 'ToolRegistry',
|
||||
summary: 'Tool registry and execution pipeline.',
|
||||
tags: [],
|
||||
members: [{
|
||||
kind: 'method',
|
||||
name: 'register',
|
||||
signature: 'register(definition: ToolDefinition): () => void',
|
||||
}],
|
||||
types: [{ name: 'ToolDefinition', declaration: 'export interface ToolDefinition {}' }],
|
||||
}],
|
||||
events: [{
|
||||
name: 'tools/change',
|
||||
mode: 'emit',
|
||||
signature: "'tools/change'(): void",
|
||||
tags: [],
|
||||
}],
|
||||
objects: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('TypertRegistry', () => {
|
||||
it('registers and queries generated schemas separately from package reflection', async () => {
|
||||
const ctx = await makeCtx()
|
||||
const contribution = toolsContribution()
|
||||
ctx.typert.register(contribution)
|
||||
|
||||
expect(typertKey('@deepseek-ai/dsh-tools', 'ToolInput')).toBe('@deepseek-ai/dsh-tools#ToolInput')
|
||||
expect(typertPackageKey('@deepseek-ai/dsh-tools', 'host')).toBe('@deepseek-ai/dsh-tools#host')
|
||||
expect(ctx.typert.get('@deepseek-ai/dsh-tools#ToolInput')).toMatchObject({
|
||||
package: '@deepseek-ai/dsh-tools',
|
||||
face: 'host',
|
||||
name: 'ToolInput',
|
||||
})
|
||||
expect(ctx.typert.get('@deepseek-ai/dsh-tools#ToolInput')?.schema).toBe(contribution.schemas[0]?.schema)
|
||||
expect(ctx.typert.getPackage('@deepseek-ai/dsh-tools', 'host')).toMatchObject({
|
||||
key: '@deepseek-ai/dsh-tools#host',
|
||||
model: { services: [{ key: 'tools' }] },
|
||||
})
|
||||
expect(ctx.typert.list()).toHaveLength(1)
|
||||
expect(ctx.typert.listPackages({ face: 'host' })).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('withdraws schemas and package metadata through the exact contribution disposer', async () => {
|
||||
const ctx = await makeCtx()
|
||||
const dispose = ctx.typert.register(toolsContribution())
|
||||
expect(ctx.typert.getPackage('@deepseek-ai/dsh-tools')).toBeDefined()
|
||||
|
||||
dispose()
|
||||
|
||||
expect(ctx.typert.get('@deepseek-ai/dsh-tools#ToolInput')).toBeUndefined()
|
||||
expect(ctx.typert.getPackage('@deepseek-ai/dsh-tools')).toBeUndefined()
|
||||
expect(ctx.typert.listPackages()).toEqual([])
|
||||
})
|
||||
|
||||
it('follows the registering plugin fiber lifecycle', async () => {
|
||||
const ctx = await makeCtx()
|
||||
const fiber = ctx.plugin(Object.assign(
|
||||
(child: Context) => { child.typert.register(toolsContribution()) },
|
||||
{ inject: ['typert'] },
|
||||
))
|
||||
await fiber
|
||||
expect(ctx.typert.getPackage('@deepseek-ai/dsh-tools')).toBeDefined()
|
||||
|
||||
await fiber.dispose()
|
||||
|
||||
expect(ctx.typert.getPackage('@deepseek-ai/dsh-tools')).toBeUndefined()
|
||||
expect(ctx.typert.list()).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects duplicate package faces and schema keys before committing', async () => {
|
||||
const ctx = await makeCtx()
|
||||
const original = toolsContribution()
|
||||
ctx.typert.register(original)
|
||||
|
||||
expect(() => ctx.typert.register(toolsContribution(z.never()))).toThrow('package face')
|
||||
expect(ctx.typert.get('@deepseek-ai/dsh-tools#ToolInput')?.schema).toBe(original.schemas[0]?.schema)
|
||||
|
||||
const duplicateBatch: TypertContribution = {
|
||||
...toolsContribution(),
|
||||
package: '@fixture/duplicate',
|
||||
schemas: [
|
||||
{ name: 'Same', schema: z.string() },
|
||||
{ name: 'Same', schema: z.number() },
|
||||
],
|
||||
}
|
||||
expect(() => ctx.typert.register(duplicateBatch)).toThrow('schema "@fixture/duplicate#Same" is already registered')
|
||||
expect(ctx.typert.getPackage('@fixture/duplicate')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects malformed contribution identities and filters both registry views', async () => {
|
||||
const ctx = await makeCtx()
|
||||
ctx.typert.register(toolsContribution())
|
||||
|
||||
expect(() => ctx.typert.register({ ...toolsContribution(), package: '' }))
|
||||
.toThrow('invalid package name')
|
||||
expect(() => ctx.typert.register({ ...toolsContribution(), package: 'bad#package' }))
|
||||
.toThrow('invalid package name')
|
||||
expect(() => ctx.typert.register({ ...toolsContribution(), face: 'worker' as 'host' }))
|
||||
.toThrow('invalid face')
|
||||
expect(() => ctx.typert.register({
|
||||
...toolsContribution(),
|
||||
package: '@fixture/schema-name',
|
||||
schemas: [{ name: 'bad#name', schema: z.string() }],
|
||||
})).toThrow('invalid schema name')
|
||||
|
||||
expect(ctx.typert.list({ package: '@fixture/absent' })).toEqual([])
|
||||
expect(ctx.typert.list({ face: 'client' })).toEqual([])
|
||||
expect(ctx.typert.listPackages({ package: '@fixture/absent' })).toEqual([])
|
||||
expect(ctx.typert.listPackages({ face: 'client' })).toEqual([])
|
||||
})
|
||||
|
||||
it('resolves required schemas and projects fresh JSON Schema documents', async () => {
|
||||
const ctx = await makeCtx()
|
||||
ctx.typert.register(toolsContribution())
|
||||
|
||||
expect(ctx.typert.resolve('@deepseek-ai/dsh-tools#ToolInput').name).toBe('ToolInput')
|
||||
expect(() => ctx.typert.resolve('@deepseek-ai/dsh-tools#Missing')).toThrow('contributes no schema named "Missing"')
|
||||
expect(() => ctx.typert.resolve('@fixture/absent#Value')).toThrow('has no registered contribution')
|
||||
expect(() => ctx.typert.resolve('invalid')).toThrow('expected "<package>#<name>"')
|
||||
const projected = ctx.typert.toJSONSchema('@deepseek-ai/dsh-tools#ToolInput')
|
||||
expect(projected).toMatchObject({ type: 'object', properties: { name: { type: 'string' } } })
|
||||
expect(ctx.typert.toJSONSchema('@deepseek-ai/dsh-tools#ToolInput')).not.toBe(projected)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../vendor/cosmokit"
|
||||
},
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'tsdown'
|
||||
|
||||
/** Build the registry and its invariant companion as independent bundles. */
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: ['lib/types/index.js'],
|
||||
outDir: 'lib',
|
||||
format: ['esm'],
|
||||
platform: 'node',
|
||||
target: 'es2024',
|
||||
fixedExtension: false,
|
||||
dts: false,
|
||||
clean: false,
|
||||
},
|
||||
{
|
||||
entry: ['lib/types/invariant.js'],
|
||||
outDir: 'lib',
|
||||
format: ['esm'],
|
||||
platform: 'node',
|
||||
target: 'es2024',
|
||||
fixedExtension: false,
|
||||
dts: false,
|
||||
clean: false,
|
||||
},
|
||||
])
|
||||
Reference in New Issue
Block a user