docs: restore generated Cordis core API

This commit is contained in:
Yichen Jiang
2026-07-20 16:32:08 +08:00
parent 8ffca1f9c4
commit 2b1b598467
17 changed files with 1795 additions and 10 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
2026-07-20-generated-cordis-core-api.md: 848dec2dba6f432c706798c40abe98e8937da651
2026-07-20-generated-cordis-core-api.zh.md: c40a480224f4e1387b71ade9264458cd84403584

View File

@@ -0,0 +1,31 @@
# Agent Note: Generate the Cordis core API reference
Status: implemented
English | [中文](2026-07-20-generated-cordis-core-api.zh.md)
## Problem
Plugin authors need the detailed Cordis APIs behind `ctx`, event dispatch, fibers, plugin registration, and services. The generated [Harness event and service catalogs](2026-06-20-generated-cordis-catalog.md) intentionally summarize inherited Cordis members, so they do not replace a method-level Cordis reference. Keeping a second hand-written copy under the website would drift from the vendored source and make the renderer an additional documentation owner.
## Decision
`scripts/cordis-core-api.ts` reads the public declarations and original JSDoc from `vendor/cordis/src` with the TypeScript compiler API. An explicit page manifest generates five files under [`docs/cordis-catalog/core/`](../../../../docs/cordis-catalog/core/context.md): Context, Events, Fiber, Registry, and Service. `scripts/gen-cordis-catalog.ts` writes these pages together with the Harness event and service catalogs, and `verify-cordis-catalog` rejects stale output.
The generator validates that documented classes and methods retain descriptive JSDoc, including parameter and non-void return contracts. It emits declaration-only `ts cordis-catalog` fences with the original JSDoc, then renders the same description, parameters, and return contract as readable Markdown. Source links point to the vendored files, and the five pages cross-link to one another. The Harness catalogs remain the exhaustive inventory of repository-declared events and `ctx.*` services; the core pages document how the inherited Cordis APIs operate.
`website/docs.ts` publishes the five canonical files under matching `/reference/cordis-api/` and `/en/reference/cordis-api/` routes. Both locales use the English generated source until the generator emits translated pages, so changing language preserves navigation structure and route identity.
## Alternatives considered
**Restore the old website files as canonical Markdown.** This would recover the pages quickly, but their signatures and prose could drift from the vendored implementation and the website would regain a second documentation source.
**Expand the inherited tier of the Harness catalogs in place.** Those catalogs answer which Harness events and services exist. Mixing full framework class references into the same pages would obscure that inventory and reverse their deliberate terse inherited tier.
**Publish vendored source declarations directly.** Source files are authoritative but do not provide stable topic pages, curated public ordering, or website navigation, and they expose implementation bodies that are not part of the reference contract.
## Consequences
The five Cordis API pages follow vendor updates through one deterministic generator and share the repository's documentation freshness gate. The website gains a dedicated Cordis API section without copied site content, while root and English navigation remain structurally identical.
The page manifest is curated, so a newly public Cordis core type needs an explicit generator entry. Generated prose is English-only, and source JSDoc quality directly limits reference quality; Chinese output requires generator-level translation rather than hand-editing the generated files.

View File

@@ -0,0 +1,31 @@
# Agent Note: 生成 Cordis 核心 API 参考文档
Status: implemented
[English](2026-07-20-generated-cordis-core-api.md) | 中文
## 问题
插件作者需要了解 `ctx`、事件派发、Fiber、插件注册和 Service 背后的详细 Cordis API。已有的 [Harness 事件与服务目录](2026-06-20-generated-cordis-catalog.md)有意只简要概括继承自 Cordis 的成员,因此无法替代方法级 Cordis 参考文档。如果在网站下维护另一份手写副本,它会与 vendored 源码产生漂移,也会让渲染器成为额外的文档所有者。
## 决策
`scripts/cordis-core-api.ts` 使用 TypeScript Compiler API`vendor/cordis/src` 读取公开声明和原始 JSDoc。一个显式页面清单在 [`docs/cordis-catalog/core/`](../../../../docs/cordis-catalog/core/context.md) 下生成五个文件Context、Events、Fiber、Registry 和 Service。`scripts/gen-cordis-catalog.ts` 将这些页面与 Harness 事件和服务目录一同写入,`verify-cordis-catalog` 会拒绝过期产物。
生成器会验证所记录的类和方法保留描述性 JSDoc包括参数和非 void 返回值契约。它生成包含原始 JSDoc 且仅含声明的 `ts cordis-catalog` 代码围栏,再将同一份说明、参数和返回值契约渲染为便于阅读的 Markdown。源码链接指向 vendored 文件五个页面之间相互交叉链接。Harness 目录仍是仓库声明的事件与 `ctx.*` 服务的完整清单;核心页面负责说明继承自 Cordis 的 API 如何工作。
`website/docs.ts` 将五个规范源文件发布到结构对应的 `/reference/cordis-api/``/en/reference/cordis-api/` 路由。在生成器产出翻译页面之前,两个 locale 都使用英文生成源,因此切换语言时导航结构和路由标识保持不变。
## 考虑过的替代方案
**将旧网站文件恢复为规范 Markdown。** 这能快速恢复页面,但其签名和说明可能与 vendored 实现漂移,网站也会重新成为第二个文档来源。
**直接扩充 Harness 目录中的继承层。** 这些目录回答有哪些 Harness 事件与服务。将完整的框架类参考混入同一页面会模糊这份清单的定位,并推翻继承层保持精简的既有决定。
**直接发布 vendored 源码声明。** 源文件具有权威性,但不能提供稳定的主题页面、经过筛选的公开顺序或网站导航,还会暴露不属于参考契约的实现体。
## 影响
五个 Cordis API 页面通过同一个确定性生成器跟随 vendor 更新,并复用仓库的文档新鲜度检查。网站无需复制内容即可获得独立的 Cordis API 章节,中文入口和英文入口的导航结构保持一致。
页面清单需要人工维护,因此新增公开 Cordis 核心类型时必须显式添加生成器条目。当前生成说明只有英文,且源码 JSDoc 的质量直接决定参考文档质量;中文产物需要在生成器层实现翻译,不能手工编辑生成文件。

View File

