docs: generated cordis events + services catalog

Add scripts/gen-cordis-catalog.ts: a fully-generated docs/cordis-catalog/
events-and-services.md cataloging every cordis event (exact signature + @mode)
and ctx.<key> service (exact interface), modeled on gen-module-graph's
--write/--check freshness gate. The harness tier renders in full from the
interface Events / interface Context declarations and their JSDoc; the inherited
cordis-core/loader/hmr/timer surface renders tersely from a curated table.

The generator hard-errors on a missing @mode tag and on a tag that contradicts
a conclusive signature shape (a trailing next param is structurally a
waterfall). Signature blocks use a ts cordis-catalog fence that doc-typecheck
skips. Type tokens cross-link to the core-data-structures catalog.

This supersedes the hand-maintained event-taxonomy table: verify-event-taxonomy
is deleted and verify-cordis-catalog joins doc-sync. architecture.md keeps the
Event taxonomy heading (TOC anchor) but points at the catalog; the Service-map
role table stays. RFC, AGENTS.md @mode authoring rule, and dependent doc/skill
references updated. Negative gate tests cover the missing-tag and
tag/shape-contradiction paths.
This commit is contained in:
Tianyi Cui
2026-06-20 19:47:09 +08:00
parent ee494969af
commit 4e5c08ef82
15 changed files with 1107 additions and 162 deletions

View File

@@ -56,6 +56,8 @@ Dependency rule: plugins depend on interface packages, never on `dsh-agent-loop`
All registrations (`registerAdapter`, `section`, `tools`, `register`, …) go through `ctx.effect()` and return disposers, so plugin hot-reload (vendored HMR) and fiber disposal clean up automatically.
For each service's full public interface (every method signature, generated from source), plus the inherited cordis-core/loader/hmr/timer surface a plugin also sees, see the `## Services` section of [cordis-catalog/events-and-services.md](cordis-catalog/events-and-services.md). This table is the at-a-glance role summary; that catalog is the exhaustive reference.
## Capability seams: interface / implementation / consumer
Swappable capabilities are split into **three packages** so each part evolves independently. The bash capability is the template:
@@ -167,26 +169,7 @@ A failure that happens once the turn is already closed has no in-turn position f
### Event taxonomy
The `agent/*` events are declared in `@deepseek-ai/dsh-agent` (so nothing depends on the loop package); each other service declares its own events (`tools/*`, `llm/*`, `system-prompt/*`, `session/*`). The table below is CI-verified against the `interface Events` declarations in source (`scripts/verify-event-taxonomy.ts`).
| Event | Mode | Purpose |
|---|---|---|
| `agent/created` / `agent/disposed` / `agent/status` / `agent/queued` | emit | lifecycle + inbox notifications |
| `agent/turn-start` / `agent/turn-end` / `agent/step-start` / `agent/step-end` | emit | boundaries |
| `agent/request` | **waterfall** | mutate the final `GenerateOptions` before the model call |
| `agent/stream-chunk` | emit | token-level UI/log feed |
| `agent/step-result` | **waterfall** | post-process the assistant message before tool dispatch |
| `agent/steering` | emit | steering content injected |
| `agent/turn-continuation` | **waterfall** | override the continue/stop decision |
| `agent/error` | emit | step/turn errors |
| `tools/execute` (dsh-tools) | **waterfall** | wrap/veto/sandbox tool execution |
| `tools/change` (dsh-tools) | emit | a tool was registered/unregistered |
| `llm/stream` / `llm/generate` (dsh-llm) | **waterfall** | model-call interception |
| `llm/adapter-change` (dsh-llm) | emit | an adapter was registered/unregistered |
| `system-prompt/assemble` (dsh-system-prompt) | **waterfall** | mutate the assembly |
| `system-prompt/change` (dsh-system-prompt) | emit | a section/tool-provider changed |
| `session/created` / `session/event` (dsh-session) | emit | session lifecycle + log feed |
| `session/flush` (dsh-session) | parallel (awaited) | durability checkpoint |
The `agent/*` events are declared in `@deepseek-ai/dsh-agent` (so nothing depends on the loop package); each other service declares its own events (`tools/*`, `llm/*`, `system-prompt/*`, `session/*`). The full catalog — every event's exact signature, dispatch mode, and prose — is **generated from source** and lives in [cordis-catalog/events-and-services.md](cordis-catalog/events-and-services.md) (the `## Events` section), alongside the `ctx.<key>` service interfaces. That file is regenerated by `scripts/gen-cordis-catalog.ts` and frozen by the `verify-cordis-catalog` freshness gate (part of `doc-sync`), so it cannot drift from the `interface Events` declarations.
### Cordis waterfall semantics (important)

