refactor: apply repository naming contract

Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
Tianyi Cui
2026-08-13 00:36:22 +08:00
parent 101df7cf58
commit a2d0f7f411
3281 changed files with 21730 additions and 21592 deletions

View File

@@ -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/protocol/README.md
README.md: 84f9f1b31cca35cffce5dc5fc84db1017adaad08
README.zh.md: 97e4734a7292f0a305ff8ed00ff1d72d1a53b5e9

View File

@@ -0,0 +1,38 @@
# @deepseek-ai/dsh-typert-protocol
English | [中文](README.zh.md)
Compiler-independent declarations shared by business packages, generated Typert artifacts, the Host Gateway, and Client API. This package owns the Remote Service base, decorators, explicit binding fallback, merge-extensible protocol maps, invocation descriptors, codecs, and provider contracts; it does not run TypeScript analysis or register a concrete Cordis service.
## Remote declarations
- `@Remote` marks a public instance method for direct invocation on its registered Cordis Service.
- `@RemoteScope(key)` marks a method whose receiver is selected from a merge-declared scoped Context kind.
- `TypertRemoteService` binds the Cordis key passed to `super(ctx, serviceKey, options?)` to the same default wire namespace.
- `bindTypertRemote(this, serviceKey, options?)` provides the same visible, frozen binding for a Service that cannot inherit from `TypertRemoteService`.
- `remoteMethods(service)` returns a detached declaration-order snapshot used by the Gateway's SRC fallback.
A Host method opts into cooperative cancellation by declaring `signal: AbortSignal` as its final parameter. `InvocationDescriptor.cancellation` records that reserved injection point; the signal never becomes a JSON parameter or lookup field. SRC recognizes the final parameter name, while strict generation also verifies the global `AbortSignal` type.
Decorator initializers retain markers in a module-private `WeakMap` keyed by the Service prototype. They do not add constructor symbols, prototype properties, parameter metadata, or runtime reflection fields. A `TypertRemoteService` exposes the same public readonly `typertRemote` binding that the explicit helper returns.
## Typert protocol
Business packages extend `TypertLookupMap` and `TypertContextMap` to associate Host objects or scoped Contexts with their wire identities. Generated artifacts extend `TypertRemoteMap`, `TypertRemoteScopeMap`, and `TypertRemoteNamespaceMap` so Client imports expose only selected Remote methods. `InvocationDescriptor` is the shared runtime form consumed by the registry, Gateway, and Client Remote.
The Host assembly extends `TypertRemoteEventSelection` with the Host events it forwards to consumers, which narrows the `ctx.remote.$on` key face; `TypertForwardableEvent` states the shapes a one-way delivery can carry at all, excluding Scope-bound and answered events. `TypertClientRemote` carries both roles of that surface: consumers subscribe through `$on`, and the Client half owning the host frame sink hands frames over through `$dispatch`.
Lookup and Context packages own both sides of their contract: declaration merging supplies the static association, while runtime providers register identity resolution with `ctx.typert`. A lookup or Host Context provider supplies the stable declaration and default resolver, while Host composition may separately configure a synchronous or asynchronous resolver; policy rejections may use `TypertLookupFailure` to carry a failure value owned by the boundary adapter. Strict codecs carry generated schemas; `src-json` codecs identify the weaker source-launch path.
## Model Experience
None, as this protocol package declares application reflection and registers nothing model-facing.
#### KV Cache effect
No direct effect.
## Known Limitations and Deferred Work
- Decorator markers contain only the method name and direct or Context invocation mode. Parameter, result, lookup, and schema reflection require the Typert build pipeline.
- Remote decorators accept only public, non-static instance methods with string names. SRC execution cannot represent overloaded, destructured, defaulted, or rest-parameter signatures.

View File

@@ -0,0 +1,38 @@
# @deepseek-ai/dsh-typert-protocol
[English](README.md) | 中文
该包提供不依赖编译器的声明,由业务包、生成的 Typert 产物、宿主网关和客户端 API 共享。它负责 Remote 服务基类、装饰器、显式绑定回退、可通过声明合并扩展的协议映射、调用描述符、编解码器和提供方约定;它不执行 TypeScript 分析,也不注册具体 Cordis 服务。
## Remote 声明
- `@Remote` 将公开实例方法标记为可在其注册的 Cordis 服务上直接调用。
- `@RemoteScope(key)` 标记接收者选自合并声明的作用域 Context 类型的方法。
- `TypertRemoteService` 将传给 `super(ctx, serviceKey, options?)` 的 Cordis 键绑定到同一默认协议命名空间。
- `bindTypertRemote(this, serviceKey, options?)` 为无法继承 `TypertRemoteService` 的服务提供同样可见且冻结的绑定。
- `remoteMethods(service)` 返回按声明顺序排列、与内部状态分离的快照,供 Gateway 的 SRC 回退路径使用。
宿主方法通过将 `signal: AbortSignal` 声明为最后一个参数来启用协作式取消。`InvocationDescriptor.cancellation` 记录这个保留的注入点;该信号绝不会成为 JSON 参数或查找字段。SRC 识别末位参数名,严格生成还会校验它是否具有全局 `AbortSignal` 类型。
装饰器初始化器将标记保存在以服务原型为键的模块私有 `WeakMap` 中。它们不会在构造函数上添加符号,也不会添加原型属性、参数元数据或运行时反射字段。`TypertRemoteService` 会暴露与显式辅助函数相同的公开只读 `typertRemote` 绑定。
## Typert 协议
业务包扩展 `TypertLookupMap``TypertContextMap`,以关联宿主对象或作用域 Context 与其协议身份。生成的产物扩展 `TypertRemoteMap``TypertRemoteScopeMap``TypertRemoteNamespaceMap`,使客户端导入后仅暴露选定的 Remote 方法。`InvocationDescriptor` 是供注册表、网关和客户端 Remote 使用的共享运行时形式。
Host 装配以转发给消费端的 Host 事件扩展 `TypertRemoteEventSelection`,从而收窄 `ctx.remote.$on` 的键面;`TypertForwardableEvent` 陈述单向投递根本能承载哪些形状,把 Scope 化事件与有返回值的事件排除在外。`TypertClientRemote` 承载该面的两种角色:消费方经 `$on` 订阅,持有 Host 帧 sink 的 Client 半经 `$dispatch` 交出帧。
查找包与 Context 包同时负责该约定的两侧:声明合并提供静态关联,运行时提供方则向 `ctx.typert` 注册身份解析。查找提供方或宿主 Context 提供方提供稳定声明与默认解析器,宿主组合可以另行配置同步或异步解析器;策略拒绝可用 `TypertLookupFailure` 携带由边界适配器拥有的失败值。严格编解码器携带生成的 schema`src-json` 编解码器标识约束更弱的源码启动路径。
## 模型体验
无,因为该协议包声明应用反射,不注册任何面向模型的内容。
#### KV Cache 影响
无直接影响。
## 已知限制与暂缓事项
- 装饰器标记仅包含方法名,以及直接调用或 Context 调用模式。参数、结果、查找和 schema 反射需要 Typert 构建流水线。
- Remote 装饰器只接受具有字符串名称的公开、非静态实例方法。SRC 执行无法表示重载签名,以及包含解构参数、默认参数或剩余参数的方法签名。