@@ -18,7 +18,7 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home;
| [user/](user/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history |
| Package README | The per-package contract: config, semantics, limitations, extension points, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns |
| [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ Agent Notes), gate-by-gate enumerations that drift from `package.json` scripts |
| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |
| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [Cordis core API](cordis-catalog/core/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |
| Skills (`.agents/skills/`) | Reusable workflows and specialized decision standards | Product and runtime contracts (→ docs or source) |
Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookbooks; type shapes → core data; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link.

View File

@@ -0,0 +1,364 @@
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
Run `pnpm run gen-cordis-catalog` to regenerate. -->
# Context
The context is the core Cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods are documented on [Events](events.md), effects and the current fiber on [Fiber](fiber.md), and plugin loading on [Registry](registry.md).
Root and child dependency containers for Cordis plugins.
A context is a proxy: normal property reads go through the service resolver, while `extend()`, `isolate()`, and `intercept()` create scoped child contexts without mutating their parent.
[Source](../../../vendor/cordis/src/context.ts#L42)
### ctx.extend(meta?)
```ts cordis-catalog
/**
* Create a child context with extra metadata on top of the current scope.
*
* The child prototypally inherits every property of this context; own
* properties of `meta` shadow the inherited ones. The parent is not mutated.
*
* @param meta — own properties (including symbol keys) to define on the child.
* @returns a child context inheriting from this one.
*/
extend(meta = {}): this
```
Create a child context with extra metadata on top of the current scope.
The child prototypally inherits every property of this context; own properties of `meta` shadow the inherited ones. The parent is not mutated.
- `meta` — own properties (including symbol keys) to define on the child.
**Returns** a child context inheriting from this one.
[Source](../../../vendor/cordis/src/context.ts#L99)
### ctx.isolate(name, label?)
```ts cordis-catalog
/**
* Create a child context with an independent service scope for `name`.
*
* Below the returned context, reads and writes of the service `name`
* resolve against the new label instead of the parent's, so a different
* implementation can be provided without affecting the parent scope.
* Passing the same `label` to two `isolate()` calls joins their scopes.
*
* @param name — the service name to isolate.
* @param label — scope label to join; defaults to a fresh unique symbol.
* @returns a child context whose `name` service resolves in the new scope.
*/
isolate(name: string, label?: symbol)
```
Create a child context with an independent service scope for `name`.
Below the returned context, reads and writes of the service `name` resolve against the new label instead of the parent's, so a different implementation can be provided without affecting the parent scope. Passing the same `label` to two `isolate()` calls joins their scopes.
- `name` — the service name to isolate.
- `label` — scope label to join; defaults to a fresh unique symbol.
**Returns** a child context whose `name` service resolves in the new scope.
[Source](../../../vendor/cordis/src/context.ts#L121)
### ctx.intercept(name, config)
```ts cordis-catalog
/**
* Add service-specific intercept config for plugins started below this
* context.
*
* Plugins loaded under the returned context see `config` merged into the
* service's resolved config (ancestor entries first; see
* `Service[symbols.resolveConfig]`). The parent context is not affected.
*
* @param name — the service name whose config to intercept.
* @param config — the intercept config to merge for that service.
* @returns a child context carrying the additional intercept entry.
*/
intercept<K extends InjectKey>(name: K, config: Context[K] extends { [symbols.config]: infer T } ? T : never): this
intercept(name: string, config: any): this
```
Add service-specific intercept config for plugins started below this context.
Plugins loaded under the returned context see `config` merged into the service's resolved config (ancestor entries first; see `Service[symbols.resolveConfig]`). The parent context is not affected.
- `name` — the service name whose config to intercept.
- `config` — the intercept config to merge for that service.
**Returns** a child context carrying the additional intercept entry.
[Source](../../../vendor/cordis/src/context.ts#L139)
### ctx.root
```ts cordis-catalog
/** The root context of the application (every child context shares it). @experimental */
root: this
```
The root context of the application (every child context shares it). @experimental
[Source](../../../vendor/cordis/src/context.ts#L22)
### ctx.baseUrl
```ts cordis-catalog
/** Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. */
baseUrl?: string
```
Base URL used to resolve relative plugin/module specifiers, if the runtime sets one.
[Source](../../../vendor/cordis/src/context.ts#L24)
### ctx.events
```ts cordis-catalog
/** The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...). */
events: EventsService
```
The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...).
[Source](../../../vendor/cordis/src/context.ts#L26)
### ctx.logger
```ts cordis-catalog
/** The logging service. Call `ctx.logger(name)` for a named logger. */
logger: LoggerService
```
The logging service. Call `ctx.logger(name)` for a named logger.
[Source](../../../vendor/cordis/src/context.ts#L28)
### ctx.reflect
```ts cordis-catalog
/** The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...). */
reflect: ReflectService
```
The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...).
[Source](../../../vendor/cordis/src/context.ts#L30)
### ctx.registry
```ts cordis-catalog
/** The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`). */
registry: RegistryService
```
The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`).
[Source](../../../vendor/cordis/src/context.ts#L32)
## Static members
### Context.effect
```ts cordis-catalog
/** Symbol key under which a disposer exposes its {@link EffectMeta} diagnostics tree. */
static readonly effect: unique symbol
```
Symbol key under which a disposer exposes its EffectMeta diagnostics tree.
[Source](../../../vendor/cordis/src/context.ts#L44)
### Context.filter
```ts cordis-catalog
/** Symbol key for a context's listener filter, consulted on every event dispatch. */
static readonly filter: unique symbol
```
Symbol key for a context's listener filter, consulted on every event dispatch.
[Source](../../../vendor/cordis/src/context.ts#L46)
### Context.isolate
```ts cordis-catalog
/** Symbol key of the isolation map (see the `Context[symbols.isolate]` property). */
static readonly isolate: unique symbol
```
Symbol key of the isolation map (see the `Context[symbols.isolate]` property).
[Source](../../../vendor/cordis/src/context.ts#L48)
### Context.intercept
```ts cordis-catalog
/** Symbol key of the intercept map (see the `Context[symbols.intercept]` property). */
static readonly intercept: unique symbol
```
Symbol key of the intercept map (see the `Context[symbols.intercept]` property).
[Source](../../../vendor/cordis/src/context.ts#L50)
### Context.is(value)
```ts cordis-catalog
/**
* Returns true for Cordis context proxies and context prototypes.
*
* Works across realms and across multiple copies of cordis, because the
* brand is keyed by a global symbol rather than by `instanceof`.
*
* @param value — the value to test.
* @returns `true` if `value` is a Cordis context, narrowing its type.
*/
static is(value: any): value is Context
```
Returns true for Cordis context proxies and context prototypes.
Works across realms and across multiple copies of cordis, because the brand is keyed by a global symbol rather than by `instanceof`.
- `value` — the value to test.
**Returns** `true` if `value` is a Cordis context, narrowing its type.
[Source](../../../vendor/cordis/src/context.ts#L61)
## Service store and mixins
### ctx.get(name, strict?)
```ts cordis-catalog
/**
* Read a service from the store without the inject requirement.
*
* @param name — the service name.
* @param strict — when `true` (default), only return implementations
* whose providing fiber is currently active.
* @returns the service value, or `undefined` when not (yet) provided.
*/
get<K extends string & keyof this>(name: K, strict?: boolean): undefined | this[K]
get(name: string, strict?: boolean): any
```
Read a service from the store without the inject requirement.
- `name` — the service name.
- `strict` — when `true` (default), only return implementations whose providing fiber is currently active.
**Returns** the service value, or `undefined` when not (yet) provided.
[Source](../../../vendor/cordis/src/reflect.ts#L16)
### ctx.set(name, value)
```ts cordis-catalog
/**
* Overwrite a provided service's value.
*
* Only the fiber that provided the service may set it; setting an
* unprovided name throws.
*
* @param name — the service name.
* @param value — the new service value.
*/
set<K extends string & keyof this>(name: K, value: undefined | this[K]): void
set(name: string, value: any): void
```
Overwrite a provided service's value.
Only the fiber that provided the service may set it; setting an unprovided name throws.
- `name` — the service name.
- `value` — the new service value.
[Source](../../../vendor/cordis/src/reflect.ts#L28)
### ctx.provide(name, value)
```ts cordis-catalog
/**
* Register a service implementation owned by the current fiber.
*
* The service becomes visible to dependents in the same isolation scope
* once the fiber is active; it is unregistered (waking dependents) when
* the returned disposer runs or the fiber unloads. Throws if the name is
* already provided in this scope or declared as an accessor.
*
* @param name — the service name.
* @param value — the service value.
* @returns a disposer that unregisters the service.
*/
provide<K extends string & keyof this>(name: K, value: undefined | this[K]): () => void
provide(name: string, value?: any): () => void
```
Register a service implementation owned by the current fiber.
The service becomes visible to dependents in the same isolation scope once the fiber is active; it is unregistered (waking dependents) when the returned disposer runs or the fiber unloads. Throws if the name is already provided in this scope or declared as an accessor.
- `name` — the service name.
- `value` — the service value.
**Returns** a disposer that unregisters the service.
[Source](../../../vendor/cordis/src/reflect.ts#L43)
### ctx.accessor(name, options)
```ts cordis-catalog
/**
* Define a computed context property backed by get/set hooks.
*
* The accessor is removed when the current fiber unloads. Throws if the
* name is already declared.
*
* @param name — the context property name.
* @param options — the `get` hook and optional `set` hook.
*/
accessor(name: string, options: Omit<Property.Accessor, 'type'>): void
```
Define a computed context property backed by get/set hooks.
The accessor is removed when the current fiber unloads. Throws if the name is already declared.
- `name` — the context property name.
- `options` — the `get` hook and optional `set` hook.
[Source](../../../vendor/cordis/src/reflect.ts#L55)
### ctx.mixin(name, mixins)
```ts cordis-catalog
/**
* Expose selected members of a service directly on `ctx`.
*
* Each mixed-in key becomes an accessor that forwards to the service
* (binding methods to it), so e.g. `ctx.on` forwards to `ctx.events.on`.
* Mixins are removed when the current fiber unloads.
*
* @param name — the context property holding the source service.
* @param mixins — keys to forward, or a source-key → ctx-key map.
*/
mixin<K extends string & keyof this>(name: K, mixins: (keyof this & keyof this[K])[] | Dict<string>): void
mixin<T extends {}>(source: T, mixins: (keyof this & keyof T)[] | Dict<string>): void
```
Expose selected members of a service directly on `ctx`.
Each mixed-in key becomes an accessor that forwards to the service (binding methods to it), so e.g. `ctx.on` forwards to `ctx.events.on`. Mixins are removed when the current fiber unloads.
- `name` — the context property holding the source service.
- `mixins` — keys to forward, or a source-key → ctx-key map.
[Source](../../../vendor/cordis/src/reflect.ts#L66)

View File

@@ -0,0 +1,207 @@
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
Run `pnpm run gen-cordis-catalog` to regenerate. -->
# Events
The event-dispatch API mixed into every context. Harness event declarations and their dispatch modes are generated separately in the [Cordis events catalog](../events.md).
### ctx.parallel(name, ...args)
```ts cordis-catalog
/**
* Dispatch an event, running all listeners concurrently.
*
* @param name — the event name.
* @param args — arguments passed to every listener.
* @returns a promise resolving once every listener has settled.
*/
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>
parallel<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promise<void>
```
Dispatch an event, running all listeners concurrently.
- `name` — the event name.
- `args` — arguments passed to every listener.
**Returns** a promise resolving once every listener has settled.
[Source](../../../vendor/cordis/src/events.ts#L43)
### ctx.emit(name, ...args)
```ts cordis-catalog
/**
* Dispatch an event synchronously, ignoring listener return values.
*
* @param name — the event name.
* @param args — arguments passed to every listener.
*/
emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void
emit<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): void
```
Dispatch an event synchronously, ignoring listener return values.
- `name` — the event name.
- `args` — arguments passed to every listener.
[Source](../../../vendor/cordis/src/events.ts#L52)
### ctx.serial(name, ...args)
```ts cordis-catalog
/**
* Dispatch an event, awaiting listeners in order until one bails.
*
* @param name — the event name.
* @param args — arguments passed to each listener.
* @returns the first bail value (non-null, non-false, non-undefined), if any.
*/
serial<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
serial<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
```
Dispatch an event, awaiting listeners in order until one bails.
- `name` — the event name.
- `args` — arguments passed to each listener.
**Returns** the first bail value (non-null, non-false, non-undefined), if any.
[Source](../../../vendor/cordis/src/events.ts#L62)
### ctx.bail(name, ...args)
```ts cordis-catalog
/**
* Dispatch an event, calling listeners in order until one bails.
*
* @param name — the event name.
* @param args — arguments passed to each listener.
* @returns the first bail value (non-null, non-false, non-undefined), if any.
*/
bail<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
bail<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
```
Dispatch an event, calling listeners in order until one bails.
- `name` — the event name.
- `args` — arguments passed to each listener.
**Returns** the first bail value (non-null, non-false, non-undefined), if any.
[Source](../../../vendor/cordis/src/events.ts#L72)
### ctx.waterfall(name, ...args)
```ts cordis-catalog
/**
* Dispatch an event whose last argument is a `next` continuation.
*
* Each listener wraps the rest of the chain: calling `next()` invokes the
* next listener (finally the built-in behavior); not calling it vetoes.
*
* @param name — the event name.
* @param args — listener arguments; the final one is the innermost `next`.
* @returns the outermost listener's return value.
*/
waterfall<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
waterfall<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
```
Dispatch an event whose last argument is a `next` continuation.
Each listener wraps the rest of the chain: calling `next()` invokes the next listener (finally the built-in behavior); not calling it vetoes.
- `name` — the event name.
- `args` — listener arguments; the final one is the innermost `next`.
**Returns** the outermost listener's return value.
[Source](../../../vendor/cordis/src/events.ts#L85)
### ctx.on(name, listener, options?)
```ts cordis-catalog
/**
* Register an event listener owned by the current fiber.
*
* @param name — the event name to listen for.
* @param listener — called with the dispatch arguments.
* @param options — listener options; a boolean is shorthand for `prepend`.
* @returns a disposer removing the listener; `true` if it was still registered.
*/
on<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
```
Register an event listener owned by the current fiber.
- `name` — the event name to listen for.
- `listener` — called with the dispatch arguments.
- `options` — listener options; a boolean is shorthand for `prepend`.
**Returns** a disposer removing the listener; `true` if it was still registered.
[Source](../../../vendor/cordis/src/events.ts#L96)
### ctx.once(name, listener, options?)
```ts cordis-catalog
/**
* Same as `on()`, but the listener disposes itself after its first call.
*
* @param name — the event name to listen for.
* @param listener — called at most once with the dispatch arguments.
* @param options — listener options; a boolean is shorthand for `prepend`.
* @returns a disposer removing the listener; `true` if it was still registered.
*/
once<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
```
Same as `on()`, but the listener disposes itself after its first call.
- `name` — the event name to listen for.
- `listener` — called at most once with the dispatch arguments.
- `options` — listener options; a boolean is shorthand for `prepend`.
**Returns** a disposer removing the listener; `true` if it was still registered.
[Source](../../../vendor/cordis/src/events.ts#L105)
## EventOptions
Options accepted by `ctx.on()` and `ctx.once()`.
```ts cordis-catalog
/** Options accepted by `ctx.on()` and `ctx.once()`. */
interface EventOptions {
/** Add the listener before existing listeners for the same event. */
prepend?: boolean
/** Receive the event regardless of context filter checks. */
global?: boolean
}
```
[Source](../../../vendor/cordis/src/events.ts#L111)
## DispatchMode
Event dispatch strategy used by the event service.
`emit` runs synchronous listeners without awaiting them, `parallel` awaits all listeners together, `serial` awaits them in order until one bails, `bail` stops on the first synchronous bail value, and `waterfall` composes listeners around a final `next` callback.
```ts cordis-catalog
/**
* Event dispatch strategy used by the event service.
*
* `emit` runs synchronous listeners without awaiting them, `parallel` awaits
* all listeners together, `serial` awaits them in order until one bails,
* `bail` stops on the first synchronous bail value, and `waterfall` composes
* listeners around a final `next` callback.
*/
type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'
```
[Source](../../../vendor/cordis/src/events.ts#L31)

View File

@@ -0,0 +1,375 @@
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
Run `pnpm run gen-cordis-catalog` to regenerate. -->
# Fiber
A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber, and `ctx.effect()` delegates to it.
### ctx.effect(execute, label?)
```ts cordis-catalog
/**
* Register a cleanup-aware effect on this fiber.
*
* `execute` runs immediately; the disposers it produces are collected and
* run (in reverse order) either when the returned disposer is called or
* when the fiber unloads, whichever comes first. Calling the disposer twice
* is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is
* already disposed, and `TypeError` if `execute` returns an invalid shape.
*
* @param execute — the effect body; see {@link Effect} for accepted shapes.
* @param label — effect label shown in `getEffects()` diagnostics.
* @returns a disposer that tears the effect down and settles once done.
*/
effect(execute: () => SyncEffect, label?: string): Disposable<Promise<void>>
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
```
Register a cleanup-aware effect on this fiber.
`execute` runs immediately; the disposers it produces are collected and run (in reverse order) either when the returned disposer is called or when the fiber unloads, whichever comes first. Calling the disposer twice is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed, and `TypeError` if `execute` returns an invalid shape.
- `execute` — the effect body; see `Effect` for accepted shapes.
- `label` — effect label shown in `getEffects()` diagnostics.
**Returns** a disposer that tears the effect down and settles once done.
[Source](../../../vendor/cordis/src/fiber.ts#L419)
### ctx.fiber
```ts cordis-catalog
/** The fiber (plugin runtime instance) that owns this context. */
fiber: Fiber
```
The fiber (plugin runtime instance) that owns this context.
[Source](../../../vendor/cordis/src/fiber.ts#L11)
## The Fiber class
Runtime instance of one plugin application.
A fiber tracks dependency state, validated config, lifecycle effects, and cleanup for the plugin context returned by `ctx.plugin()`.
[Source](../../../vendor/cordis/src/fiber.ts#L183)
### fiber.uid
```ts cordis-catalog
/** Unique id within the registry; 0 for the root fiber, `null` once disposed. */
public uid: number | null
```
Unique id within the registry; 0 for the root fiber, `null` once disposed.
[Source](../../../vendor/cordis/src/fiber.ts#L185)
### fiber.ctx
```ts cordis-catalog
/** The context this fiber's plugin runs in (extends the parent context). */
public readonly ctx: Context
```
The context this fiber's plugin runs in (extends the parent context).
[Source](../../../vendor/cordis/src/fiber.ts#L187)
### fiber.config
```ts cordis-catalog
/** The validated plugin config (updated by `update()`). */
public config: any
```
The validated plugin config (updated by `update()`).
[Source](../../../vendor/cordis/src/fiber.ts#L189)
### fiber.state
```ts cordis-catalog
/** Current lifecycle state; transitions emit `internal/status`. */
public state
```
Current lifecycle state; transitions emit `internal/status`.
[Source](../../../vendor/cordis/src/fiber.ts#L191)
### fiber.dispose
```ts cordis-catalog
/** Dispose this fiber: unload the plugin, then settle once cleanup finished. */
public readonly dispose: () => Promise<void>
```
Dispose this fiber: unload the plugin, then settle once cleanup finished.
[Source](../../../vendor/cordis/src/fiber.ts#L193)
### fiber.store
```ts cordis-catalog
/** Snapshot of required service implementations while loaded; `undefined` otherwise. */
public store: Dict<Impl> | undefined
```
Snapshot of required service implementations while loaded; `undefined` otherwise.
[Source](../../../vendor/cordis/src/fiber.ts#L195)
### fiber.inertia
```ts cordis-catalog
/** The in-flight load/unload transition, if one is currently running. */
public inertia: Promise<void> | undefined
```
The in-flight load/unload transition, if one is currently running.
[Source](../../../vendor/cordis/src/fiber.ts#L197)
### fiber.name
```ts cordis-catalog
/** The plugin's display name, inherited from the nearest named ancestor, else `'root'`. */
get name()
```
The plugin's display name, inherited from the nearest named ancestor, else `'root'`.
[Source](../../../vendor/cordis/src/fiber.ts#L340)
### fiber.assertActive()
```ts cordis-catalog
/**
* Throw if the fiber has already been disposed.
*
* @returns nothing when the fiber is still active.
* @throws {CordisError} `INACTIVE_EFFECT` when the fiber's uid has been cleared.
*/
assertActive()
```
Throw if the fiber has already been disposed.
**Returns** nothing when the fiber is still active.
[Source](../../../vendor/cordis/src/fiber.ts#L355)
### fiber.effect(execute, label?)
```ts cordis-catalog
/**
* Register a cleanup-aware effect on this fiber.
*
* `execute` runs immediately; the disposers it produces are collected and
* run (in reverse order) either when the returned disposer is called or
* when the fiber unloads, whichever comes first. Calling the disposer twice
* is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is
* already disposed, and `TypeError` if `execute` returns an invalid shape.
*
* @param execute — the effect body; see {@link Effect} for accepted shapes.
* @param label — effect label shown in `getEffects()` diagnostics.
* @returns a disposer that tears the effect down and settles once done.
*/
effect(execute: () => SyncEffect, label?: string): Disposable<Promise<void>>
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
```
Register a cleanup-aware effect on this fiber.
`execute` runs immediately; the disposers it produces are collected and run (in reverse order) either when the returned disposer is called or when the fiber unloads, whichever comes first. Calling the disposer twice is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed, and `TypeError` if `execute` returns an invalid shape.
- `execute` — the effect body; see `Effect` for accepted shapes.
- `label` — effect label shown in `getEffects()` diagnostics.
**Returns** a disposer that tears the effect down and settles once done.
[Source](../../../vendor/cordis/src/fiber.ts#L419)
### fiber.getEffects()
```ts cordis-catalog
/**
* Return metadata for currently registered effects.
*
* @returns one {@link EffectMeta} tree per labeled live effect.
*/
getEffects()
```
Return metadata for currently registered effects.
**Returns** one `EffectMeta` tree per labeled live effect.
[Source](../../../vendor/cordis/src/fiber.ts#L572)
### fiber.await()
```ts cordis-catalog
/**
* Wait for current lifecycle work and rethrow startup errors.
*
* @returns this fiber, once it has settled into a stable state.
* @throws the config-validation or plugin-startup error, if any.
*/
async await()
```
Wait for current lifecycle work and rethrow startup errors.
**Returns** this fiber, once it has settled into a stable state.
[Source](../../../vendor/cordis/src/fiber.ts#L701)
### fiber.restart()
```ts cordis-catalog
/**
* Dispose and immediately reload this plugin with its current config.
*
* @returns a promise resolving once the reload settled.
* @throws {CordisError} `INACTIVE_EFFECT` when the fiber is already disposed.
*/
async restart()
```
Dispose and immediately reload this plugin with its current config.
**Returns** a promise resolving once the reload settled.
[Source](../../../vendor/cordis/src/fiber.ts#L715)
### fiber.update(config, noSave?)
```ts cordis-catalog
/**
* Validate and apply new config, then restart the plugin.
*
* Runs the `internal/update` waterfall first, so update hooks (and HMR)
* can veto or replace the restart.
*
* @param config — the new raw config; validated before anything restarts.
* @param noSave — hint for persistence hooks not to write the change back.
* @returns nothing; the restart runs behind the `internal/update` waterfall.
* @throws {ValidationError} when the new config fails validation.
*/
update(config: any, noSave = false)
```
Validate and apply new config, then restart the plugin.
Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto or replace the restart.
- `config` — the new raw config; validated before anything restarts.
- `noSave` — hint for persistence hooks not to write the change back.
**Returns** nothing; the restart runs behind the `internal/update` waterfall.
[Source](../../../vendor/cordis/src/fiber.ts#L733)
## Effect
Effect body result accepted by `ctx.effect()` and plugin startup.
Either a single disposer, a promise of one, or a (possibly async) iterable yielding several — generator effects register each yielded disposer as it is produced.
```ts cordis-catalog
/**
* Effect body result accepted by `ctx.effect()` and plugin startup.
*
* Either a single disposer, a promise of one, or a (possibly async) iterable
* yielding several — generator effects register each yielded disposer as it
* is produced.
*/
type Effect<T = any> =
| SyncEffect<T>
| AsyncEffect<T>
```
[Source](../../../vendor/cordis/src/fiber.ts#L82)
## Disposable
Function returned by an effect to release resources during disposal.
Disposers run in reverse registration order when the owning fiber unloads; they may be async, in which case unloading awaits them.
```ts cordis-catalog
/**
* Function returned by an effect to release resources during disposal.
*
* Disposers run in reverse registration order when the owning fiber unloads;
* they may be async, in which case unloading awaits them.
*/
type Disposable<T = any> = () => T
```
[Source](../../../vendor/cordis/src/fiber.ts#L73)
## EffectMeta
Tree node used to expose nested effect labels for diagnostics.
```ts cordis-catalog
/** Tree node used to expose nested effect labels for diagnostics. */
interface EffectMeta {
/** Human-readable effect label, e.g. `ctx.on("event")` or `ctx.provide("name")`. */
label: string
/** Metadata of nested effects registered while this effect ran. */
children: EffectMeta[]
}
```
[Source](../../../vendor/cordis/src/fiber.ts#L95)
## CordisError
Framework error with a stable machine-readable code.
```ts cordis-catalog
/** Framework error with a stable machine-readable code. */
class CordisError extends Error {
/**
* @param code — the stable error code; also the default message.
* @param message — optional human-readable override.
*/
constructor(public code: CordisError.Code, message?: string)
}
/** Cordis error code definitions. */
namespace CordisError {
export type Code = keyof typeof Code
export const Code = {
INACTIVE_EFFECT: 'cannot create effect on inactive context',
} as const
}
```
[Source](../../../vendor/cordis/src/fiber.ts#L156)
## ValidationError
Error raised when plugin configuration fails standard-schema validation.
```ts cordis-catalog
/** Error raised when plugin configuration fails standard-schema validation. */
class ValidationError extends TypeError {
name = 'ValidationError'
/**
* Build the aggregated message from schema issues.
*
* @param issues — the standard-schema issues, one message line each.
*/
constructor(issues: readonly StandardSchemaV1.Issue[])
}
```
[Source](../../../vendor/cordis/src/fiber.ts#L18)

View File

@@ -0,0 +1,152 @@
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
Run `pnpm run gen-cordis-catalog` to regenerate. -->
# Registry
Plugin loading and dependency injection.
### ctx.inject(deps, callback)
```ts cordis-catalog
/**
* Run a callback once the requested services are available.
*
* Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback
* is unloaded and re-run whenever a required service changes.
*
* @param deps — required services, as an array or a name → config map.
* @param callback — plugin body called with `(ctx, config)`.
* @returns the fiber; awaiting it settles once loading finished.
*/
inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>
```
Run a callback once the requested services are available.
Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback is unloaded and re-run whenever a required service changes.
- `deps` — required services, as an array or a name → config map.
- `callback` — plugin body called with `(ctx, config)`.
**Returns** the fiber; awaiting it settles once loading finished.
[Source](../../../vendor/cordis/src/registry.ts#L175)
### ctx.plugin(plugin, ...args)
```ts cordis-catalog
/**
* Load a plugin in the current context.
*
* @param plugin — a function, class, or `{ apply }` object plugin.
* @param args — the plugin config, validated against its `Config` schema.
* @returns the fiber; awaiting it settles once loading finished
* (rejecting on config or startup errors).
*/
plugin<P extends Plugin>(plugin: P, ...args: Spread<GetPluginConfig<P>>): Fiber & PromiseLike<Fiber>
```
Load a plugin in the current context.
- `plugin` — a function, class, or `{ apply }` object plugin.
- `args` — the plugin config, validated against its `Config` schema.
**Returns** the fiber; awaiting it settles once loading finished (rejecting on config or startup errors).
[Source](../../../vendor/cordis/src/registry.ts#L184)
## Plugin
Supported plugin entrypoint shapes.
```ts cordis-catalog
/** Supported plugin entrypoint shapes. */
type Plugin<T = any> =
| Plugin.Function<T>
| Plugin.Constructor<T>
| Plugin.Object<T>
/** Types associated with plugin entrypoints and runtime records. */
namespace Plugin {
/** Shared metadata understood by the plugin registry and related tooling. */
export interface Base<T = any> {
/** Display name used for fiber diagnostics and logger names. */
name?: string
/** Standard-schema validator applied to config before the plugin starts. */
Config?: StandardSchemaV1<any, T>
/** Services the plugin requires; it only loads while all are available. */
inject?: Inject
/** Service name(s) the plugin provides (read by `Service` and by loaders). */
provide?: string | string[]
/** Service names whose intercept config the plugin declares it consumes. */
intercept?: Dict<boolean>
}
export interface Transform<S, T> {
/** Marks the transform object as a schema/config transform. */
schema?: true
/** Convert user-facing config to runtime config. */
Config: (config: S) => T
}
/** Function plugin called with `(ctx, config)`. */
export interface Function<T = any> extends Base<T> {
(ctx: Context, config: T): any
}
/** Class plugin constructed with `(ctx, config)`. */
export interface Constructor<T = any> extends Base<T> {
new (ctx: Context, config: T): any
}
/** Object plugin with an `apply(ctx, config)` method. */
export interface Object<T = any> extends Base<T> {
apply(ctx: Context, config: T): any
}
/** Mutable registry record shared by all fibers of one plugin callback. */
export interface Runtime {
/** Display name copied from the first registered plugin shape. */
name?: string
/** Every live fiber of this plugin (one per `ctx.plugin()` call). */
fibers: DisposableList<Fiber>
/** The executable entrypoint all fibers share (registry identity key). */
callback: globalThis.Function
/** Standard-schema validator applied to each fiber's config. */
Config?: StandardSchemaV1
}
}
```
[Source](../../../vendor/cordis/src/registry.ts#L91)
## Inject
Service dependency declaration accepted by plugins and the `@Inject` decorator.
Array form requests services without intercept config. Object form maps each service name to optional intercept config for the plugin context.
```ts cordis-catalog
/**
* Service dependency declaration accepted by plugins and the `@Inject`
* decorator.
*
* Array form requests services without intercept config. Object form maps each
* service name to optional intercept config for the plugin context.
*/
type Inject<M = Dict> = (keyof M)[] | { [K in keyof M]?: M[K] }
/** Utilities for normalizing plugin dependency declarations. */
namespace Inject {
/**
* Convert array/object/class-inherited inject metadata into a plain map.
*
* @param inject — the declaration to normalize; `null`/`undefined` add nothing.
* @param result — the map to fill (service name → intercept config or `null`).
* @returns `result`.
*/
export function resolve(inject: Inject | null | undefined, result: Dict = Object.create(null))
}
```
[Source](../../../vendor/cordis/src/registry.ts#L18)

View File

@@ -0,0 +1,102 @@
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
Run `pnpm run gen-cordis-catalog` to regenerate. -->
# Service
The base class for context services. A subclass loaded as a plugin registers itself as `ctx.<name>`.
Base class for services that expose a named API on `ctx`.
Subclasses call `super(ctx, name)` from their constructor. The service is registered immediately and is automatically removed with the owning fiber.
[Source](../../../vendor/cordis/src/service.ts#L11)
### service.name
```ts cordis-catalog
/** The service name this instance is registered under. */
public name!: string
```
The service name this instance is registered under.
[Source](../../../vendor/cordis/src/service.ts#L30)
## Static members
### Service.init
```ts cordis-catalog
/** Symbol key of an instance method run after construction (class plugins). */
static readonly init: unique symbol
```
Symbol key of an instance method run after construction (class plugins).
[Source](../../../vendor/cordis/src/service.ts#L13)
### Service.check
```ts cordis-catalog
/** Symbol key of the availability predicate passed to `ctx.provide()`. */
static readonly check: unique symbol
```
Symbol key of the availability predicate passed to `ctx.provide()`.
[Source](../../../vendor/cordis/src/service.ts#L15)
### Service.config
```ts cordis-catalog
/** Symbol key of the phantom intercept-config type parameter. */
static readonly config: unique symbol
```
Symbol key of the phantom intercept-config type parameter.
[Source](../../../vendor/cordis/src/service.ts#L17)
### Service.invoke
```ts cordis-catalog
/** Symbol key of the call body making a service callable (e.g. `ctx.logger()`). */
static readonly invoke: unique symbol
```
Symbol key of the call body making a service callable (e.g. `ctx.logger()`).
[Source](../../../vendor/cordis/src/service.ts#L19)
### Service.extend
```ts cordis-catalog
/** Symbol key of the helper deriving an extended service instance. */
static readonly extend: unique symbol
```
Symbol key of the helper deriving an extended service instance.
[Source](../../../vendor/cordis/src/service.ts#L21)
### Service.tracker
```ts cordis-catalog
/** Symbol key of the tracker metadata used for context tracing. */
static readonly tracker: unique symbol
```
Symbol key of the tracker metadata used for context tracing.
[Source](../../../vendor/cordis/src/service.ts#L23)
### Service.resolveConfig
```ts cordis-catalog
/** Symbol key of the intercept-config resolution helper below. */
static readonly resolveConfig: unique symbol
```
Symbol key of the intercept-config resolution helper below.
[Source](../../../vendor/cordis/src/service.ts#L25)

View File

@@ -7,7 +7,7 @@ Every cordis event a plugin can listen to: exact signature, dispatch mode, and o
This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them.
The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely.
The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. The event-dispatch methods themselves are generated in the [Cordis core Events API](core/events.md).
Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`).

View File

@@ -7,7 +7,7 @@ Every `ctx.<key>` service a plugin can call: the exact public interface with ori
This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them.
The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely.
The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely. Detailed Context, Fiber, Registry, and Service APIs are generated in the [Cordis core API](core/context.md).
## `ctx.agentLoop` — `AgentLoop`

View File

@@ -0,0 +1,49 @@
/** Tests for the generated Cordis core API reference. */
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
CORDIS_CORE_API_PAGES,
renderCordisCoreApiPage,
renderCordisCoreApiPages,
type CordisCoreApiPage,
} from './cordis-core-api.ts'
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
describe('Cordis core API generation', () => {
it('renders the five detailed pages from pinned vendor declarations', () => {
const pages = renderCordisCoreApiPages()
expect([...pages.keys()]).toEqual(CORDIS_CORE_API_PAGES.map(page => page.out))
expect(pages.get('docs/cordis-catalog/core/context.md')).toContain('### ctx.extend(meta?)')
expect(pages.get('docs/cordis-catalog/core/events.md')).toContain('## DispatchMode')
expect(pages.get('docs/cordis-catalog/core/fiber.md')).toContain('## EffectMeta')
expect(pages.get('docs/cordis-catalog/core/registry.md')).toContain('## Plugin')
expect(pages.get('docs/cordis-catalog/core/service.md')).toContain('### Service.resolveConfig')
const fiber = pages.get('docs/cordis-catalog/core/fiber.md') ?? ''
expect(fiber).toContain('```\n\nRegister a cleanup-aware effect on this fiber.')
expect(fiber).toContain('- `execute` — the effect body; see `Effect` for accepted shapes.')
expect(fiber).toContain('**Returns** a disposer that tears the effect down and settles once done.')
})
it('rejects a public core class without source JSDoc', () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-cordis-core-api-'))
roots.push(root)
mkdirSync(join(root, 'vendor/cordis/src'), { recursive: true })
writeFileSync(join(root, 'vendor/cordis/src/service.ts'), 'export class Service {\n run(): string { return "ok" }\n}\n')
const page: CordisCoreApiPage = {
out: 'docs/cordis-catalog/core/service.md',
title: 'Service',
intro: 'Service API.',
sections: [{ kind: 'class', file: 'vendor/cordis/src/service.ts', symbol: 'Service' }],
}
expect(() => renderCordisCoreApiPage(page, root)).toThrow('class Service')
})
})

433
scripts/cordis-core-api.ts Normal file
View File

@@ -0,0 +1,433 @@
/** Generate detailed Cordis core API pages from pinned vendor declarations. */
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import ts from 'typescript'
import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc, reportViolations } from './jsdoc.ts'
import { cordisModuleBody } from './cordis-walk.ts'
const root = resolve(import.meta.dirname, '..')
const FENCE = 'ts cordis-catalog'
/** One declaration group rendered on a Cordis core API page. */
type CordisCoreApiSection =
| { kind: 'class'; file: string; symbol: string; prefix?: string; heading?: string }
| { kind: 'context-merge'; file: string; heading?: string }
| { kind: 'decl'; file: string; symbol: string }
/** One generated Cordis core API page. */
export interface CordisCoreApiPage {
out: string
title: string
intro: string
sections: CordisCoreApiSection[]
}
/** Explicit editorial grouping for the pinned Cordis core surface. */
export const CORDIS_CORE_API_PAGES: CordisCoreApiPage[] = [
{
out: 'docs/cordis-catalog/core/context.md',
title: 'Context',
intro: 'The context is the core Cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods are documented on [Events](events.md), effects and the current fiber on [Fiber](fiber.md), and plugin loading on [Registry](registry.md).',
sections: [
{ kind: 'class', file: 'vendor/cordis/src/context.ts', symbol: 'Context', prefix: 'ctx.' },
{ kind: 'context-merge', file: 'vendor/cordis/src/reflect.ts', heading: 'Service store and mixins' },
],
},
{
out: 'docs/cordis-catalog/core/events.md',
title: 'Events',
intro: 'The event-dispatch API mixed into every context. Harness event declarations and their dispatch modes are generated separately in the [Cordis events catalog](../events.md).',
sections: [
{ kind: 'context-merge', file: 'vendor/cordis/src/events.ts' },
{ kind: 'decl', file: 'vendor/cordis/src/events.ts', symbol: 'EventOptions' },
{ kind: 'decl', file: 'vendor/cordis/src/events.ts', symbol: 'DispatchMode' },
],
},
{
out: 'docs/cordis-catalog/core/fiber.md',
title: 'Fiber',
intro: 'A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber, and `ctx.effect()` delegates to it.',
sections: [
{ kind: 'context-merge', file: 'vendor/cordis/src/fiber.ts' },
{ kind: 'class', file: 'vendor/cordis/src/fiber.ts', symbol: 'Fiber', heading: 'The Fiber class' },
{ kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'Effect' },
{ kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'Disposable' },
{ kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'EffectMeta' },
{ kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'CordisError' },
{ kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'ValidationError' },
],
},
{
out: 'docs/cordis-catalog/core/registry.md',
title: 'Registry',
intro: 'Plugin loading and dependency injection.',
sections: [
{ kind: 'context-merge', file: 'vendor/cordis/src/registry.ts' },
{ kind: 'decl', file: 'vendor/cordis/src/registry.ts', symbol: 'Plugin' },
{ kind: 'decl', file: 'vendor/cordis/src/registry.ts', symbol: 'Inject' },
],
},
{
out: 'docs/cordis-catalog/core/service.md',
title: 'Service',
intro: 'The base class for context services. A subclass loaded as a plugin registers itself as `ctx.<name>`.',
sections: [
{ kind: 'class', file: 'vendor/cordis/src/service.ts', symbol: 'Service' },
],
},
]
interface MemberDoc {
name: string
heading: string
signatures: string[]
jsDoc: string
doc: string
params: { name: string; text: string }[]
returns: string | null
source: string
}
interface RenderContext {
scanRoot: string
cache: Map<string, { sf: ts.SourceFile; text: string }>
violations: string[]
}
function load(ctx: RenderContext, rel: string): { sf: ts.SourceFile; text: string } {
const cached = ctx.cache.get(rel)
if (cached !== undefined) return cached
const text = readFileSync(resolve(ctx.scanRoot, rel), 'utf8')
const entry = { sf: ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true), text }
ctx.cache.set(rel, entry)
return entry
}
function sourceJsDoc(text: string, sf: ts.SourceFile, node: ts.Node): string {
const raw = rawJsDoc(text, node)
if (raw === '') return ''
const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf))
const lineStart = sf.getPositionOfLineAndCharacter(line, 0)
const indent = text.slice(lineStart, node.getStart(sf))
return raw.split('\n')
.map((sourceLine, index) => index > 0 && sourceLine.startsWith(indent)
? sourceLine.slice(indent.length)
: sourceLine)
.join('\n')
}
function signatureOf(member: ts.Node, sf: ts.SourceFile): string {
const full = member.getText(sf)
const tail = (member as { body?: ts.Node; initializer?: ts.Node }).body
?? (member as { initializer?: ts.Node }).initializer
const signature = tail
? full.slice(0, full.length - tail.getText(sf).length).replace(/[=\s]+$/, '')
: full
return signature.replace(/\s*;?\s*$/, '').replace(/\s+/g, ' ').trim()
}
function headingParams(parameters: readonly ts.ParameterDeclaration[], sf: ts.SourceFile): string {
const names = parameters
.filter(parameter => !(ts.isIdentifier(parameter.name) && parameter.name.text === 'this'))
.map((parameter) => {
const rest = parameter.dotDotDotToken ? '...' : ''
const optional = parameter.questionToken || parameter.initializer ? '?' : ''
return `${rest}${parameter.name.getText(sf)}${optional}`
})
return `(${names.join(', ')})`
}
function isPublicInstance(member: ts.ClassElement): boolean {
const modifiers = ts.getCombinedModifierFlags(member)
if (modifiers & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected | ts.ModifierFlags.Static)) return false
if (!member.name || ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) return false
return !member.name.getText().startsWith('_')
}
function isPublicStatic(member: ts.ClassElement): boolean {
const modifiers = ts.getCombinedModifierFlags(member)
if (modifiers & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) return false
if (!(modifiers & ts.ModifierFlags.Static)) return false
if (!member.name || ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) return false
return !member.name.getText().startsWith('_')
}
type Member = ts.MethodDeclaration
| ts.MethodSignature
| ts.PropertyDeclaration
| ts.PropertySignature
| ts.GetAccessorDeclaration
function memberDoc(ctx: RenderContext, where: string, name: string, group: Member[], rel: string): MemberDoc {
const { sf, text } = load(ctx, rel)
const first = group[0]
if (first === undefined) throw new Error(`cordis-core-api: empty member group for ${name}.`)
const rawDocs = group.map(member => sourceJsDoc(text, sf, member))
const docIndex = rawDocs.findIndex(raw => parseJsDoc(raw).doc !== '')
const raw = docIndex === -1 ? '' : (rawDocs[docIndex] ?? '')
const doc = parseJsDoc(raw).doc
if (doc === '') ctx.violations.push(`${where} has no JSDoc prose.`)
const { params: tags, returns } = parseTags(raw)
const functionMembers = group.filter((member): member is ts.MethodDeclaration | ts.MethodSignature =>
ts.isMethodDeclaration(member) || ts.isMethodSignature(member))
const docCarrier = functionMembers[docIndex === -1 ? 0 : docIndex]
const params: { name: string; text: string }[] = []
if (docCarrier !== undefined) {
checkParams(where, 'cordis-core-api', docCarrier.parameters, tags, sf,
parameter => ts.isIdentifier(parameter.name) && parameter.name.text === 'this', ctx.violations)
if (docCarrier.type !== undefined) {
checkReturns(where, docCarrier.type, returns, sf, ctx.violations)
} else if (returns === null && ts.isMethodDeclaration(docCarrier)) {
ctx.violations.push(`${where} has no return type annotation; document the result with @returns.`)
}
for (const parameter of docCarrier.parameters) {
if (!ts.isIdentifier(parameter.name) || parameter.name.text === 'this') continue
const text = tags.get(parameter.name.text)
if (text !== undefined) params.push({ name: parameter.name.text, text })
}
}
const headingSource = docCarrier ?? functionMembers[0]
const signatures = ts.isMethodDeclaration(first) && functionMembers.length > 1
? functionMembers.filter(member => ts.isMethodDeclaration(member) && member.body === undefined)
: group
return {
name,
heading: headingSource === undefined ? '' : headingParams(headingSource.parameters, sf),
signatures: signatures.map(member => signatureOf(member, sf)),
jsDoc: raw,
doc,
params,
returns,
source: pointer(rel, sf, first),
}
}
function heritageMembers(
statement: ts.InterfaceDeclaration,
sf: ts.SourceFile,
groups: Map<string, (ts.MethodSignature | ts.PropertySignature | ts.MethodDeclaration)[]>,
): void {
for (const clause of statement.heritageClauses ?? []) {
for (const type of clause.types) {
if (!ts.isIdentifier(type.expression) || type.expression.text !== 'Pick') continue
const [target, keys] = type.typeArguments ?? []
if (target === undefined || keys === undefined || !ts.isTypeReferenceNode(target)) continue
const targetName = target.typeName.getText(sf)
const cls = sf.statements.find(
(entry): entry is ts.ClassDeclaration => ts.isClassDeclaration(entry) && entry.name?.text === targetName,
)
if (cls === undefined) continue
const picked = new Set<string>()
const collect = (node: ts.TypeNode): void => {
if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) picked.add(node.literal.text)
if (ts.isUnionTypeNode(node)) node.types.forEach(collect)
}
collect(keys)
for (const member of cls.members) {
if (!ts.isMethodDeclaration(member)) continue
const name = member.name.getText(sf)
if (!picked.has(name)) continue
const group = groups.get(name) ?? []
group.push(member)
groups.set(name, group)
}
}
}
}
function contextMergeMembers(ctx: RenderContext, rel: string): MemberDoc[] {
const { sf } = load(ctx, rel)
const body = cordisModuleBody(sf)
if (body === null) throw new Error(`cordis-core-api: ${rel} has no Context module merge.`)
const groups = new Map<string, (ts.MethodSignature | ts.PropertySignature | ts.MethodDeclaration)[]>()
for (const statement of body.statements) {
if (!ts.isInterfaceDeclaration(statement) || statement.name.text !== 'Context') continue
heritageMembers(statement, sf, groups)
for (const member of statement.members) {
if (!ts.isMethodSignature(member) && !ts.isPropertySignature(member)) continue
if (ts.isComputedPropertyName(member.name)) continue
const name = member.name.getText(sf)
const group = groups.get(name) ?? []
group.push(member)
groups.set(name, group)
}
}
return [...groups.entries()].map(([name, group]) =>
memberDoc(ctx, `ctx.${name} (${rel})`, name, group, rel))
}
function classMembers(ctx: RenderContext, rel: string, className: string): {
doc: string
instance: MemberDoc[]
statics: MemberDoc[]
source: string
} {
const { sf, text } = load(ctx, rel)
const cls = sf.statements.find(
(statement): statement is ts.ClassDeclaration =>
ts.isClassDeclaration(statement) && statement.name?.text === className,
)
if (cls === undefined) throw new Error(`cordis-core-api: class ${className} not found in ${rel}.`)
const doc = parseJsDoc(rawJsDoc(text, cls)).doc
if (doc === '') ctx.violations.push(`class ${className} (${pointer(rel, sf, cls)}) has no JSDoc.`)
const instance = new Map<string, Member[]>()
const statics = new Map<string, Member[]>()
for (const member of cls.members) {
if (!ts.isMethodDeclaration(member) && !ts.isPropertyDeclaration(member) && !ts.isGetAccessorDeclaration(member)) continue
const name = member.name.getText(sf)
if (isPublicInstance(member)) {
const group = instance.get(name) ?? []
group.push(member)
instance.set(name, group)
} else if (isPublicStatic(member) && !ts.isGetAccessorDeclaration(member)) {
const group = statics.get(name) ?? []
group.push(member)
statics.set(name, group)
}
}
const declaration = sf.statements.find(
(statement): statement is ts.InterfaceDeclaration =>
ts.isInterfaceDeclaration(statement) && statement.name.text === className,
)
for (const member of declaration?.members ?? []) {
if (!ts.isPropertySignature(member) || ts.isComputedPropertyName(member.name)) continue
const name = member.name.getText(sf)
const group = instance.get(name) ?? []
group.push(member)
instance.set(name, group)
}
const render = (groups: Map<string, Member[]>, prefix: string): MemberDoc[] =>
[...groups.entries()].map(([name, group]) => memberDoc(ctx, `${prefix}${name} (${rel})`, name, group, rel))
return {
doc,
instance: render(instance, `${className}#`),
statics: render(statics, `${className}.`),
source: pointer(rel, sf, cls),
}
}
function stripBodies(node: ts.Node, sf: ts.SourceFile): string {
const cuts: { start: number; end: number }[] = []
const visit = (entry: ts.Node): void => {
const functionLike = ts.isMethodDeclaration(entry)
|| ts.isConstructorDeclaration(entry)
|| ts.isFunctionDeclaration(entry)
|| ts.isGetAccessorDeclaration(entry)
|| ts.isSetAccessorDeclaration(entry)
if (functionLike && entry.body !== undefined) {
const signatureEnd = (entry.type ?? entry.parameters.at(-1) ?? entry).getEnd()
cuts.push({ start: signatureEnd, end: entry.body.getEnd() })
return
}
entry.forEachChild(visit)
}
visit(node)
const base = node.getStart(sf)
let output = node.getText(sf)
for (const cut of cuts.sort((left, right) => right.start - left.start)) {
const head = output.slice(0, cut.start - base)
const between = output.slice(cut.start - base, cut.end - base)
const bodyBrace = between.indexOf('{')
output = head + between.slice(0, bodyBrace).trimEnd() + output.slice(cut.end - base)
}
return output
}
function declarationPaste(ctx: RenderContext, rel: string, symbol: string): { doc: string; code: string; source: string } {
const { sf, text } = load(ctx, rel)
const matches = sf.statements.filter((statement) => {
const named = ts.isInterfaceDeclaration(statement)
|| ts.isTypeAliasDeclaration(statement)
|| ts.isClassDeclaration(statement)
|| ts.isEnumDeclaration(statement)
|| ts.isModuleDeclaration(statement)
return named && statement.name?.getText(sf) === symbol
})
const first = matches[0]
if (first === undefined) throw new Error(`cordis-core-api: declaration ${symbol} not found in ${rel}.`)
const doc = parseJsDoc(sourceJsDoc(text, sf, first)).doc
const code = matches.map((statement) => {
const jsDoc = sourceJsDoc(text, sf, statement)
const declaration = stripBodies(statement, sf).replace(/^export\s+(default\s+)?/, '')
return jsDoc === '' ? declaration : `${jsDoc}\n${declaration}`
}).join('\n\n')
return { doc, code, source: pointer(rel, sf, first) }
}
function sourceLink(source: string): string {
const [file, line] = source.split(':')
return `[Source](../../../${file}${line === undefined ? '' : `#L${line}`})`
}
function unlink(text: string): string {
return text.replace(/\{@link\s+([^}|\s]+)\s*(?:[|\s]\s*([^}]*))?\}/g, (_match, target: string, label?: string) => {
const name = label?.trim()
return name && name !== '' ? name : `\`${target}\``
})
}
function prose(doc: string): string[] {
const paragraphs = unlink(doc)
.split(/\n\s*\n/)
.map(paragraph => paragraph.replace(/\s*\n\s*/g, ' ').trim())
.filter(paragraph => paragraph !== '')
return paragraphs.flatMap((paragraph, index) => index === 0 ? [paragraph] : ['', paragraph])
}
function renderMember(prefix: string, member: MemberDoc): string[] {
const lines = [`### ${prefix}${member.name}${member.heading}`, '', `\`\`\`${FENCE}`]
if (member.jsDoc !== '') lines.push(member.jsDoc)
lines.push(...member.signatures, '```', '')
if (member.doc !== '') lines.push(...prose(member.doc), '')
for (const parameter of member.params) lines.push(`- \`${parameter.name}\`${unlink(parameter.text)}`)
if (member.params.length > 0) lines.push('')
if (member.returns !== null && member.returns !== '') lines.push(`**Returns** ${unlink(member.returns)}`, '')
lines.push(sourceLink(member.source), '')
return lines
}
/** Render one detailed Cordis core API page and reject undocumented members. */
export function renderCordisCoreApiPage(
page: CordisCoreApiPage,
scanRoot: string = root,
): string {
const ctx: RenderContext = { scanRoot, cache: new Map(), violations: [] }
const lines = [
'<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.',
' Run `pnpm run gen-cordis-catalog` to regenerate. -->',
'',
`# ${page.title}`,
'',
page.intro,
'',
]
for (const section of page.sections) {
if (section.kind !== 'decl' && section.heading !== undefined) lines.push(`## ${section.heading}`, '')
if (section.kind === 'context-merge') {
for (const member of contextMergeMembers(ctx, section.file)) lines.push(...renderMember('ctx.', member))
} else if (section.kind === 'class') {
const cls = classMembers(ctx, section.file, section.symbol)
if (cls.doc !== '') lines.push(...prose(cls.doc), '')
lines.push(sourceLink(cls.source), '')
const prefix = section.prefix ?? `${section.symbol.toLowerCase()}.`
for (const member of cls.instance) lines.push(...renderMember(prefix, member))
if (cls.statics.length > 0) {
lines.push('## Static members', '')
for (const member of cls.statics) lines.push(...renderMember(`${section.symbol}.`, member))
}
} else {
const declaration = declarationPaste(ctx, section.file, section.symbol)
lines.push(`## ${section.symbol}`, '')
if (declaration.doc !== '') lines.push(...prose(declaration.doc), '')
lines.push(`\`\`\`${FENCE}`, declaration.code, '```', '', sourceLink(declaration.source), '')
}
}
reportViolations('gen-cordis-catalog', ctx.violations)
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`
}
/** Render every detailed Cordis core API page. */
export function renderCordisCoreApiPages(scanRoot: string = root): Map<string, string> {
return new Map(CORDIS_CORE_API_PAGES.map(page => [page.out, renderCordisCoreApiPage(page, scanRoot)]))
}

View File

@@ -5,9 +5,10 @@
* curated table below. `--check` verifies both committed artifacts.
*/
import { globSync, readFileSync, writeFileSync } from 'node:fs'
import { resolve, sep } from 'node:path'
import { globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve, sep } from 'node:path'
import ts from 'typescript'
import { renderCordisCoreApiPages } from './cordis-core-api.ts'
import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc, reportViolations, type Mode } from './jsdoc.ts'
import { cordisModuleBody, eventMembers, serviceClasses } from './cordis-walk.ts'
@@ -503,7 +504,7 @@ export function renderEvents(events: EventEntry[]): string {
'',
GATE_NOTICE,
'',
'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely.',
'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. The event-dispatch methods themselves are generated in the [Cordis core Events API](core/events.md).',
'',
'Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`).',
'',
@@ -538,7 +539,7 @@ export function renderServices(services: ServiceEntry[]): string {
'',
GATE_NOTICE,
'',
'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely.',
'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely. Detailed Context, Fiber, Registry, and Service APIs are generated in the [Cordis core API](core/context.md).',
'',
]
for (const s of services) lines.push(...renderService(s))
@@ -562,6 +563,7 @@ function main(): void {
const outputs: [string, string][] = [
[OUT_EVENTS, renderEvents(collectEvents())],
[OUT_SERVICES, renderServices(collectServices())],
...renderCordisCoreApiPages(),
]
if (process.argv.includes('--check')) {
const stale: string[] = []
@@ -578,15 +580,19 @@ function main(): void {
if (committed !== content) stale.push(out)
}
if (stale.length === 0) {
console.log(`gen-cordis-catalog: ${OUT_EVENTS} and ${OUT_SERVICES} are up to date.`)
console.log(`gen-cordis-catalog: ${outputs.length} generated file(s) are up to date.`)
process.exit(0)
}
console.error(`gen-cordis-catalog: ${stale.join(' and ')} ${stale.length === 1 ? 'is' : 'are'} stale. Run \`pnpm run gen-cordis-catalog\` and commit the result.`)
process.exit(1)
}
for (const [out, content] of outputs) writeFileSync(resolve(root, out), content)
console.log(`gen-cordis-catalog: wrote ${OUT_EVENTS} and ${OUT_SERVICES}.`)
for (const [out, content] of outputs) {
const destination = resolve(root, out)
mkdirSync(dirname(destination), { recursive: true })
writeFileSync(destination, content)
}
console.log(`gen-cordis-catalog: wrote ${outputs.length} generated file(s).`)
}
// Run only when invoked as a script, not when imported by a test.

View File

@@ -160,6 +160,18 @@ describe('docsPages locale routes', () => {
}
}
})
it('publishes the Cordis core API under matching locale structures', () => {
const files = ['context.md', 'events.md', 'fiber.md', 'registry.md', 'service.md']
for (const file of files) {
const root = docsPages.find(page => page.route === `reference/cordis-api/${file}`)
const english = docsPages.find(page => page.route === `en/reference/cordis-api/${file}`)
expect(root?.source).toBe(`docs/cordis-catalog/core/${file}`)
expect(root?.section).toBe('Cordis API')
expect(english?.source).toBe(root?.source)
expect(english?.section).toBe('Cordis Core API')
}
})
})
describe('addProjectionFrontmatter', () => {

View File

@@ -15,6 +15,7 @@ const sectionOrder = [
'实战',
'概念',
'生成参考',
'Cordis API',
'数据结构',
'开发手册',
'Guide',
@@ -23,6 +24,7 @@ const sectionOrder = [
'Practice',
'Concepts',
'Generated reference',
'Cordis Core API',
'Data structures',
'Cookbook',
]

View File

@@ -237,6 +237,21 @@ const reference = mirroredPages([
section: { root: '生成参考', en: 'Generated reference' },
order,
})),
...([
['context.md', 'Context', 'Context'],
['events.md', 'Events', 'Events'],
['fiber.md', 'Fiber', 'Fiber'],
['registry.md', 'Plugin Registry', 'Plugin Registry'],
['service.md', 'Service', 'Service'],
] as const).map(([file, rootLabel, enLabel], order): MirroredPage => ({
source: `docs/cordis-catalog/core/${file}`,
route: `reference/cordis-api/${file}`,
contentLocale: 'en-US',
label: { root: rootLabel, en: enLabel },
sidebar: { root: 'zh-reference', en: 'en-reference' },
section: { root: 'Cordis API', en: 'Cordis Core API' },
order,
})),
...([
['core.md', '核心数据结构', 'Core data structures'],
['scope.md', '作用域', 'Scopes'],