View File

@@ -0,0 +1,482 @@
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
Run `pnpm run gen-cordis-catalog` to regenerate. -->
# Cordis Events & Services Catalog
An index reference to the **wiring** a plugin author works against: every cordis event you can listen to (exact signature + dispatch mode) and every `ctx.<key>` service you can call (exact public interface). It complements [core-data-structures/](../core-data-structures/core.md), which catalogs the *data structures* these signatures move around — this page is the verbs, that page is the nouns.
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 (skipped by doc-typecheck, since a bare signature is not standalone-compilable). 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 surface a plugin also sees — pinned vendor source, summarized tersely.
## Events
Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../architecture.md#cordis-waterfall-semantics-important)), **parallel** (awaited fan-out, no veto). The harness declares 24 events across 5 scopes.
### `agent/*`
#### `agent/created` — emit
An agent was registered in the AgentRegistry and is ready to receive messages.
```ts cordis-catalog
'agent/created'(agent: Agent): void
```
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/agent/src/types.ts:140`](../../packages/agent/src/types.ts)
#### `agent/disposed` — emit
An agent was disposed and removed from the registry; its fiber and any in-flight turn have been torn down.
```ts cordis-catalog
'agent/disposed'(agent: Agent): void
```
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/agent/src/types.ts:146`](../../packages/agent/src/types.ts)
#### `agent/error` — emit
A step or turn errored. The loop reports a failure here (plus the logger) even when the error has no in-turn position for a session `error` event.
```ts cordis-catalog
'agent/error'(agent: Agent, turn: number, step: number, error: Error): void
```
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/agent/src/types.ts:223`](../../packages/agent/src/types.ts)
#### `agent/queued` — emit
A message entered the agent's inbox (queued or steering). `source` is the resolved source (defaults applied), not the caller's raw options.
```ts cordis-catalog
'agent/queued'(agent: Agent, content: ContentBlock[], info: { source: MessageSource; steering: boolean }): void
```
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
Source: [`packages/agent/src/types.ts:159`](../../packages/agent/src/types.ts)
#### `agent/request` — waterfall
Waterfall: mutate the fully-assembled GenerateOptions before the model call (hooks, compaction, model switching, tool filtering, …). Call `next()` to delegate, or return without it to short-circuit.
```ts cordis-catalog
'agent/request'(agent: Agent, turn: number, step: number, options: GenerateOptions, next: () => Promise<GenerateOptions>): Promise<GenerateOptions>
```
Types: [Agent](../core-data-structures/core.md) · [GenerateOptions](../core-data-structures/core.md)
Source: [`packages/agent/src/types.ts:192`](../../packages/agent/src/types.ts)
#### `agent/status` — emit
Agent status changed (`idle` ⇄ `running`, or → `disposed`). Drive lifecycle off this transition, never off a status you just requested — `send()` does not flip status to `running` before it returns.
```ts cordis-catalog
'agent/status'(agent: Agent, status: AgentStatus): void
```
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/agent/src/types.ts:153`](../../packages/agent/src/types.ts)
#### `agent/steering` — emit
Steering content was injected into a running turn.
```ts cordis-catalog
'agent/steering'(agent: Agent, turn: number, content: ContentBlock[], source: MessageSource): void
```
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
Source: [`packages/agent/src/types.ts:217`](../../packages/agent/src/types.ts)
#### `agent/step-end` — emit
A step ended.
```ts cordis-catalog
'agent/step-end'(agent: Agent, turn: number, step: number): void
```
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/agent/src/types.ts:183`](../../packages/agent/src/types.ts)
#### `agent/step-result` — waterfall
Waterfall: post-process the assembled assistant Message before tool dispatch (validation, content rewriting, …).
```ts cordis-catalog
'agent/step-result'(agent: Agent, turn: number, step: number, message: Message, next: () => Promise<Message>): Promise<Message>
```
Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md)
Source: [`packages/agent/src/types.ts:198`](../../packages/agent/src/types.ts)
#### `agent/step-start` — emit
A step (one model call plus its tool dispatch) began. `step` is 1-based within the turn; a turn runs one or more steps.
```ts cordis-catalog
'agent/step-start'(agent: Agent, turn: number, step: number): void
```
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/agent/src/types.ts:178`](../../packages/agent/src/types.ts)
#### `agent/stream-chunk` — emit
A raw StreamChunk arrived from the model (token-level UI/log feed).
```ts cordis-catalog
'agent/stream-chunk'(agent: Agent, turn: number, step: number, chunk: StreamChunk): void
```
Types: [Agent](../core-data-structures/core.md) · [StreamChunk](../core-data-structures/llm-streaming.md)
Source: [`packages/agent/src/types.ts:212`](../../packages/agent/src/types.ts)
#### `agent/turn-continuation` — waterfall
Waterfall: override the turn-continuation decision. The default (computed by the loop) is `hadToolCalls || steeringInjected`. Listeners can force-continue (/goal, /loop) or force-stop (budget guards).
```ts cordis-catalog
'agent/turn-continuation'(agent: Agent, turn: number, defaultDecision: boolean, next: () => Promise<boolean>): Promise<boolean>
```
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/agent/src/types.ts:205`](../../packages/agent/src/types.ts)
#### `agent/turn-end` — emit
A turn ended. `reason` distinguishes a clean stop from a truncated or aborted one (`completed` | `aborted` | `error` | `disposed` | `max-tokens`).
```ts cordis-catalog
'agent/turn-end'(agent: Agent, turn: number, reason: TurnEndReason): void
```
Types: [Agent](../core-data-structures/core.md) · [TurnEndReason](../core-data-structures/session.md)
Source: [`packages/agent/src/types.ts:172`](../../packages/agent/src/types.ts)
#### `agent/turn-start` — emit
A turn began. `turn` is the 1-based turn number within the session.
```ts cordis-catalog
'agent/turn-start'(agent: Agent, turn: number): void
```
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/agent/src/types.ts:166`](../../packages/agent/src/types.ts)
### `llm/*`
#### `llm/adapter-change` — emit
An adapter was registered or unregistered (the model→adapter map changed).
```ts cordis-catalog
'llm/adapter-change'(): void
```
Source: [`packages/llm/src/index.ts:43`](../../packages/llm/src/index.ts)
#### `llm/generate` — waterfall
Waterfall around every non-streaming model call. Bound to the LlmService; call `next()` to delegate to the adapter.
```ts cordis-catalog
'llm/generate'(this: LlmService, options: GenerateOptions, next: () => Promise<GenerateResult>): Promise<GenerateResult>
```
Types: [GenerateOptions](../core-data-structures/core.md) · [GenerateResult](../core-data-structures/core.md)
Source: [`packages/llm/src/index.ts:38`](../../packages/llm/src/index.ts)
#### `llm/stream` — waterfall
Waterfall around every streaming model call (retry, caching, routing). Bound to the LlmService; call `next()` to reach the resolved adapter's stream, or yield your own chunks to short-circuit.
```ts cordis-catalog
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
```
Types: [GenerateOptions](../core-data-structures/core.md) · [StreamChunk](../core-data-structures/llm-streaming.md)
Source: [`packages/llm/src/index.ts:32`](../../packages/llm/src/index.ts)
### `session/*`
#### `session/created` — emit
A session was created in the store.
```ts cordis-catalog
'session/created'(session: Session): void
```
Source: [`packages/session/src/index.ts:30`](../../packages/session/src/index.ts)
#### `session/event` — emit
An event was appended to a session log (sync, fire-and-forget). This is the per-append feed a UI or invariant plugin tails.
```ts cordis-catalog
'session/event'(session: Session, event: SessionEvent): void
```
Types: [SessionEvent](../core-data-structures/core.md)
Source: [`packages/session/src/index.ts:36`](../../packages/session/src/index.ts)
#### `session/flush` — parallel
Awaited durability checkpoint. The agent loop awaits `ctx.parallel('session/flush', session)` at every turn end; persistence plugins (JSONL, SQLite) drain their write-behind buffers here and on fiber dispose. Awaited (parallel), not a waterfall: every listener runs and the loop waits for all of them, but none can veto.
```ts cordis-catalog
'session/flush'(session: Session): Promise<void> | void
```
Source: [`packages/session/src/index.ts:45`](../../packages/session/src/index.ts)
### `system-prompt/*`
#### `system-prompt/assemble` — waterfall
Waterfall around prompt assembly — mutate or extend the PromptAssembly (sections + tool schemas) before it is rendered. Bound to the SystemPrompt service; call `next()` to delegate.
```ts cordis-catalog
'system-prompt/assemble'(this: SystemPrompt, assembly: PromptAssembly, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
```
Source: [`packages/system-prompt/src/index.ts:24`](../../packages/system-prompt/src/index.ts)
#### `system-prompt/change` — emit
A section or tool provider was registered or unregistered (the assembly inputs changed).
```ts cordis-catalog
'system-prompt/change'(): void
```
Source: [`packages/system-prompt/src/index.ts:30`](../../packages/system-prompt/src/index.ts)
### `tools/*`
#### `tools/change` — emit
A tool was registered or unregistered (the available tool set changed).
```ts cordis-catalog
'tools/change'(): void
```
Source: [`packages/tools/src/index.ts:48`](../../packages/tools/src/index.ts)
#### `tools/execute` — waterfall
Waterfall around every tool execution — the single seam where sandbox, permission, hook, and plan-mode plugins wrap or veto a call. Listeners receive `(exec, next)`: call `next()` to proceed (possibly around your own logic), or return a ToolExecutionResult without calling `next()` to short-circuit (veto).
```ts cordis-catalog
'tools/execute'(this: ToolRegistry, exec: ToolExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
```
Types: [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md)
Source: [`packages/tools/src/index.ts:43`](../../packages/tools/src/index.ts)
## Services
The 8 `ctx.<key>` services the harness provides. An abstract seam (e.g. `ctx.bash`) is implemented by a separate package; the interface is what consumers code against.
### `ctx.agentLoop` — `AgentLoop`
The agent-loop plugin (`ctx.agentLoop`): creates ReactLoopAgents, runs their loops, and registers them in `ctx.agents`. Also implements the AgentFactory seam, so plugins create/resume agents through `ctx.agents` (the interface) without depending on this concrete package.
The loop itself is deliberately thin — every behavior beyond "call the model, run the tools, repeat" belongs to plugins listening on the event taxonomy declared in @deepseek-ai/dsh-agent.
```ts cordis-catalog
create(id: string, options: AgentOptions = {}): ReactLoopAgent
createAgent(options: CreateAgentOptions): AgentHandle
async resume(options: ResumeAgentOptions): Promise<AgentHandle>
```
Source: [`packages/agent-loop/src/index.ts:60`](../../packages/agent-loop/src/index.ts)
### `ctx.agents` — `AgentRegistry`
Agent registry (`ctx.agents`): tracks live agents so UI, hook, and orchestrator plugins can find them without depending on the concrete loop package. Agent *creation* is provided by whichever plugin implements the AgentFactory (phase 1: `@deepseek-ai/dsh-agent-loop`), registered via setFactory.
```ts cordis-catalog
setFactory(factory: AgentFactory): () => void
create(options: CreateAgentOptions): AgentHandle
async resume(options: ResumeAgentOptions): Promise<AgentHandle>
register(agent: Agent): () => void
get(id: string): Agent | undefined
list(): Agent[]
```
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/agent/src/index.ts:105`](../../packages/agent/src/index.ts)
### `ctx.bash` — `BashExecutor` (abstract seam)
Abstract bash execution service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.bash` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
Semantics every implementation must honor:
- run REJECTS only for infrastructure failures (unusable workdir, missing shell, pre-aborted signal). Nonzero exits, timeout kills, and abort kills RESOLVE with a descriptive BashRunResult — reporting a failed command is the tool layer's job, not an exception.
- start returns immediately; no timeout applies to background tasks (callers stop them via kill or the spec's AbortSignal). Completion must fire the onTaskDone listeners exactly once per task, and must NOT fire after the service is disposed.
- readOutput is incremental: consecutive reads never re-deliver output. Implementations bound their buffers; reads that lost data flag `lossy` and point at full-stream spill files when available.
- Disposal kills every running task and awaits their exit (no orphan processes survive `fiber.dispose()`).
```ts cordis-catalog
abstract resolve(request: BashExecRequest): BashExecSpec
abstract run(spec: BashExecSpec): Promise<BashRunResult>
abstract start(spec: BashExecSpec): BashTask
abstract get(id: string): BashTask | undefined
abstract ownerOf(id: string): string | undefined
abstract list(): BashTask[]
abstract readOutput(id: string): BashTaskRead
abstract kill(id: string): boolean
onTaskDone(listener: BashTaskListener): () => void
protected notifyTaskDone(task: BashTask): void
```
Types: [BashExecRequest](../core-data-structures/bash.md) · [BashExecSpec](../core-data-structures/bash.md) · [BashRunResult](../core-data-structures/bash.md) · [BashTask](../core-data-structures/bash.md)
Source: [`packages/bash/src/index.ts:58`](../../packages/bash/src/index.ts)
### `ctx.llm` — `LlmService`
The abstract `llm` service: an adapter registry plus streaming / non-streaming call surfaces, both interceptable via waterfall events.
```ts cordis-catalog
registerAdapter(models: string[], adapter: LlmAdapter): () => void
models(): string[]
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
async * streamBlocks(options: GenerateOptions): AsyncIterable<ContentBlock>
generate(options: GenerateOptions): Promise<GenerateResult>
```
Types: [ContentBlock](../core-data-structures/core.md) · [GenerateOptions](../core-data-structures/core.md) · [GenerateResult](../core-data-structures/core.md) · [StreamChunk](../core-data-structures/llm-streaming.md)
Source: [`packages/llm/src/index.ts:81`](../../packages/llm/src/index.ts)
### `ctx.sessionPersistence` — `SessionPersistence` (abstract seam)
Abstract durable session-persistence service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.sessionPersistence` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
Contracts every implementation MUST honor (a DB backend asserts them inside a transaction; a file backend appends at EOF):
- **Append-only; a crashed turn is closed, not truncated.** Committed events — those at or below a flushed `turn/end` — are never rewritten. A crash can leave an unclosed final turn whose events are real (and possibly large); load preserves them and closes the orphaned turn with synthetic boundary events (see load). Only a never-fully-written torn tail fragment is discarded.
- **Contiguous seq.** A persisted log is contiguous: `events[i].seq === i`. load rejects a parse error or a `seq` gap in the COMMITTED region (unloadable); append's first event `seq` MUST equal the backend's stored next-seq (after `load` has balanced any interrupted turn).
- **JSON-serializable data.** `SessionEventMap` is merge-extensible and `event.data` is typed only as `SessionEventMap[K]`, so append REJECTS non-JSON-serializable data with an error naming the offending event type. A backend snapshots (serializes/clones) each event when it buffers, since `session.events` hands out the live mutable object.
- **Durability.** append returns only once the batch is durable (the file backend fsyncs; a DB commits). create MAY defer the physical write until the first append (lazy materialization).
```ts cordis-catalog
abstract create(meta: SessionHeader): Promise<void>
abstract append(id: SessionId, events: readonly SessionEvent[]): Promise<void>
abstract load(id: SessionId): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
abstract list(): Promise<SessionHeader[]>
abstract has(id: SessionId): Promise<boolean>
abstract delete(id: SessionId): Promise<void>
```
Types: [SessionEvent](../core-data-structures/core.md)
Source: [`packages/session-persistence/src/index.ts:98`](../../packages/session-persistence/src/index.ts)
### `ctx.sessions` — `SessionStore`
In-memory session store (`ctx.sessions`).
Persistence is intentionally not implemented here — persistence plugins subscribe to `session/event` and flush on `session/flush` / dispose.
```ts cordis-catalog
create(id?: string, options?: CreateSessionOptions): Session
prepare(id?: string, options?: CreateSessionOptions): Session
enter(session: Session): () => void
announce(session: Session): void
get(id: string): Session | undefined
list(): Session[]
```
Source: [`packages/session/src/index.ts:222`](../../packages/session/src/index.ts)
### `ctx.systemPrompt` — `SystemPrompt`
Registry service (`ctx.systemPrompt`): plugins contribute ordered text sections and tool-schema providers; the agent loop calls `assemble()` once per step.
```ts cordis-catalog
section(section: PromptSection): () => void
tools(provider: () => ToolSchema[]): () => void
assemble(): Promise<PromptAssembly>
```
Source: [`packages/system-prompt/src/index.ts:71`](../../packages/system-prompt/src/index.ts)
### `ctx.tools` — `ToolRegistry`
Tool registry (`ctx.tools`): tool plugins register definitions; the agent loop executes calls through the `tools/execute` waterfall. The registry contributes its schemas into the system-prompt assembly.
```ts cordis-catalog
register(definition: ToolDefinition): () => void
get(name: string): ToolDefinition | undefined
schemas(): ToolSchema[]
async execute(exec: ToolExecution): Promise<ToolExecutionResult>
```
Types: [ToolDefinition](../core-data-structures/tools.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md)
Source: [`packages/tools/src/index.ts:277`](../../packages/tools/src/index.ts)
## Inherited tier (cordis core + loader/hmr/timer)
The framework surface every plugin inherits, beyond the harness vocabulary above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the catalog is a complete picture of what `ctx` and the event bus offer, without elevating framework internals to the harness tier's prominence.
### Inherited events
- `internal/plugin` — A plugin fiber was created. ([`vendor/cordis/src/events.ts:197`](../../vendor/cordis/src/events.ts))
- `internal/status` — A fiber changed lifecycle state. ([`vendor/cordis/src/events.ts:198`](../../vendor/cordis/src/events.ts))
- `internal/service` — Interception hook for a service binding (no core producer). ([`vendor/cordis/src/events.ts:199`](../../vendor/cordis/src/events.ts))
- `internal/update` — Waterfall: a fiber config update is being applied. ([`vendor/cordis/src/events.ts:200`](../../vendor/cordis/src/events.ts))
- `internal/get` — Waterfall: a service is being read from the store. ([`vendor/cordis/src/events.ts:201`](../../vendor/cordis/src/events.ts))
- `internal/set` — Waterfall: a service is being written to the store. ([`vendor/cordis/src/events.ts:202`](../../vendor/cordis/src/events.ts))
- `internal/listener` — A listener was registered. ([`vendor/cordis/src/events.ts:203`](../../vendor/cordis/src/events.ts))
- `internal/dispatch` — An event is being dispatched to listeners. ([`vendor/cordis/src/events.ts:204`](../../vendor/cordis/src/events.ts))
- `hmr/change` — A watched source file changed on disk. ([`vendor/hmr/src/index.ts:20`](../../vendor/hmr/src/index.ts))
- `hmr/reload` — Plugins are being reloaded after a change. ([`vendor/hmr/src/index.ts:21`](../../vendor/hmr/src/index.ts))
- `exit` — The process is exiting on a signal. ([`vendor/loader/src/index.ts:23`](../../vendor/loader/src/index.ts))
- `loader/config-update` — The loader config tree changed. ([`vendor/loader/src/index.ts:24`](../../vendor/loader/src/index.ts))
- `loader/entry-init` — A config entry is being initialized. ([`vendor/loader/src/index.ts:25`](../../vendor/loader/src/index.ts))
- `loader/partial-dispose` — An entry is being partially disposed on reload. ([`vendor/loader/src/index.ts:26`](../../vendor/loader/src/index.ts))
- `loader/patch-context` — A context is being patched during a reload. ([`vendor/loader/src/index.ts:27`](../../vendor/loader/src/index.ts))
### Inherited `ctx` members
- `ctx.on / ctx.once` — Register an event listener (disposable). ([`vendor/cordis/src/events.ts:29`](../../vendor/cordis/src/events.ts))
- `ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall` — Dispatch an event (sync / awaited / first-non-nullish / veto-chain). ([`vendor/cordis/src/events.ts:29`](../../vendor/cordis/src/events.ts))
- `ctx.plugin / ctx.inject` — Load a plugin / declare required services. ([`vendor/cordis/src/registry.ts:144`](../../vendor/cordis/src/registry.ts))
- `ctx.effect` — Register a disposable side effect tied to the fiber. ([`vendor/cordis/src/fiber.ts:9`](../../vendor/cordis/src/fiber.ts))
- `ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin` — Low-level service-store access and binding. ([`vendor/cordis/src/reflect.ts:7`](../../vendor/cordis/src/reflect.ts))
- `ctx.extend / ctx.isolate / ctx.intercept` — Derive a child context (scoped services / isolation / interception). ([`vendor/cordis/src/context.ts:35`](../../vendor/cordis/src/context.ts))
- `ctx.root / ctx.scope / ctx.fiber / ctx.registry / ctx.reflect / ctx.events / ctx.logger` — Ambient handles onto the running context graph. ([`vendor/cordis/src/context.ts:16`](../../vendor/cordis/src/context.ts))
- `ctx.timer (+ interval / timeout / throttle / debounce / setTimeout / setInterval)` — Disposable timer helpers. The `timer` key is provided at runtime; the six helpers are mixed onto ctx directly (declared via Pick). ([`vendor/timer/src/index.ts:4`](../../vendor/timer/src/index.ts))
- `ctx.loader` — The config Loader that booted the app (present under the loader). ([`vendor/loader/src/index.ts:30`](../../vendor/loader/src/index.ts))
- `ctx.hmr` — The hot-module-reload watcher (present under the hmr plugin). ([`vendor/hmr/src/index.ts:15`](../../vendor/hmr/src/index.ts))

View File

@@ -93,17 +93,18 @@ pnpm run typecheck # build declarations, then typecheck source, tests, and
pnpm run lint # eslint .
pnpm run lint:fix # eslint . --fix
pnpm run doc-typecheck # compile checked TypeScript snippets in Markdown docs
pnpm run verify-event-taxonomy # compare docs/architecture.md event names with source
pnpm run gen-cordis-catalog # regenerate docs/cordis-catalog/events-and-services.md from source
pnpm run verify-cordis-catalog # fail if the cordis events/services catalog is stale
pnpm run verify-md-wrap # fail on hard-wrapped prose paragraphs in docs/README markdown
pnpm run verify-type-equiv # fail if a ```ts type-equiv doc block drifts from its source type
pnpm run doc-sync # doc-typecheck, event taxonomy, markdown wrap/link, and type-equiv verification
pnpm run doc-sync # doc-typecheck, cordis-catalog freshness, markdown wrap/link, and type-equiv verification
pnpm run gen-module-graph # regenerate docs/module-graph.md from package peerDeps
pnpm run verify-module-graph # fail if docs/module-graph.md is stale
pnpm run build # build declarations and JS bundles
pnpm run hygiene # knip, publint, and workspace constraints
```
When changing package public behavior, update the relevant README or JSDoc in the same change. `pnpm run doc-sync` catches checked TypeScript snippets, event-taxonomy drift, and hard-wrapped markdown prose, but broader prose/API sync still needs review.
When changing package public behavior, update the relevant README or JSDoc in the same change. `pnpm run doc-sync` catches checked TypeScript snippets, cordis events/services catalog drift, and hard-wrapped markdown prose, but broader prose/API sync still needs review.
## Demos

View File

@@ -62,6 +62,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
| [Drop the mutable session summary](implemented/2026-06-19-drop-mutable-session-summary.md) | 2026-06-19 |
| [Shared persistence write coordinator](implemented/2026-06-18-shared-persistence-write-coordinator.md) | 2026-06-18 |
| [Agent lifecycle and ownership seams](implemented/2026-06-18-agent-lifecycle-and-ownership-seams.md) | 2026-06-18 |
| [Generated cordis events + services catalog](implemented/2026-06-20-generated-cordis-catalog.md) | 2026-06-20 |
## Rejected

View File

@@ -13,7 +13,7 @@ AGENTS.md promises that docs and code stay strictly in sync, but the promise was
Two gates, mirroring the existing `scripts/` style (tsx ESM, one job each):
1. **`doc-typecheck`** extracts every fenced ` ```ts ` block from `README.md`, `docs/**`, and `packages/*/README.md`, writes them to a temp project, and compiles with `tsc --noEmit`. The temp tsconfig copies only resolution-relevant options and the workspace `paths` map from `tsconfig.typecheck.json` (vendor → built `lib`, harness → `src`) — resolving vendor to `lib` is essential, or tsc type-checks raw vendor source and floods the run. A block that is a deliberate sketch opts out with an explicit ` ```ts ignore-check ` info string; the script reports the opt-out ratio and fails if it exceeds half, so the escape hatch can't quietly become the norm.
2. **`verify-event-taxonomy`** extracts the event names from the `interface Events` blocks across `packages/*/src` and from the taxonomy table in `docs/architecture.md`, and asserts the two sets match exactly. Verify, don't generate: the table keeps its hand-written Mode/Purpose columns; only the set of names is checked. (Landing this surfaced three events the table had been missing — `tools/change`, `llm/adapter-change`, `system-prompt/change`.)
2. **`verify-event-taxonomy`** extracts the event names from the `interface Events` blocks across `packages/*/src` and from the taxonomy table in `docs/architecture.md`, and asserts the two sets match exactly. Verify, don't generate: the table keeps its hand-written Mode/Purpose columns; only the set of names is checked. (Landing this surfaced three events the table had been missing — `tools/change`, `llm/adapter-change`, `system-prompt/change`.) **Superseded** by [the generated cordis catalog](2026-06-20-generated-cordis-catalog.md): this gate and its `architecture.md` table are retired in favor of a fully-generated `docs/cordis-catalog/events-and-services.md` and its `verify-cordis-catalog` freshness gate. The other gates here (`doc-typecheck`, and the `verify-md-wrap` amendment below) are unaffected.
Both run via a shared `doc-sync` package.json script that the lefthook pre-push hook and CI both invoke ([mechanical quality gates](2026-06-11-quality-gates.md): hooks and CI call the same scripts, so the gate fires locally before a push — not only after it). They run after `pnpm run typecheck` (which emits the vendor `lib/` that doc-typecheck resolves against). API-extractor golden reports ([the deferred API-extractor-reports proposal](../proposed/2026-06-11-api-extractor-reports.md)) were deliberately **deferred** — low value for an internal monorepo where reviewers already see the source diff, and a heavy, finicky dependency.

View File

@@ -0,0 +1,35 @@
# RFC: Generated cordis events + services catalog
Status: implemented (accepted 2026-06-20)
<!-- XXX: legacy ADR/RFC body format, not yet normalized to a unified RFC template. -->
## Context
A plugin author needs two reference surfaces that no single document gave them: every cordis **event** they can listen to (with its exact signature and dispatch mode) and every `ctx.<key>` **service** they can call (with its exact interface). The pieces existed but were scattered — a hand-maintained event-taxonomy *table* in `docs/architecture.md` (names + prose Mode/Purpose, name-set-checked by `verify-event-taxonomy`), a Service-map table (8 rows of role prose), and the `interface Events` / `interface Context` declarations themselves. The taxonomy table also could not catch a brand-new *undocumented* event: a name-set verifier only checks the names that are already in the table on both sides.
This is the wiring-axis complement to the [core-data-structures catalog](../../core-data-structures/core.md): that one catalogs the *data structures* the loop moves around (verified hand-pastes); this one catalogs the *events and services* that move them.
## Decision
Generate the catalog from source instead of hand-maintaining a table and verifying a subset.
`scripts/gen-cordis-catalog.ts` walks the `interface Events` and `interface Context` declarations (plus the service classes) with the TypeScript compiler API and emits `docs/cordis-catalog/events-and-services.md` — one `## Events` section (grouped by scope, each event rendered as signature + mode badge + its source JSDoc) and one `## Services` section (each `ctx.<key>` with its public method signatures + class JSDoc). It mirrors the `gen-module-graph` pattern exactly: `--write` regenerates, `--check` fails if the committed file is stale, output is deterministic (sorted), and the file is a build artifact that is never hand-edited. `verify-cordis-catalog` (the `--check`) runs inside `doc-sync`, so the freshness gate fires in the same lefthook pre-push and CI paths as every other doc gate.
Pure generation is correct here because the codebase is disciplined enough that the AST is the whole truth: every event/service name is a string literal that round-trips to a static declaration — there are no dynamically-named events and no runtime-only services. So a generated doc cannot be wrong, and it closes the undocumented-event gap structurally (generation enumerates source rather than checking a hand-written subset).
Specific choices:
- **`@mode` tag, cross-checked.** Each harness event's JSDoc carries an explicit `@mode emit|waterfall|parallel` tag; the generator hard-errors on a missing tag. Where the signature shape is conclusive — a trailing `next: () => …` parameter is structurally a waterfall — it asserts the tag agrees and hard-errors on a contradiction. The emit-vs-parallel distinction is not structurally visible (`session/flush` returns `Promise<void> | void` with no `next`), so it is trusted from the tag. The authoring rule lives in [AGENTS.md](../../../AGENTS.md).
- **Tiered scope.** The harness tier (the 8 `@deepseek-ai/dsh-*` services + their events) is rendered in full from source. The inherited tier (cordis-core `ctx.on/emit/effect/provide/…` + the `internal/*` events + loader/hmr/timer) is pinned vendor source a plugin also sees; it is rendered tersely (name + one-line + source pointer) from a curated table in the generator, NOT walked from the vendor AST — the cordis-core `Context` mixes true ctx members with non-service fields (`root`, `baseUrl`, `logger`), and the vendor surface changes only on a deliberate vendor sync.
- **Cross-links to the data-structure catalog.** A type name in a signature (`GenerateOptions`, `StreamChunk`, `ToolDefinition`, …) links to the core-data-structures page that documents it. The map is a small hand-curated const in the generator — NOT `type-equiv.manifest.json`, which documents the `…Map` symbols while signatures reference the derived union names, and lists a few symbols on two pages.
- **A dedicated fence.** Signature blocks use a ` ```ts cordis-catalog ` info string that `doc-typecheck` recognizes and skips (a bare signature fragment is not standalone-compilable), excluded from the opt-out ratio — the same treatment `type-equiv` blocks get.
This **supersedes the event-taxonomy half** of [doc-sync enforcement](2026-06-11-doc-sync-enforcement.md): `verify-event-taxonomy` and its `docs/architecture.md` table are retired (the architecture.md heading stays, its body now points at the catalog; the Service-map role table stays as curated prose). The verify-don't-generate principle that RFC chose for the taxonomy is reversed *for this surface only* — the data here is mechanically complete, so generation is strictly stronger (full signatures, cannot drift, catches undocumented events) than a name-set check of a hand-table. doc-typecheck, verify-md-wrap, verify-md-links, and verify-type-equiv are unchanged.
## Consequences
- The catalog cannot drift: a source change that the committed file doesn't reflect fails `verify-cordis-catalog` in the pre-push hook and CI. A new event with no `@mode` tag, or a tag that contradicts its signature, fails the generator outright.
- Event prose now has a single home — the JSDoc at the declaration. Thin JSDoc yields a thin catalog entry, which pressures authors to document at the source (the generator is a forcing function for the AGENTS.md "every export has a semantic JSDoc" rule).
- The inherited tier is hand-summarized, so a vendor sync that adds/renames a cordis-core event or `ctx` member needs a matching edit to the curated table in `gen-cordis-catalog.ts`. This is the deliberate cost of not walking pinned vendor source; it changes rarely and is called out in the generator.
- `verify-event-taxonomy.ts` is deleted and the `docs/architecture.md` event table is gone; anyone who linked to a specific table row now lands on the generated catalog instead.