View File

@@ -0,0 +1,47 @@
{
"name": "@deepseek-ai/dsh-typert-protocol",
"description": "Compiler-independent Remote metadata and Typert provider protocols",
"version": "0.0.1-rc.2",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/typert/protocol"
},
"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"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -0,0 +1,280 @@
/**
* Remote decorators and explicit Gateway bindings backed only by private
* module state. Strict reflection remains a Typert compiler responsibility.
* @module @deepseek-ai/dsh-typert-protocol
*/
import { Service, type Context } from '@deepseek-ai/cordis'
import type { TypertContextMap } from './types.ts'
const TYPERT_REMOTE_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/
/**
* Test one generated Remote name against the Connection endpoint grammar.
* @param value - namespace, method, lookup, or Context segment.
* @returns whether the value can cross the shared RPC carrier unchanged.
*/
export function isTypertRemoteSegment(value: string): boolean {
return value !== '.' && value !== '..' && TYPERT_REMOTE_SEGMENT_PATTERN.test(value)
}
/**
* A lookup policy rejection whose typed payload belongs to the active boundary adapter.
* Gateway adapters preserve this payload instead of collapsing it into an infrastructure failure.
*/
export class TypertLookupFailure<Failure = unknown> extends Error {
/** Adapter-owned failure returned to the caller. */
readonly failure: Failure
/**
* Wrap one adapter failure without exposing the rejected identity.
* @param failure - typed failure owned by the active boundary adapter.
*/
constructor(failure: Failure) {
super('Typert lookup policy rejected the requested identity')
this.name = 'TypertLookupFailure'
this.failure = failure
}
}
export type {
InvocationDescriptor,
InvocationParameterDescriptor,
InvocationSourceLocation,
RemoteFailure,
RemoteResult,
TypertClientRemote,
TypertClientContextBinder,
TypertCodec,
TypertContext,
TypertContextMap,
TypertContextRegistry,
TypertContextWire,
TypertDisposer,
TypertForwardableEvent,
TypertHostContextProvider,
TypertHostContextResolver,
TypertLocalRegistry,
TypertLookup,
TypertLookupDefinition,
TypertLookupHost,
TypertLookupMap,
TypertLookupProvider,
TypertLookupResolver,
TypertLookupRegistry,
TypertLookupWire,
TypertRemoteScopeApi,
TypertRemoteScopeMap,
TypertRemoteScopeNamespace,
TypertRemoteContribution,
TypertRemoteEvent,
TypertRemoteEventSelection,
TypertRemoteMap,
TypertRemoteNamespace,
TypertRemoteNamespaceMap,
TypertRemoteRegistry,
TypertRegistryChange,
TypertRegistryListener,
TypertSchema,
TypertRegistryContract,
} from './types.ts'
/** Options for an explicit Service-to-Gateway binding. */
export interface TypertGatewayBindingOptions {
/** Wire namespace; defaults to the Cordis service key. */
readonly namespace?: string
}
/** Visible declaration that one Service participates in Typert Gateway export. */
export interface TypertGatewayBinding<Service extends object = object> {
readonly service: Service
readonly serviceKey: string
readonly namespace: string
}
/** Invocation mode recorded by a Remote method decorator. */
export type RemoteInvocationMarker =
| { readonly kind: 'direct' }
| { readonly kind: 'context'; readonly context: string }
/** One decorator marker discovered for a live Service instance. */
export interface RemoteMethodMarker {
/** Public instance method carrying the implementation. */
readonly method: string
/** Endpoint method when it differs from the implementation member. */
readonly exportName?: string
readonly invocation: RemoteInvocationMarker
}
type RemoteMethodDecorator = <This extends object, Args extends unknown[], Result>(
method: (this: This, ...args: Args) => Result,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Result>,
) => void
interface RemoteInitializerContext<This extends object> {
readonly private: boolean
readonly static: boolean
readonly name: string | symbol
addInitializer(initializer: (this: This) => void): void
}
interface StoredRemoteMethodMarker {
readonly exportName?: string
readonly invocation: RemoteInvocationMarker
}
const markers = new WeakMap<object, Map<string, StoredRemoteMethodMarker>>()
/**
* Bind one visible Service field to a Cordis key and Remote namespace.
* @param service - owning Service instance, normally `this`.
* @param serviceKey - exact Cordis service key.
* @param options - optional distinct wire namespace.
* @returns a frozen, inspectable binding with no compiler-injected metadata.
*/
export function bindTypertRemote<Service extends object>(
service: Service,
serviceKey: string,
options: TypertGatewayBindingOptions = {},
): TypertGatewayBinding<Service> {
validateName('service key', serviceKey)
const namespace = options.namespace ?? serviceKey
validateName('namespace', namespace)
return Object.freeze({ service, serviceKey, namespace })
}
/** Cordis Service base that exposes its registered name through Typert Gateway. */
export abstract class TypertRemoteService<out T = never> extends Service<T> {
/** Visible binding consumed by the Gateway's source-mode discovery. */
readonly typertRemote: TypertGatewayBinding<this>
/**
* Register the Service and bind the same key to Typert Gateway.
* @param ctx - owning Cordis Context.
* @param serviceKey - exact Cordis service key and default wire namespace.
* @param options - optional distinct wire namespace.
*/
protected constructor(ctx: Context, serviceKey: string, options: TypertGatewayBindingOptions = {}) {
super(ctx, serviceKey)
this.typertRemote = bindTypertRemote(this, this.name, options)
}
}
/**
* Mark one public instance method as a direct Remote invocation.
* @param _method - decorated method; retained only by the class itself.
* @param context - standard decorator context used to schedule private marking.
*/
export function Remote<This extends object, Args extends unknown[], Result>(
_method: (this: This, ...args: Args) => Result,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Result>,
): void
/**
* Mark one public instance method under a distinct exported method name.
* @param exportName - Remote endpoint method, without a namespace or slash.
* @returns a standard method decorator.
*/
export function Remote(exportName: string): RemoteMethodDecorator
export function Remote<This extends object, Args extends unknown[], Result>(
methodOrExportName: string | ((this: This, ...args: Args) => Result),
context?: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Result>,
): void | RemoteMethodDecorator {
if (typeof methodOrExportName === 'string') {
validateName('Remote export name', methodOrExportName)
return function <DecoratorThis extends object, DecoratorArgs extends unknown[], DecoratorResult>(
_method: (this: DecoratorThis, ...args: DecoratorArgs) => DecoratorResult,
decoratorContext: ClassMethodDecoratorContext<
DecoratorThis,
(this: DecoratorThis, ...args: DecoratorArgs) => DecoratorResult
>,
): void {
addMarkerInitializer(decoratorContext, { kind: 'direct' }, methodOrExportName)
}
}
if (context === undefined) throw new TypeError('typert-protocol: Remote decorator context is missing')
addMarkerInitializer(context, { kind: 'direct' })
}
/**
* Create a decorator for a method resolved from one Remote Scope.
* @param key - scope key declared through the Context map.
* @param exportName - optional Remote export name; defaults to the method name.
* @returns a standard method decorator that records only private module state.
*/
export function RemoteScope(
key: Extract<keyof TypertContextMap, string>,
exportName?: string,
): RemoteMethodDecorator {
validateName('Scope key', key)
if (exportName !== undefined) validateName('Remote export name', exportName)
return function <This extends object, Args extends unknown[], Result>(
_method: (this: This, ...args: Args) => Result,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Result>,
): void {
addMarkerInitializer(context, { kind: 'context', context: key }, exportName)
}
}
/**
* Read Remote markers attached to a live Service by decorator initializers.
* The returned snapshot cannot mutate the private marker table.
* @param service - live Service instance.
* @returns markers in class declaration order.
*/
export function remoteMethods(service: object): readonly RemoteMethodMarker[] {
const prototype = Object.getPrototypeOf(service) as object | null
if (prototype === null) return []
return [...(markers.get(prototype) ?? [])].map(([method, marker]) => ({ method, ...marker }))
}
function addMarkerInitializer<This extends object>(
context: RemoteInitializerContext<This>,
invocation: RemoteInvocationMarker,
exportName?: string,
): void {
if (context.private || context.static || typeof context.name !== 'string') {
throw new TypeError('typert-protocol: Remote decorators require a public instance method with a string name')
}
const method = context.name
context.addInitializer(function (this: This) {
const prototype = Object.getPrototypeOf(this) as object | null
if (prototype === null) {
throw new TypeError(`typert-protocol: cannot mark Remote method "${method}" on an object without a prototype`)
}
mark(prototype, method, invocation, exportName)
})
}
function mark(
prototype: object,
method: string,
invocation: RemoteInvocationMarker,
exportName?: string,
): void {
let table = markers.get(prototype)
if (table === undefined) {
table = new Map()
markers.set(prototype, table)
}
const marker: StoredRemoteMethodMarker = {
...(exportName === undefined || exportName === method ? {} : { exportName }),
invocation: Object.freeze(invocation),
}
const current = table.get(method)
if (current !== undefined) {
if (current.exportName === marker.exportName && sameInvocation(current.invocation, invocation)) return
throw new Error(`typert-protocol: Remote method "${method}" has conflicting invocation markers`)
}
table.set(method, Object.freeze(marker))
}
function sameInvocation(left: RemoteInvocationMarker, right: RemoteInvocationMarker): boolean {
return left.kind === right.kind
&& (left.kind === 'direct' || (right.kind === 'context' && left.context === right.context))
}
function validateName(subject: string, value: string): void {
if (!isTypertRemoteSegment(value)) {
throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`)
}
}

View File

@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-typert-protocol`.
* @module @deepseek-ai/dsh-typert-protocol/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-typert-protocol'
/** Cordis companion plugin name. */
export const name = 'typert-protocol-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: decorators retain private immutable declarations and
* bindings are frozen values with no independent event stream to cross-check.
*/
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 */

View File

@@ -0,0 +1,491 @@
/**
* Compiler-independent Typert protocol shared by business packages, generated
* Remote artifacts, the Host Gateway, and Client API implementations.
* @module @deepseek-ai/dsh-typert-protocol/types
*/
import type { Context, Events } from '@deepseek-ai/cordis'
declare const LOOKUP_HOST: unique symbol
declare const LOOKUP_WIRE: unique symbol
declare const CONTEXT_WIRE: unique symbol
/** Type-level association between a Host object and its wire identity. */
export interface TypertLookup<Host, Wire> {
readonly [LOOKUP_HOST]: Host
readonly [LOOKUP_WIRE]: Wire
}
/** Extract the Host object associated with one lookup declaration. */
export type TypertLookupHost<Lookup> = Lookup extends TypertLookup<infer Host, infer _Wire> ? Host : never
/** Extract the wire identity associated with one lookup declaration. */
export type TypertLookupWire<Lookup> = Lookup extends TypertLookup<infer _Host, infer Wire> ? Wire : never
/** Type-level association between a scoped Context kind and its wire identity. */
export interface TypertContext<Wire> {
readonly [CONTEXT_WIRE]: Wire
}
/** Extract the wire identity associated with one scoped Context declaration. */
export type TypertContextWire<ContextType> = ContextType extends TypertContext<infer Wire> ? Wire : never
/** Merge-extensible Host object lookup declarations. */
export interface TypertLookupMap {}
/** Merge-extensible scoped Context declarations. */
export interface TypertContextMap {}
/** Merge-extensible direct Remote method signatures generated for consumers. */
export interface TypertRemoteMap {}
/**
* One Remote call's failure as the carrier reported it. `code` stays open here:
* the closed RPC code union belongs to the carrier package, which already
* depends on this one, so naming it would invert that edge.
*/
export interface RemoteFailure {
readonly code: string
readonly message: string
readonly details: object
}
/**
* What every generated Remote method resolves to. The Remote face itself folds
* carrier failures into the error branch, so no consumer wraps a call to
* recover one; only assembly faults (arity, an unmounted method, a missing
* Context binder) still reject.
* @template T - the Host method's business result.
*/
export type RemoteResult<T> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: RemoteFailure }
/** Merge-extensible scoped Remote method signatures generated for consumers. */
export interface TypertRemoteScopeMap {}
/**
* Cordis event names whose shape a one-way Remote delivery can carry: unbound
* from any Scope and returning `void`. Which ones are actually forwarded is the
* Host assembly's selection; this predicate only excludes shapes the carrier
* cannot represent.
*/
export type TypertForwardableEvent = {
[Event in keyof Events]: unknown extends ThisParameterType<Events[Event]>
? ReturnType<Events[Event]> extends void ? Event : never
: never
}[keyof Events]
/** Merge-extensible forwarding selection declared once by the Host assembly. */
export interface TypertRemoteEventSelection {}
/** Legal `$on` keys: selected events that exist in the current compilation face. */
export type TypertRemoteEvent = Extract<keyof Events, keyof TypertRemoteEventSelection>
/**
* Resolve one direct Remote namespace from the generated flat endpoint map.
* @template Namespace - wire namespace before the endpoint slash.
*/
export type TypertRemoteNamespace<Namespace extends string> = {
[Endpoint in keyof TypertRemoteMap as Endpoint extends `${Namespace}/${infer Method}`
? Method
: never]: TypertRemoteMap[Endpoint]
}
/**
* Resolve one scoped Remote namespace across every generated Context kind.
* The calling Cordis Context supplies the concrete identity at runtime.
* @template Namespace - wire namespace between the Context prefix and method.
*/
export type TypertRemoteScopeNamespace<
Namespace extends string,
ContextKey extends string = string,
> = {
[Endpoint in keyof TypertRemoteScopeMap as Endpoint extends `${ContextKey}:${Namespace}/${infer Method}`
? Method
: never]: TypertRemoteScopeMap[Endpoint]
}
type TypertRemoteScopeNamespaceKey<
ContextKey extends string,
Endpoint = keyof TypertRemoteScopeMap,
> = Endpoint extends `${ContextKey}:${infer Namespace}/${string}` ? Namespace : never
/** Generated scoped Remote namespaces available to one Context kind. */
export type TypertRemoteScopeApi<ContextKey extends string> = {
[Namespace in TypertRemoteScopeNamespaceKey<ContextKey>]:
TypertRemoteScopeNamespace<Namespace, ContextKey>
}
/** Merge-extensible direct namespace surface generated for Client Remote services. */
export interface TypertRemoteNamespaceMap {}
/** Awaitable disposer returned by Cordis-owned Typert registrations. */
export type TypertDisposer = () => Promise<void>
type StringKeyOf<Value> = Extract<keyof Value, string>
/** Minimal runtime-schema capability carried by strict generated codecs. */
export interface TypertSchema<Output = unknown> {
/**
* Parse and validate one boundary value.
* @param value - untrusted boundary value.
* @returns the validated value.
*/
parse(value: unknown): Output
}
/** Codec attached to one invocation parameter or result. */
export type TypertCodec =
| {
readonly mode: 'strict'
readonly typeSymbol: string
readonly schema: TypertSchema
}
| {
readonly mode: 'src-json'
}
/** One ordered business parameter in a Remote invocation. */
export interface InvocationParameterDescriptor {
/** Source-level parameter name. */
readonly name: string
/** Required key in the wire `args` object. */
readonly wire: string
/** Whether the value is JSON or requires a registered Host lookup. */
readonly source: 'json' | 'lookup'
/** Lookup key when `source` is `lookup`. */
readonly lookup?: string
/** Boundary codec for the wire representation. */
readonly codec: TypertCodec
/** Missing wire fields decode to `undefined` only for an explicitly declared `T | undefined`. */
readonly acceptsUndefined?: true
}
/** Source position retained for diagnostics from generated definitions. */
export interface InvocationSourceLocation {
readonly file: string
readonly line: number
readonly column: number
}
/** Carrier-independent description of one exported method invocation. */
export interface InvocationDescriptor {
/** Globally stable generated identity. */
readonly id: string
/** Cordis service key owning the method. */
readonly service: string
/** Wire namespace, defaulting to the service key. */
readonly namespace: string
/** Public instance method name. */
readonly method: string
/** Service member invoked when the exported method name is an alias. */
readonly implementation?: string
/** Receiver selection mode. */
readonly invocation:
| { readonly kind: 'direct' }
| {
readonly kind: 'context'
readonly context: string
readonly wire: string
readonly codec: TypertCodec
}
/** Optional consuming-Context projection for one direct lookup parameter. */
readonly scope?: {
/** Context kind whose Client binder supplies the identity. */
readonly context: string
/** Lookup parameter wire field replaced by the Context identity. */
readonly wire: string
}
/** Ordered business parameters. */
readonly parameters: readonly InvocationParameterDescriptor[]
/** Transport cancellation injected after business parameters instead of entering wire args. */
readonly cancellation?: {
/** Reserved final Host method parameter. */
readonly parameter: 'signal'
}
/** Codec for the resolved method result. */
readonly result: TypertCodec
/** Source declaration used only for diagnostics. */
readonly sourceLocation?: InvocationSourceLocation
}
/** Generated Host contract selected explicitly by a Client assembly. */
export interface TypertRemoteContribution {
/** npm package that owns the Remote methods. */
readonly package: string
/** Consumer-side invocation descriptors generated from that package. */
readonly descriptors: readonly InvocationDescriptor[]
}
/** Client Remote capability implemented by the Gateway and consumed by Remote assemblies. */
export interface TypertClientRemote extends TypertRemoteNamespaceMap {
/**
* Mount one generated Host-for-Client contribution in the caller's fiber.
* @param contribution - explicitly selected Remote package artifact.
* @returns disposer after namespace services and concrete methods are ready.
*/
$mount(contribution: TypertRemoteContribution): Promise<TypertDisposer>
/**
* Subscribe to one forwarded Host event; delivery is one-way, in registration
* order, and isolates a throwing listener from the rest.
* @template Event - forwarded event name selected by the Host assembly.
* @param event - forwarded Host event name, unchanged on the wire.
* @param listener - receives the Host's argument list as declared by Cordis `Events`.
* @returns disposer owned by the calling fiber.
*/
$on<Event extends TypertRemoteEvent>(event: Event, listener: Events[Event]): () => void
/**
* Hand one decoded forwarded frame to the subscription table. The carrier
* owning the Host frame sink calls this; a consumer subscribes with
* {@link TypertClientRemote.$on} and never calls it.
*
* `event` is a plain string because this is the wire boundary: the name is
* whatever the Host assembly's allowlist selected, and one nobody subscribed
* to is dropped silently.
* @param event - forwarded Host event name, exactly as the Host emitted it.
* @param args - the Host argument list, already JSON-decoded.
*/
$dispatch(event: string, args: readonly unknown[]): void
}
/**
* Resolve one validated wire identity, synchronously or asynchronously.
* @param id - validated wire identity.
* @returns the Host object, or `undefined` when unavailable.
*/
export type TypertLookupResolver<Host = unknown, Wire = unknown> = (
id: Wire,
) => Host | undefined | Promise<Host | undefined>
/** Runtime provider for one declared Host object lookup. */
export interface TypertLookupProvider<Host = unknown, Wire = unknown> {
/** Source parameter name recognized by the SRC weak parser. */
readonly parameter: string
/** Wire field replacing the Host object parameter. */
readonly wire: string
/** Canonical Host type symbol used by strict generation. */
readonly hostTypeSymbol: string
/** Canonical wire type symbol used by strict generation. */
readonly wireTypeSymbol: string
/**
* Resolve a wire identity through the provider's default policy.
* @param id - validated wire identity.
* @returns the object, `undefined` when unavailable, or either asynchronously.
*/
resolve(id: Wire): Host | undefined | Promise<Host | undefined>
}
/** Stable wire declaration retained after a lookup provider unloads. */
export interface TypertLookupDefinition {
/** Merge-declared lookup key. */
readonly key: string
/** Source parameter name recognized by the SRC weak parser. */
readonly parameter: string
/** Wire field replacing the Host object parameter. */
readonly wire: string
/** Canonical Host type symbol used by strict generation. */
readonly hostTypeSymbol: string
/** Canonical wire type symbol used by strict generation. */
readonly wireTypeSymbol: string
}
/** Host resolver for one scoped Remote kind. */
export interface TypertHostContextProvider<Wire = unknown> {
/** Wire field carrying the Context identity. */
readonly wire: string
/** Canonical wire type symbol used by strict generation. */
readonly wireTypeSymbol: string
/**
* Resolve a wire identity to its live scoped Context.
* @param id - validated wire identity.
* @returns the scoped Context, or `undefined` when unavailable.
*/
resolve(id: Wire): Context | undefined | Promise<Context | undefined>
}
/** Composition-owned resolver replacing one Host Context provider's default lookup policy. */
export type TypertHostContextResolver<Wire = unknown> = (
id: Wire,
) => Context | undefined | Promise<Context | undefined>
/** Client resolver for the identity carried by the calling scoped Context. */
export interface TypertClientContextBinder<Wire = unknown> {
/**
* Read the Remote identity represented by a calling Context.
* @param ctx - Context rebound by the Cordis service tracker.
* @returns the wire identity, or `undefined` when the Context has the wrong scope.
*/
identity(ctx: Context): Wire | undefined
}
/** Notification emitted after a Typert runtime registry changes. */
export interface TypertRegistryChange {
readonly kind: 'local' | 'remote' | 'lookup' | 'host-context' | 'client-context'
readonly key: string
}
/** Listener for one Typert runtime registry. */
export type TypertRegistryListener = (change: TypertRegistryChange) => void
/** Current-environment invocation definitions. */
export interface TypertLocalRegistry {
/**
* Look up one invocation by `<namespace>/<method>`.
* @param endpoint - canonical endpoint.
* @returns the live descriptor, or `undefined` when absent.
*/
get(endpoint: string): InvocationDescriptor | undefined
/**
* Report whether a strict definition has existed during this Typert Service lifetime.
* @param endpoint - canonical endpoint.
* @returns `true` after the endpoint has been registered at least once, even if withdrawn.
*/
hasSeen(endpoint: string): boolean
/** @returns a registration-order snapshot of local descriptors. */
list(): readonly InvocationDescriptor[]
/**
* Observe later local-definition changes.
* @param listener - synchronous contained observer.
* @returns disposer for this subscription.
*/
subscribe(listener: TypertRegistryListener): TypertDisposer
}
/** Consumer-selected Remote contribution registry. */
export interface TypertRemoteRegistry {
/**
* Register one generated contribution for the calling Cordis fiber.
* @param contribution - generated Remote descriptors.
* @returns disposer withdrawing the exact contribution.
*/
register(contribution: TypertRemoteContribution): TypertDisposer
/**
* Look up one Remote descriptor by endpoint.
* @param endpoint - canonical endpoint.
* @returns the descriptor, or `undefined` when unmounted.
*/
get(endpoint: string): InvocationDescriptor | undefined
/** @returns a registration-order snapshot of Remote descriptors. */
list(): readonly InvocationDescriptor[]
/**
* Observe later Remote contribution changes.
* @param listener - synchronous contained observer.
* @returns disposer for this subscription.
*/
subscribe(listener: TypertRegistryListener): TypertDisposer
}
/** Runtime registry for Host object lookup providers. */
export interface TypertLookupRegistry {
/**
* Register one provider under its merge-declared key.
* @param key - lookup key.
* @param provider - owning package's live resolver.
* @returns disposer withdrawing the exact provider.
*/
register<K extends StringKeyOf<TypertLookupMap>>(
key: K,
provider: TypertLookupProvider<
TypertLookupHost<TypertLookupMap[K]>,
TypertLookupWire<TypertLookupMap[K]>
>,
): TypertDisposer
/**
* Replace one provider's default resolution policy while this contribution is active.
* Configuration may precede provider registration; without a live provider, `get()` remains unavailable.
* @param key - lookup key whose wire declaration remains provider-owned.
* @param resolver - composition-owned resolver used by every lookup of this key.
* @returns disposer restoring the provider's default resolver.
*/
configure<K extends StringKeyOf<TypertLookupMap>>(
key: K,
resolver: TypertLookupResolver<
TypertLookupHost<TypertLookupMap[K]>,
TypertLookupWire<TypertLookupMap[K]>
>,
): TypertDisposer
/**
* Look up one provider by runtime key.
* @param key - descriptor lookup key.
* @returns the live provider, or `undefined` when absent.
*/
get(key: string): TypertLookupProvider | undefined
/** @returns lookup declarations observed during this Typert Service lifetime. */
definitions(): readonly TypertLookupDefinition[]
/** @returns a snapshot of registered provider keys. */
keys(): readonly string[]
/**
* Observe later lookup changes.
* @param listener - synchronous contained observer.
* @returns disposer for this subscription.
*/
subscribe(listener: TypertRegistryListener): TypertDisposer
}
/** Runtime registry for Host Context resolvers and Client Context binders. */
export interface TypertContextRegistry {
/**
* Register a Host Context resolver.
* @param key - merge-declared Context key.
* @param provider - owning package's Host resolver.
* @returns disposer withdrawing the exact provider.
*/
registerHost<K extends StringKeyOf<TypertContextMap>>(
key: K,
provider: TypertHostContextProvider<TypertContextWire<TypertContextMap[K]>>,
): TypertDisposer
/**
* Override one Host Context key's identity policy for the calling fiber.
* Configuration may precede provider registration and restores the provider's default resolver on disposal.
* @param key - merge-declared Context key.
* @param resolver - composition-owned resolver used by every Host Context lookup of this key.
* @returns disposer restoring the provider's default resolver.
*/
configureHost<K extends StringKeyOf<TypertContextMap>>(
key: K,
resolver: TypertHostContextResolver<TypertContextWire<TypertContextMap[K]>>,
): TypertDisposer
/**
* Register a Client Context identity binder.
* @param key - merge-declared Context key.
* @param binder - Client scope identity resolver.
* @returns disposer withdrawing the exact binder.
*/
registerClient<K extends StringKeyOf<TypertContextMap>>(
key: K,
binder: TypertClientContextBinder<TypertContextWire<TypertContextMap[K]>>,
): TypertDisposer
/**
* Look up a Host Context resolver.
* @param key - descriptor Context key.
* @returns the provider, or `undefined` when absent.
*/
getHost(key: string): TypertHostContextProvider | undefined
/**
* Look up a Client Context binder.
* @param key - descriptor Context key.
* @returns the binder, or `undefined` when absent.
*/
getClient(key: string): TypertClientContextBinder | undefined
/**
* Observe later Context provider changes.
* @param listener - synchronous contained observer.
* @returns disposer for this subscription.
*/
subscribe(listener: TypertRegistryListener): TypertDisposer
}
/** Minimal Typert runtime consumed through dependency inversion. */
export interface TypertRegistryContract {
readonly local: TypertLocalRegistry
readonly remotes: TypertRemoteRegistry
readonly lookups: TypertLookupRegistry
readonly contexts: TypertContextRegistry
}
declare module '@deepseek-ai/cordis' {
interface Context {
typert: TypertRegistryContract
}
}

View File

@@ -0,0 +1,32 @@
import { Context } from '@deepseek-ai/cordis'
import {
TypertRemoteService,
Remote,
RemoteScope,
remoteMethods,
} from '@deepseek-ai/dsh-typert-protocol'
class Goals extends TypertRemoteService {
constructor(ctx: Context) {
super(ctx, 'goals')
}
@Remote
create(value: string): string {
return value
}
@RemoteScope('agent')
scoped(value: string): string {
return value
}
}
const methods = remoteMethods(new Goals(new Context()))
const actual = JSON.stringify(methods)
const expected = JSON.stringify([
{ method: 'create', invocation: { kind: 'direct' } },
{ method: 'scoped', invocation: { kind: 'context', context: 'agent' } },
])
if (actual !== expected) throw new Error(`unexpected Remote declarations: ${actual}`)
process.stdout.write(actual)

View File

@@ -0,0 +1,266 @@
import { execFileSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, expectTypeOf, it } from 'vitest'
import {
bindTypertRemote,
TypertRemoteService,
Remote,
RemoteScope,
remoteMethods,
type TypertContext,
type TypertForwardableEvent,
type TypertRemoteEvent,
} from '@deepseek-ai/dsh-typert-protocol'
declare module '@deepseek-ai/cordis' {
interface Events {
/**
* Test-only one-way event: bound to no Scope and returning nothing.
* @param value - marker payload.
*/
'meta-fixture/forwardable'(value: string): void
/**
* Test-only Scope-bound event, which no carrier can deliver one-way.
* @param value - marker payload.
*/
'meta-fixture/scoped'(this: Context, value: string): void
/**
* Test-only answered event, whose result no one-way delivery can return.
* @param value - marker payload.
* @returns the replacement value.
*/
'meta-fixture/answered'(value: string): string
}
}
declare module '@deepseek-ai/dsh-typert-protocol' {
interface TypertContextMap {
metaFixture: TypertContext<string>
}
interface TypertRemoteEventSelection extends
Record<'meta-fixture/forwardable' | 'meta-fixture/absent', true> {}
}
describe('typert-protocol Remote declarations', () => {
it('binds a TypertRemoteService name and executes decorators through the Vitest source transform', async () => {
class Goals extends TypertRemoteService {
constructor(ctx: Context) {
super(ctx, 'goals')
}
@Remote
create(value: string): string {
return value
}
@RemoteScope('metaFixture')
scoped(value: string): string {
return value
}
}
class NamespacedGoals extends TypertRemoteService {
constructor(ctx: Context) {
super(ctx, 'internalGoals', { namespace: 'goals' })
}
}
const ctx = new Context()
const goals = new Goals(ctx)
const namespaced = new NamespacedGoals(ctx)
expect(goals.typertRemote).toEqual({ service: goals, serviceKey: 'goals', namespace: 'goals' })
expect(namespaced.typertRemote).toEqual({
service: namespaced,
serviceKey: 'internalGoals',
namespace: 'goals',
})
expect(remoteMethods(goals)).toEqual([
{ method: 'create', invocation: { kind: 'direct' } },
{ method: 'scoped', invocation: { kind: 'context', context: 'metaFixture' } },
])
await ctx.fiber.dispose()
})
it('executes standard decorator syntax through the TSX source launcher', () => {
const fixture = fileURLToPath(new URL('./fixtures/source-launch.ts', import.meta.url))
const output = execFileSync(process.execPath, ['--import', 'tsx/esm', fixture], { encoding: 'utf8' })
expect(JSON.parse(output)).toEqual([
{ method: 'create', invocation: { kind: 'direct' } },
{ method: 'scoped', invocation: { kind: 'context', context: 'agent' } },
])
})
it('keeps decorator markers in private module state', () => {
class Goals {
readonly typertRemote = bindTypertRemote(this, 'goals')
create(agent: object, request: object): object {
return { agent, request }
}
scoped(request: object): object {
return request
}
}
const initializers: Array<(this: Goals) => void> = []
Remote(
Reflect.get(Goals.prototype, 'create') as (this: Goals, ...args: unknown[]) => unknown,
methodContext('create', initializers),
)
RemoteScope('metaFixture')(
Reflect.get(Goals.prototype, 'scoped') as (this: Goals, ...args: unknown[]) => unknown,
methodContext('scoped', initializers),
)
const goals = new Goals()
for (const initialize of initializers) initialize.call(goals)
expect(goals.typertRemote).toEqual({ service: goals, serviceKey: 'goals', namespace: 'goals' })
expect(Object.isFrozen(goals.typertRemote)).toBe(true)
expect(remoteMethods(goals)).toEqual([
{ method: 'create', invocation: { kind: 'direct' } },
{ method: 'scoped', invocation: { kind: 'context', context: 'metaFixture' } },
])
expect(Reflect.ownKeys(Goals)).toEqual(['length', 'name', 'prototype'])
expect(Reflect.ownKeys(Goals.prototype)).toEqual(['constructor', 'create', 'scoped'])
})
it('keeps markers idempotent across instances and returns detached snapshots', () => {
class Service {
run(value: string): string {
return value
}
}
const initializers: Array<(this: Service) => void> = []
Remote(
Reflect.get(Service.prototype, 'run') as (this: Service, ...args: unknown[]) => unknown,
methodContext('run', initializers),
)
const first = new Service()
const second = new Service()
for (const initialize of initializers) {
initialize.call(first)
initialize.call(second)
}
const snapshot = remoteMethods(first)
expect(remoteMethods(second)).toEqual(snapshot)
;(snapshot as unknown as { method: string }[])[0]!.method = 'changed'
expect(remoteMethods(first)).toEqual([{ method: 'run', invocation: { kind: 'direct' } }])
})
it('supports explicit export names without exposing marker storage', () => {
class Service {
run(value: string): string {
return value
}
scoped(value: string): string {
return value
}
}
const initializers: Array<(this: Service) => void> = []
Remote('execute')(
Reflect.get(Service.prototype, 'run') as (this: Service, ...args: unknown[]) => unknown,
methodContext('run', initializers),
)
RemoteScope('metaFixture', 'inspect')(
Reflect.get(Service.prototype, 'scoped') as (this: Service, ...args: unknown[]) => unknown,
methodContext('scoped', initializers),
)
const service = new Service()
for (const initialize of initializers) initialize.call(service)
expect(remoteMethods(service)).toEqual([
{ method: 'run', exportName: 'execute', invocation: { kind: 'direct' } },
{ method: 'scoped', exportName: 'inspect', invocation: { kind: 'context', context: 'metaFixture' } },
])
expect(remoteMethods({})).toEqual([])
const prototypeLess: object = {}
Reflect.setPrototypeOf(prototypeLess, null)
expect(remoteMethods(prototypeLess)).toEqual([])
})
it('rejects malformed decorator calls and targets', () => {
const method: (this: object) => void = function (this: object): void {}
expect(() => { (Remote as unknown as (value: typeof method) => void)(method) }).toThrow('context is missing')
expect(() => Remote('bad/name')).toThrow('export name')
expect(() => Remote('bad#name')).toThrow('export name')
expect(() => Remote('bad name')).toThrow('export name')
expect(() => Remote('.')).toThrow('export name')
expect(() => Remote('..')).toThrow('export name')
expect(() => RemoteScope('' as 'metaFixture')).toThrow('Scope key')
expect(() => RemoteScope('metaFixture', 'bad/name')).toThrow('export name')
for (const context of [
{ ...methodContext('run', []), private: true },
{ ...methodContext('run', []), static: true },
{ ...methodContext('run', []), name: Symbol('run') },
]) {
expect(() => { Remote(method, context) })
.toThrow('public instance method')
}
})
it('rejects prototype-less initialization and conflicting markers', () => {
const method: (this: object) => void = function (this: object): void {}
const direct: Array<(this: object) => void> = []
Remote(method, methodContext('run', direct))
const prototypeLess: object = {}
Reflect.setPrototypeOf(prototypeLess, null)
expect(() => { direct[0]!.call(prototypeLess) }).toThrow('without a prototype')
class Service {
run(): void {}
}
const conflicting: Array<(this: Service) => void> = []
Remote(
Reflect.get(Service.prototype, 'run'),
methodContext('run', conflicting),
)
RemoteScope('metaFixture')(
Reflect.get(Service.prototype, 'run'),
methodContext('run', conflicting),
)
const service = new Service()
conflicting[0]!.call(service)
expect(() => { conflicting[1]!.call(service) }).toThrow('conflicting invocation markers')
})
it('rejects ambiguous binding names', () => {
expect(() => bindTypertRemote({}, '')).toThrow('service key')
expect(() => bindTypertRemote({}, 'goals', { namespace: 'api/goals' })).toThrow('namespace')
expect(() => bindTypertRemote({}, 'goals', { namespace: 'api goals' })).toThrow('namespace')
})
it('admits only one-way event shapes and only selected events that exist', () => {
expectTypeOf<'meta-fixture/forwardable'>().toExtend<TypertForwardableEvent>()
expectTypeOf<'meta-fixture/scoped'>().not.toExtend<TypertForwardableEvent>()
expectTypeOf<'meta-fixture/answered'>().not.toExtend<TypertForwardableEvent>()
expectTypeOf<'meta-fixture/forwardable'>().toExtend<TypertRemoteEvent>()
expectTypeOf<'meta-fixture/scoped'>().not.toExtend<TypertRemoteEvent>()
expectTypeOf<'meta-fixture/absent'>().not.toExtend<TypertRemoteEvent>()
})
})
function methodContext<This extends object>(
name: string,
initializers: Array<(this: This) => void>,
): ClassMethodDecoratorContext<This, (this: This, ...args: unknown[]) => unknown> {
return {
kind: 'method',
name,
static: false,
private: false,
metadata: {},
access: {
has: object => name in object,
get: object => (object as Record<string, unknown>)[name] as (this: This, ...args: unknown[]) => unknown,
},
addInitializer: (initializer) => { initializers.push(initializer) },
}
}

View File

@@ -0,0 +1,21 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../runtime-diagnostics/invariants"
}
]
}