From 44b035c711482016e91438a163234ecdb8efbfe6 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:11:18 +0800 Subject: [PATCH] docs: qualify plan-mode timing and workspace no-write claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ds-review-bot round 3: - set() after a turn's final step lands in a later turn (selection never forces continuation) — the same-turn phrasing overstated the guarantee; set() JSDoc + subsystem page (both languages). - Workspace attach/insert/detach no-op claims now carry the filtered-candidate-prune caveat: mutate() durably prunes stale candidates even when the requested change is a no-op, a state the sessionIds contract explicitly permits; Workspace fence re-synced on both page sides; catalogs regenerated. --- docs/AGENTS.md | 4 +- docs/cordis-catalog/events.md | 5 +- docs/cordis-catalog/services.md | 16 +- docs/subsystems/client-modules.i18n.yaml | 4 +- docs/subsystems/client-modules.md | 2 +- docs/subsystems/client-modules.zh.md | 2 +- docs/subsystems/core.i18n.yaml | 4 +- docs/subsystems/core.md | 1 - docs/subsystems/core.zh.md | 1 - docs/subsystems/http-server.i18n.yaml | 4 +- docs/subsystems/http-server.md | 10 +- docs/subsystems/http-server.zh.md | 8 +- docs/subsystems/permission.i18n.yaml | 4 +- docs/subsystems/permission.md | 15 +- docs/subsystems/permission.zh.md | 15 +- docs/subsystems/plan.i18n.yaml | 4 +- docs/subsystems/plan.md | 2 +- docs/subsystems/plan.zh.md | 2 +- docs/subsystems/telemetry.i18n.yaml | 4 +- docs/subsystems/telemetry.md | 15 +- docs/subsystems/telemetry.zh.md | 15 +- docs/subsystems/workspace.i18n.yaml | 4 +- docs/subsystems/workspace.md | 13 +- docs/subsystems/workspace.zh.md | 13 +- .../tests/cordis-catalog-contract.spec.ts | 4 +- packages/workspace/workspace/README.i18n.yaml | 2 +- packages/workspace/workspace/README.zh.md | 1 + packages/workspace/workspace/src/types.ts | 13 +- scripts/gen-cordis-catalog.ts | 492 +++--------------- scripts/type-equiv.manifest.json | 39 +- 30 files changed, 187 insertions(+), 531 deletions(-) diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 3bbd4f330d..488a7a1dd6 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -20,7 +20,7 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home. |---|---|---| | Root `AGENTS.md` | Standing orders: rules an agent needs in context in every session, one to three lines each, linking its home | Stories, worked examples, situational procedures, anything restated from a linked home | | Subtree `AGENTS.md` (`packages/`, `examples/`, `docs/`, `.agents/notes/`) | Orders specific to that subtree | Repo-wide rules the root file already carries | -| [architecture.md](architecture.md) | The system map: services, the loop, extension seams — read before changing `packages/` | Type shapes (→ core-data-structures), per-package detail (→ package READMEs), decision rationale (→ Agent Notes), implementation-status annotations | +| [architecture.md](architecture.md) | The system map: services, the loop, extension seams — read before changing `packages/` | Type shapes (→ subsystems), per-package detail (→ package READMEs), decision rationale (→ Agent Notes), implementation-status annotations | | [subsystems/](subsystems/core.md) | The type catalog: literal shapes and semantics of the spine and seam vocabulary | Behavior narration (→ architecture.md) | | [Agent Notes](../.agents/notes/README.md) | Decision records under their own lifecycle contract | Migration plans, checklists, and spec-speak once implemented; archived notes are frozen history | | [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — | @@ -37,7 +37,7 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home. - **Every non-trivial change includes at least one Agent Note in the same PR.** Update the owning note or add one; only mechanical/local edits are exempt ([scope](../.agents/notes/README.md#when-to-write-one)). - **One physical line per paragraph** (`verify-md-wrap`): use editor soft-wrap. Code blocks, tables, and list structure keep their formatting; code comments stay under the linter's column limit. - **Fenced `ts` blocks must compile** (`doc-typecheck`); a pasted type declaration and its original JSDoc use ` ```ts type-equiv `, while a body-stripped public class declaration uses ` ```ts public-api `; register either in the manifest so neither can drift ([mechanics](development.md#documenting-types-verbatim-ts-type-equiv)). -- **The [core-data-structures catalog](subsystems/core.md) updates in the same change** that reshapes a documented type. `verify-type-equiv` catches drifted pastes, not never-documented new types ([what counts as core](subsystems/core.md#what-counts-as-core)). +- **The [subsystems catalog](subsystems/core.md) updates in the same change** that reshapes a documented type. `verify-type-equiv` catches drifted pastes, not never-documented new types ([what counts as core](subsystems/core.md#what-counts-as-core)). - **Bilingual pairs update together**: editing either side obligates the counterpart and a re-record in the same change ([i18n contract](i18n/README.md)). - **Comments and JSDoc state complete contracts, not reasoning transcripts.** Preserve behavior, timing, modality, exceptions, consequences, and non-obvious orientation; delete narration, test walkthroughs, review analysis, and code restatement. Keep the local contract and link its rationale. Use [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for details. - Your audience is professional programmers. Prefer concise and straight-forward English over metaphor. Do not overuse words like "gate", "vocabulary", "surface", "seams". diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index 6d060cf26b..fb05eaa8e6 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -824,6 +824,8 @@ Transform one outbound record before it reaches the backend. This waterfall is t 'telemetry/record'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord ``` +Types: [TelemetryRecord](../subsystems/telemetry.md) + Source: [`packages/session/session-telemetry/src/index.ts:43`](../../packages/session/session-telemetry/src/index.ts) ## `tools/*` @@ -1090,8 +1092,7 @@ The framework events every plugin also sees, beyond the harness vocabulary above - `internal/listener` — A listener was registered. ([`vendor/cordis/src/events.ts:340`](../../vendor/cordis/src/events.ts)) - `internal/dispatch` — An event is being dispatched to listeners. ([`vendor/cordis/src/events.ts:342`](../../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:22`](../../vendor/hmr/src/index.ts)) -- `hmr/config-update-failed` — A watched config-file refresh failed. ([`vendor/hmr/src/index.ts:29`](../../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)) diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index dbab056105..88f1762cff 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -377,6 +377,8 @@ onRebuilt(listener: (id: string, rev: string) => void): () => void onGraphChanged(listener: () => void): () => void ``` +Types: [WebBootGraph](../subsystems/client-modules.md) + Source: [`packages/client/modules/src/index.ts:184`](../../packages/client/modules/src/index.ts) ## `ctx.codeRuntime` — `CodeRuntime` (abstract seam) @@ -866,6 +868,8 @@ tapIndex(transform: (html: string) => string): () => void applyIndexTaps(html: string): string ``` +Types: [WebRoute](../subsystems/http-server.md) + Source: [`packages/host/webserver/src/index.ts:60`](../../packages/host/webserver/src/index.ts) ## `ctx.invariants` — `InvariantService` @@ -884,6 +888,8 @@ Package-owned invariant registry with global and regex-based selection. register(packageName: string, installer: InvariantInstaller): () => void ``` +Types: [InvariantInstaller](../subsystems/invariants.md) + Source: [`packages/support/invariants/src/index.ts:94`](../../packages/support/invariants/src/index.ts) ## `ctx.llm` — `LlmService` @@ -1060,7 +1066,7 @@ optionOf(name: string): PresetOption set(session: Session, name: string): void ``` -Types: [Session](../subsystems/session.md) · [SessionEvent](../subsystems/core.md) +Types: [PresetOption](../subsystems/permission.md) · [PresetSpec](../subsystems/permission.md) · [Session](../subsystems/session.md) · [SessionEvent](../subsystems/core.md) Source: [`packages/interaction/permission/src/index.ts:159`](../../packages/interaction/permission/src/index.ts) @@ -2036,6 +2042,8 @@ mount(form: K, facility: StorageForms[K]): () => v form(form: K): StorageForms[K] ``` +Types: [StorageForms](../subsystems/storage.md) + Source: [`packages/storage/storage/src/index.ts:47`](../../packages/storage/storage/src/index.ts) ## `ctx.storageDomain` — `DomainFacility` @@ -2079,6 +2087,8 @@ get(name: string): DomainImpl | undefined async closeAll(): Promise ``` +Types: [Domain](../subsystems/storage.md) · [DomainSpec](../subsystems/storage.md) + Source: [`packages/storage/storage-domain/src/index.ts:69`](../../packages/storage/storage-domain/src/index.ts) ## `ctx.subagents` — `SubagentService` @@ -2473,6 +2483,8 @@ flush?(): void abstract shutdown(): Promise ``` +Types: [TelemetryRecord](../subsystems/telemetry.md) + Source: [`packages/session/session-telemetry/src/index.ts:140`](../../packages/session/session-telemetry/src/index.ts) ## `ctx.tokenMeter` — `TokenMeterService` @@ -2885,7 +2897,7 @@ archiveSession(sessionId: SessionId): Promise async resolveByPath(path: string): Promise ``` -Types: [SessionId](../subsystems/core.md) +Types: [SessionId](../subsystems/core.md) · [Workspace](../subsystems/workspace.md) · [WorkspaceId](../subsystems/workspace.md) Source: [`packages/workspace/workspace/src/index.ts:81`](../../packages/workspace/workspace/src/index.ts) diff --git a/docs/subsystems/client-modules.i18n.yaml b/docs/subsystems/client-modules.i18n.yaml index 8988609cdf..f0df40a866 100644 --- a/docs/subsystems/client-modules.i18n.yaml +++ b/docs/subsystems/client-modules.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/subsystems/client-modules.md -client-modules.md: bd5ecdc85ea26b040e1705e373ceac4e7cb17b3c -client-modules.zh.md: 5da7740e4c9272a3516f1a28712be7c95f29a6b3 +client-modules.md: 5dc89a87db7e79e4a297002fa059fdeffcceda20 +client-modules.zh.md: b3aa9c2aecf8e051a41b7b05f05d364b9ff8d2a6 diff --git a/docs/subsystems/client-modules.md b/docs/subsystems/client-modules.md index bd5ecdc85e..5dc89a87db 100644 --- a/docs/subsystems/client-modules.md +++ b/docs/subsystems/client-modules.md @@ -27,7 +27,7 @@ interface WebBootEntry { rev: string /** Package-name dependency edges, informational (preflight display / HMR diffing). */ inject?: string[] - /** Stage-one prefetch mark: fetch + execute (factory registration) during module-face boot. */ + /** Stage-one prefetch mark: load the script for factory registration during module-face boot. */ immediately?: boolean } ``` diff --git a/docs/subsystems/client-modules.zh.md b/docs/subsystems/client-modules.zh.md index 5da7740e4c..b3aa9c2aec 100644 --- a/docs/subsystems/client-modules.zh.md +++ b/docs/subsystems/client-modules.zh.md @@ -27,7 +27,7 @@ interface WebBootEntry { rev: string /** Package-name dependency edges, informational (preflight display / HMR diffing). */ inject?: string[] - /** Stage-one prefetch mark: fetch + execute (factory registration) during module-face boot. */ + /** Stage-one prefetch mark: load the script for factory registration during module-face boot. */ immediately?: boolean } ``` diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index 8bf98e026d..a2af261bc6 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/docs/subsystems/core.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/subsystems/core.md -core.md: 1d0bf6c61500817860dfdd452bddc25adbbeffcc -core.zh.md: 2b4766dcfcf1901de2bb8a8f9168888a67af7fba +core.md: 3a8ccbef4163eabba1cd287284eda68135f2db13 +core.zh.md: 0cd6a57971ee98c75b52dd45b6f648632dab8a90 diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index 1d0bf6c615..3a8ccbef41 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -51,7 +51,6 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t | [invariants.md](invariants.md) | the runtime-invariant registry: selection `Config`, `InvariantInstaller`/`InvariantFailure`, the empty-companion contract | | [http-server.md](http-server.md) | the HTTP carrier: `WebRouteKind`/`WebRoute`, match order, the static dist fallback, index taps | | [storage.md](storage.md) | the storage subsystem: the backend seam (`StorageBackend`), `StorageForms`, `DomainSpec`/`Domain`, `domain/changed` | -| [tui.md](tui.md) | the terminal-extension seam: `TuiOverlayRequest`/`Host`/`Session`, close reasons and outcomes, the modal queue | | [workspace.md](workspace.md) | the workspace registry: `Workspace`/`WorkspaceId`, registration and resolution, the session `cwd` relationship | | [client-modules.md](client-modules.md) | the web plugin table: `dshClient` declarations, `WebBootGraph` wire composition, the bundle route and index tap | | [telemetry.md](telemetry.md) | the outbound reporting seam: `TelemetryRecord`/`TelemetrySeverity`, the `TelemetryBackend` contract, the `telemetry/record` redact waterfall | diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index 2b4766dcfc..0cd6a57971 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -51,7 +51,6 @@ harness 是一个微内核:一个极小的核心加上众多插件。大多数 | [invariants.md](invariants.md) | 运行时不变式注册表:选择配置 `Config`、`InvariantInstaller`/`InvariantFailure`、空配套插件契约 | | [http-server.md](http-server.md) | HTTP 载体:`WebRouteKind`/`WebRoute`、匹配顺序、静态 dist 回退、index 转换 | | [storage.md](storage.md) | 存储子系统:后端 seam(`StorageBackend`)、`StorageForms`、`DomainSpec`/`Domain`、`domain/changed` | -| [tui.md](tui.md) | 终端扩展 seam:`TuiOverlayRequest`/`Host`/`Session`、关闭原因与结果、模态队列 | | [workspace.md](workspace.md) | 工作区注册表:`Workspace`/`WorkspaceId`、注册与解析、与会话 `cwd` 的关系 | | [client-modules.md](client-modules.md) | Web 插件表:`dshClient` 声明、`WebBootGraph` 线上组合、bundle 路由与 index 转换 | | [telemetry.md](telemetry.md) | 对外上报 seam:`TelemetryRecord`/`TelemetrySeverity`、`TelemetryBackend` 契约、`telemetry/record` 脱敏 waterfall | diff --git a/docs/subsystems/http-server.i18n.yaml b/docs/subsystems/http-server.i18n.yaml index 8369b94dac..6f1facfad7 100644 --- a/docs/subsystems/http-server.i18n.yaml +++ b/docs/subsystems/http-server.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/subsystems/http-server.md -http-server.md: 565df72f2720a1211f50650d4b1bc8d6df4a007a -http-server.zh.md: 69e4a0bd7a1576737583625b23fac2ed05d8ca61 +http-server.md: 200d3eed683f203b65824ea3f62cf56ce3df912f +http-server.zh.md: 9a12d3769a93270810927075be5f236875688b91 diff --git a/docs/subsystems/http-server.md b/docs/subsystems/http-server.md index 565df72f27..200d3eed68 100644 --- a/docs/subsystems/http-server.md +++ b/docs/subsystems/http-server.md @@ -2,7 +2,7 @@ English | [中文](http-server.zh.md) -[dsh-host-webserver](../../packages/host/webserver) is the web-shape HTTP carrier for the GUI host: a single `node:http` plugin providing `ctx.httpServer`, a named-route registry plus index.html transform taps over a static dist fallback. It is not part of the agent-loop spine and not a capability seam — it knows no harness concepts, and every feature surface (the `/api` bridge, plugin bundles, the HMR event stream) is a route some other plugin registers ([layering note](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)). Web (browser) shape only: Electron loads dist over `file://` and carries fetch over an IPC bridge, not this server. +[dsh-host-webserver](../../packages/host/webserver) is the web-shape HTTP carrier for the GUI host: a single `node:http` plugin providing `ctx.httpServer`, a named-route registry, index.html transform taps, and a single claimable fallback seat. It is not part of the agent-loop spine and not a capability seam — it knows no harness concepts, and every feature surface (the `/api` bridge, plugin bundles, the HMR event stream) is a route some other plugin registers ([layering note](../../.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md)). Web (browser) shape only: Electron loads dist over `file://` and carries fetch over an IPC bridge, not this server. Source: [`packages/host/webserver/src/index.ts`](../../packages/host/webserver/src/index.ts) @@ -24,23 +24,21 @@ interface WebRoute { } ``` -Match order is fixed: exact table first, then longest matching prefix, then the static dist fallback. Registration order carries no request-facing semantics — named routes are composed to be disjoint, and the fallback answers anything not yet claimed during the boot window. The fallback keeps locked semantics: non-GET/HEAD is 405, traversal outside the dist root is 403, any miss falls back to `index.html` with HTTP 200 (SPA routing), and unknown extensions ship as octet-stream ([`static.ts`](../../packages/host/webserver/src/static.ts)). +Match order is fixed: exact table first, then longest matching prefix, then the registered fallback. Registration order carries no request-facing semantics — named routes are composed to be disjoint, and the fallback seat answers anything no named route claims; one owner only, a second registration throws. The shipped Web composition claims the seat with [`dsh-frontend-static`](../../packages/host/frontend-static/src/index.ts), the SPA dist server with locked semantics: non-GET/HEAD is 405, traversal outside the dist root is 403, any miss falls back to `index.html` with HTTP 200 (SPA routing), and unknown extensions ship as octet-stream. ## Config ```ts type-equiv -/** Gateway config: listen address plus the static dist anchor (injected by the composing app, never self-resolved). */ +/** Gateway config: the listen address. */ interface Config { /** Listen host; the two supported values are loopback and all-interfaces. */ host: '127.0.0.1' | '0.0.0.0' /** Listen port; zero requests an OS-assigned port. */ port: number - /** Absolute path of index.html inside the static root (dist location is workspace knowledge of the app). */ - distIndex: string } ``` -`host` accepts only `127.0.0.1` (default posture) and `0.0.0.0` (deliberate network exposure); there is no TLS, auth, or origin policy, so a non-loopback bind exposes the server to that network. `distIndex` is an assembly fact the composing app resolves and injects. +`host` accepts only `127.0.0.1` (default posture) and `0.0.0.0` (deliberate network exposure); there is no TLS, auth, or origin policy, so a non-loopback bind exposes the server to that network. The dist location is an assembly fact of the frontend plugin that claims the seat. ## The service diff --git a/docs/subsystems/http-server.zh.md b/docs/subsystems/http-server.zh.md index 69e4a0bd7a..9a12d3769a 100644 --- a/docs/subsystems/http-server.zh.md +++ b/docs/subsystems/http-server.zh.md @@ -24,23 +24,21 @@ interface WebRoute { } ``` -匹配顺序固定:先查 exact 表,再取最长匹配前缀,最后落到静态 dist 回退。注册顺序不携带任何面向请求的语义:具名路由在组合上互不相交,启动窗口期内尚未被认领的请求全部由回退应答。回退遵循固定语义:非 GET/HEAD 返回 405,越出 dist 根目录的遍历返回 403,任何未命中都以 HTTP 200 回退到 `index.html`(SPA 路由),未知扩展名按 octet-stream 提供([`static.ts`](../../packages/host/webserver/src/static.ts))。 +匹配顺序固定:先查 exact 表,再取最长匹配前缀,最后落到已注册的回退。注册顺序不携带任何面向请求的语义:具名路由在组合上互不相交,任何未被具名路由认领的请求都由回退席位应答;席位只有一个所有者,第二次注册会抛出异常。发布的 Web 组合用 [`dsh-frontend-static`](../../packages/host/frontend-static/src/index.ts) 认领席位,即遵循固定语义的 SPA dist 服务器:非 GET/HEAD 返回 405,越出 dist 根目录的遍历返回 403,任何未命中都以 HTTP 200 回退到 `index.html`(SPA 路由),未知扩展名按 octet-stream 发送。 ## 配置 ```ts type-equiv -/** Gateway config: listen address plus the static dist anchor (injected by the composing app, never self-resolved). */ +/** Gateway config: the listen address. */ interface Config { /** Listen host; the two supported values are loopback and all-interfaces. */ host: '127.0.0.1' | '0.0.0.0' /** Listen port; zero requests an OS-assigned port. */ port: number - /** Absolute path of index.html inside the static root (dist location is workspace knowledge of the app). */ - distIndex: string } ``` -`host` 只接受 `127.0.0.1`(默认姿态)和 `0.0.0.0`(刻意的网络暴露);没有 TLS、认证或 origin 策略,因此绑定到非回环地址会把服务器暴露给该网络。`distIndex` 是组合应用解析后注入的组装事实。 +`host` 只接受 `127.0.0.1`(默认姿态)和 `0.0.0.0`(刻意的网络暴露);没有 TLS、认证或 origin 策略,因此绑定到非回环地址会把服务器暴露给该网络。dist 位置是认领席位的前端插件的组装事实。 ## 服务 diff --git a/docs/subsystems/permission.i18n.yaml b/docs/subsystems/permission.i18n.yaml index 284f29bb62..c63da5efdc 100644 --- a/docs/subsystems/permission.i18n.yaml +++ b/docs/subsystems/permission.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/subsystems/permission.md -permission.md: 49dd51ff86d0dbce26fca0ed21527f71bb144dec -permission.zh.md: c2ab6ecdfb4b80511107bee41e080b06fae45f2c +permission.md: ec8cd4a923b617934f1612c31284103266aea4ef +permission.zh.md: 4534c33e2715e4a500344f547c7895128877f4fb diff --git a/docs/subsystems/permission.md b/docs/subsystems/permission.md index 49dd51ff86..ec8cd4a923 100644 --- a/docs/subsystems/permission.md +++ b/docs/subsystems/permission.md @@ -2,9 +2,9 @@ English | [中文](permission.zh.md) -The permission-preset layer of [dsh-permission](../../packages/ui/permission) (`ctx.permission`, `PermissionService`) bundles the two independent enforcement knobs — [sandbox mode](sandbox.md) (`sandbox/mode`) and [approval policy](approval.md) (`approval/policy`) — into named presets a client offers as one Permissions selector. It is one optional capability, not part of the agent-loop spine, and it owns no enforcement: execution, prompt narration, and replay keep reading their knob folds, and a preset switch only records intent and writes through each knob's canonical setter. The [package README](../../packages/ui/permission/README.md) owns composition status and limitations; the [sandbox switching design](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns the rationale. +The permission-preset layer of [dsh-permission](../../packages/interaction/permission) (`ctx.permission`, `PermissionService`) bundles the two independent enforcement knobs — [sandbox mode](sandbox.md) (`sandbox/mode`) and [approval policy](approval.md) (`approval/policy`) — into named presets a client offers as one Permissions selector. It is one optional capability, not part of the agent-loop spine, and it owns no enforcement: execution, prompt narration, and replay keep reading their knob folds, and a preset switch only records intent and writes through each knob's canonical setter. The [package README](../../packages/interaction/permission/README.md) owns composition status and limitations; the [sandbox switching design](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns the rationale. -Source: [`packages/ui/permission/src/index.ts`](../../packages/ui/permission/src/index.ts) +Source: [`packages/interaction/permission/src/index.ts`](../../packages/interaction/permission/src/index.ts) ## The preset table @@ -25,7 +25,7 @@ interface PresetSpec { ``` ```ts type-equiv -/** The {@link PermissionService} config: the deployment's preset table. */ +/** The {@link PermissionService} config: preset table and composition default. */ interface Config { /** * The preset table: name → knob bundle. Defaults to `workspace-write` @@ -33,6 +33,11 @@ interface Config { * never). The name `custom` is reserved for the derived not-a-preset state. */ presets?: Record + /** + * Default for new sessions. When omitted, the preset matching the composed + * sandbox and approval defaults is used. + */ + defaultPreset?: string } ``` @@ -51,13 +56,13 @@ interface PresetOption { value: string /** The display label. */ name: string - /** One user-facing sentence on what the value means. */ + /** One user-facing sentence on what the value means; omitted when not configured. */ description?: string } ``` ## Switching and the `permission/preset` event -`set(session, name)` resolves the preset (unknown names throw), appends a log-only `permission/preset` event unless `name` is already the effective preset, then writes each knob through its own setter — `setSandboxMode` from [dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) and `setApprovalPolicy` from [dsh-user-approval](../../packages/ui/user-approval) — only when that knob's effective value changes. The selection event precedes the knob events in the same turn, and re-selecting the effective preset appends nothing at all. +`set(session, name)` resolves the preset (unknown names throw), appends a log-only `permission/preset` event unless `name` is already the effective preset, then writes each knob through its own setter — `setSandboxMode` from [dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) and `setApprovalPolicy` from [dsh-user-approval](../../packages/interaction/user-approval) — only when that knob's effective value changes. The selection event precedes the knob events in the same turn, and re-selecting the effective preset appends nothing at all. `permission/preset` is durable, log-only user intent: it stays out of the model transcript (the knob events own the model-visible consequences through their consumers), and it exists so `current()` can preserve WHICH preset the user chose when two presets share a bundle; `effectivePermissionPreset(events)` folds the last one, and replay needs no catch-up state. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md); the method signatures are in the generated [service catalog](../cordis-catalog/services.md#ctxpermission--permissionservice). diff --git a/docs/subsystems/permission.zh.md b/docs/subsystems/permission.zh.md index c2ab6ecdfb..4534c33e27 100644 --- a/docs/subsystems/permission.zh.md +++ b/docs/subsystems/permission.zh.md @@ -2,9 +2,9 @@ [English](permission.md) | 中文 -[dsh-permission](../../packages/ui/permission) 的权限预设层(`ctx.permission`,`PermissionService`)把两个相互独立的强制执行旋钮(knob),即[沙箱模式](sandbox.md)(`sandbox/mode`)与[审批策略](approval.md)(`approval/policy`),捆绑成具名预设,供客户端作为单个权限(Permissions)选择器提供。它是一项可选能力,不属于 agent loop(智能体循环)主干,也不拥有任何强制执行:执行、提示词叙述与回放仍然读取各自旋钮的折叠结果,预设切换只记录意图,并通过每个旋钮各自的规范 setter 写入。[包(package)README](../../packages/ui/permission/README.md) 负责组合状态与限制;[沙箱切换设计](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)负责决策依据。 +[dsh-permission](../../packages/interaction/permission) 的权限预设层(`ctx.permission`,`PermissionService`)把两个相互独立的强制执行旋钮(knob),即[沙箱模式](sandbox.md)(`sandbox/mode`)与[审批策略](approval.md)(`approval/policy`),捆绑成具名预设,供客户端作为单个权限(Permissions)选择器提供。它是一项可选能力,不属于 agent loop(智能体循环)主干,也不拥有任何强制执行:执行、提示词叙述与回放仍然读取各自旋钮的折叠结果,预设切换只记录意图,并通过每个旋钮各自的规范 setter 写入。[包(package)README](../../packages/interaction/permission/README.md) 负责组合状态与限制;[沙箱切换设计](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)负责决策依据。 -源码:[`packages/ui/permission/src/index.ts`](../../packages/ui/permission/src/index.ts) +源码:[`packages/interaction/permission/src/index.ts`](../../packages/interaction/permission/src/index.ts) ## 预设表 @@ -25,7 +25,7 @@ interface PresetSpec { ``` ```ts type-equiv -/** The {@link PermissionService} config: the deployment's preset table. */ +/** The {@link PermissionService} config: preset table and composition default. */ interface Config { /** * The preset table: name → knob bundle. Defaults to `workspace-write` @@ -33,6 +33,11 @@ interface Config { * never). The name `custom` is reserved for the derived not-a-preset state. */ presets?: Record + /** + * Default for new sessions. When omitted, the preset matching the composed + * sandbox and approval defaults is used. + */ + defaultPreset?: string } ``` @@ -51,13 +56,13 @@ interface PresetOption { value: string /** The display label. */ name: string - /** One user-facing sentence on what the value means. */ + /** One user-facing sentence on what the value means; omitted when not configured. */ description?: string } ``` ## 切换与 `permission/preset` 事件 -`set(session, name)` 解析预设(未知名称抛出异常),在 `name` 尚不是生效预设时追加一条仅记日志的 `permission/preset` 事件,然后通过各旋钮自己的 setter([dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) 的 `setSandboxMode` 与 [dsh-user-approval](../../packages/ui/user-approval) 的 `setApprovalPolicy`)写入,且仅当该旋钮的生效值发生变化时才写。同一轮次内,选择事件先于旋钮事件出现;重新选择当前生效的预设则什么都不追加。 +`set(session, name)` 解析预设(未知名称抛出异常),在 `name` 尚不是生效预设时追加一条仅记日志的 `permission/preset` 事件,然后通过各旋钮自己的 setter([dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) 的 `setSandboxMode` 与 [dsh-user-approval](../../packages/interaction/user-approval) 的 `setApprovalPolicy`)写入,且仅当该旋钮的生效值发生变化时才写。同一轮次内,选择事件先于旋钮事件出现;重新选择当前生效的预设则什么都不追加。 `permission/preset` 是持久、仅记日志的用户意图:它不进入模型 transcript(文本记录),模型可见的后果由旋钮事件经各自消费方承担;它存在是为了在两个预设共享同一个旋钮组合时,让 `current()` 仍能保住用户选择的究竟是哪一个预设;`effectivePermissionPreset(events)` 折叠最后一条,回放不需要任何追赶状态。完整事件声明见[持久化日志事件目录](../persistence-catalog.md);方法签名见生成的[服务目录](../cordis-catalog/services.md#ctxpermission--permissionservice)。 diff --git a/docs/subsystems/plan.i18n.yaml b/docs/subsystems/plan.i18n.yaml index 3295167755..f82002420b 100644 --- a/docs/subsystems/plan.i18n.yaml +++ b/docs/subsystems/plan.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/subsystems/plan.md -plan.md: c8bb6c9b6a0bcfd20ed6eadece4552d49f3c757c -plan.zh.md: c8acedaef0724581227b57afbf81418a7e538aed +plan.md: 661a393b18f9154771c0b9358ad7f016a4fdec17 +plan.zh.md: 068b63fd9f3bf46b7962031bba154cc7fc7daafd diff --git a/docs/subsystems/plan.md b/docs/subsystems/plan.md index c8bb6c9b6a..661a393b18 100644 --- a/docs/subsystems/plan.md +++ b/docs/subsystems/plan.md @@ -12,7 +12,7 @@ Source: [`packages/plan/plan-mode/src/index.ts`](../../packages/plan/plan-mode/s ## Pending intent and the step-boundary flush -Because every session event is turn-enclosed, a user selection is held as pending intent until the next step boundary — within the same turn when one is open. `set(agent, active)` records the pending selection (a no-op when the target equals the logged-or-already-pending state), and `get(agent)` returns `{ active: boolean; pending?: boolean }` — the logged state shaping the current step, plus the optimistic selection awaiting a boundary. +Because every session event is turn-enclosed, a user selection is held as pending intent until the next step boundary — the next request derivation, in whichever turn it occurs (selection never forces continuation, so an intent recorded after a turn's final step lands in a later turn). `set(agent, active)` records the pending selection (a no-op when the target equals the logged-or-already-pending state), and `get(agent)` returns `{ active: boolean; pending?: boolean }` — the logged state shaping the current step, plus the optimistic selection awaiting a boundary. The sole flush point is a prepended `agent/step` listener — the loop's in-turn interception seam that runs before every request derivation, including turn 1 step 1 and request-recovery retries. Prompt admission itself never flushes: it happens pre-turn, where a `plan/mode` append would land outside any open turn, so a selection made at the prompt is landed by the first step boundary inside the turn it starts. The prepend means the flush runs before the downstream `agent/step` listener chain. A flush failure is contained — plan policy can never block a turn — and the failed append stays pending for a later boundary. A flushed user selection also narrates the switch as one plugin-sourced `user/message` notice, but only when the last logged request header described the other state, so the model is told exactly when its context changed and never redundantly. A pending selection made while idle is process-local and lost on exit before the next boundary ([README limitation](../../packages/plan/plan-mode/README.md#known-limitations-and-deferred-work)). diff --git a/docs/subsystems/plan.zh.md b/docs/subsystems/plan.zh.md index c8acedaef0..068b63fd9f 100644 --- a/docs/subsystems/plan.zh.md +++ b/docs/subsystems/plan.zh.md @@ -12,7 +12,7 @@ ## 待定意图与步骤边界冲刷 -由于每个会话事件都位于轮次之内,用户的选择会作为待定意图保留到下一个步骤边界——已有轮次开启时即在同一轮次之内。`set(agent, active)` 记录待定选择(目标值与已记录或已在待定中的状态相同时不做任何事),`get(agent)` 返回 `{ active: boolean; pending?: boolean }`,即影响当前步骤的已记录状态,加上正在等待边界的乐观选择。 +由于每个会话事件都位于轮次之内,用户的选择会作为待定意图保留到下一个步骤边界——即下一次请求派生,落在哪个轮次就在哪个轮次生效(选择绝不强制续行,因此在某轮最后一步之后记录的意图会在之后的轮次落地)。`set(agent, active)` 记录待定选择(目标值与已记录或已在待定中的状态相同时不做任何事),`get(agent)` 返回 `{ active: boolean; pending?: boolean }`,即影响当前步骤的已记录状态,加上正在等待边界的乐观选择。 唯一的冲刷点是一个前置(prepend)注册的 `agent/step` 监听器——agent loop 的轮内拦截 seam,在每次请求派生之前运行,包括第 1 轮第 1 步和请求恢复重试。提示词提交本身绝不冲刷:它发生在轮次开启之前,此时追加 `plan/mode` 会落在任何开启的轮次之外,因此在提示词处做出的选择由它开启的轮次内的第一个步骤边界落地。前置注册意味着冲刷先于下游的 `agent/step` 监听器链运行。冲刷失败会被收容(计划策略绝不能阻塞轮次),追加失败的选择保持待定,等待后续边界。已冲刷的用户选择还会以一条插件来源的 `user/message` 通知叙述这次切换,但仅当最后记录的请求头描述的是另一种状态时才叙述,因此模型恰好在上下文变化时被告知,且绝不重复。空闲时做出的待定选择只存在于进程内,进程在下一个边界之前退出即丢失([README 限制](../../packages/plan/plan-mode/README.md#known-limitations-and-deferred-work))。 diff --git a/docs/subsystems/telemetry.i18n.yaml b/docs/subsystems/telemetry.i18n.yaml index e97a6f2e27..2eb634bb2d 100644 --- a/docs/subsystems/telemetry.i18n.yaml +++ b/docs/subsystems/telemetry.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/subsystems/telemetry.md -telemetry.md: 92235a26fe278dfd6ac2b2ea5703b5c0d837b336 -telemetry.zh.md: e076abd90bd5054ddbfd6fbe7b725ee45d50a038 +telemetry.md: cf570723650c32f048ee797923a58f09c777455d +telemetry.zh.md: 1bcaa86264e3afd741d66d6ee718946e6151496f diff --git a/docs/subsystems/telemetry.md b/docs/subsystems/telemetry.md index 92235a26fe..cf57072365 100644 --- a/docs/subsystems/telemetry.md +++ b/docs/subsystems/telemetry.md @@ -2,9 +2,9 @@ English | [中文](telemetry.zh.md) -Outbound session reporting, split as a [capability seam](../capability-seams.md): the seam ([dsh-session-telemetry](../../packages/telemetry/session-telemetry), `ctx.telemetry`) owns the capture points, the fixed chunk projection, the `telemetry/record` redaction waterfall, the handoff cursor, and the minimal backend contract; the backend a deployment loads ([dsh-session-telemetry-otel](../../packages/telemetry/session-telemetry-otel)) is the OpenTelemetry JS SDK's log pipeline configured verbatim. It is one optional capability, not part of the agent-loop spine, and nothing here reaches a model request. The boundary axiom — the harness's aspect ends at `emit()`; batching, retry, queueing, and loss policy belong to the reporting SDK — and the rejected alternatives are pinned in the [revival Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md); the capture points, cursor, and projection contracts live in the [seam README](../../packages/telemetry/session-telemetry/README.md). +Outbound session reporting, split as a [capability seam](../capability-seams.md): the seam ([dsh-session-telemetry](../../packages/session/session-telemetry), `ctx.telemetry`) owns the capture points, the fixed chunk projection, the `telemetry/record` redaction waterfall, the handoff cursor, and the minimal backend contract; the backend a deployment loads ([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel)) is the OpenTelemetry JS SDK's log pipeline configured verbatim. It is one optional capability, not part of the agent-loop spine, and nothing here reaches a model request. The boundary axiom — the harness's aspect ends at `emit()`; batching, retry, queueing, and loss policy belong to the reporting SDK — and the rejected alternatives are pinned in the [revival Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md); the capture points, cursor, and projection contracts live in the [seam README](../../packages/session/session-telemetry/README.md). -Source: [`packages/telemetry/session-telemetry/src/index.ts`](../../packages/telemetry/session-telemetry/src/index.ts) +Source: [`packages/session/session-telemetry/src/index.ts`](../../packages/session/session-telemetry/src/index.ts) ## The logical record @@ -12,7 +12,7 @@ Source: [`packages/telemetry/session-telemetry/src/index.ts`](../../packages/tel /** * Severity of a telemetry record, pre-mapped at capture so a receiver can * alert with zero configuration: `error` for events whose own outcome flag - * says so (`tool/result.isError`, `turn/end` error reasons) and for + * says so (the tool-result block's `isError`, `turn/end` error reasons) and for * `agent-error` operational records. Captured events otherwise default to * `info`; `warn` remains available to `telemetry/record` policies and * backends. @@ -69,9 +69,10 @@ interface TelemetryBackend { /** * Hand one record to the backend's pipeline. MUST be a non-blocking * enqueue — the coordinator calls this synchronously from the - * `session/event` hot path, so anything slower than a queue push would tax - * the agent loop. Errors thrown here are contained by the coordinator and - * logged; they never reach the loop. + * `session/event` hot path or an explicit canonical-log capture, so anything + * slower than a queue push would tax the agent loop or feedback handling. + * Errors thrown here are contained by the coordinator and logged; they + * never reach the loop. * @param record - the logical record to report; owned by the backend after the call. */ emit(record: TelemetryRecord): void @@ -96,6 +97,8 @@ interface TelemetryBackend { * coordinator emits its dispose-time `shutdown` markers immediately before * calling this). Awaited by the coordinator's dispose; a rejection is * logged as a warning and never fails application teardown. + * The coordinator captures dispose-time shutdown markers immediately before + * this call for live capture; on-demand capture creates no ops records. * @returns resolves when the backend's pipeline has quiesced. */ shutdown(): Promise diff --git a/docs/subsystems/telemetry.zh.md b/docs/subsystems/telemetry.zh.md index e076abd90b..1bcaa86264 100644 --- a/docs/subsystems/telemetry.zh.md +++ b/docs/subsystems/telemetry.zh.md @@ -2,9 +2,9 @@ [English](telemetry.md) | 中文 -对外的会话上报,拆分为一项[能力 seam](../capability-seams.md):seam 一侧([dsh-session-telemetry](../../packages/telemetry/session-telemetry),`ctx.telemetry`)拥有捕获点、固定分片投影、`telemetry/record` 脱敏 waterfall(瀑布式事件)、handoff 游标与最小后端契约;部署方加载的后端([dsh-session-telemetry-otel](../../packages/telemetry/session-telemetry-otel))则是原样配置的 OpenTelemetry JS SDK 日志流水线。它是一项可选能力,不属于 agent loop(智能体循环)主干,这里也没有任何内容会进入模型请求。边界公理(harness 的职责止于 `emit()`;批处理、重试、排队与丢失策略都属于上报 SDK)连同被否决的替代方案,均已在[复活 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)中定案;捕获点、游标与投影的契约见 [seam README](../../packages/telemetry/session-telemetry/README.md)。 +对外的会话上报,拆分为一项[能力 seam](../capability-seams.md):seam 一侧([dsh-session-telemetry](../../packages/session/session-telemetry),`ctx.telemetry`)拥有捕获点、固定分片投影、`telemetry/record` 脱敏 waterfall(瀑布式事件)、handoff 游标与最小后端契约;部署方加载的后端([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel))则是原样配置的 OpenTelemetry JS SDK 日志流水线。它是一项可选能力,不属于 agent loop(智能体循环)主干,这里也没有任何内容会进入模型请求。边界公理(harness 的职责止于 `emit()`;批处理、重试、排队与丢失策略都属于上报 SDK)连同被否决的替代方案,均已在[复活 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)中定案;捕获点、游标与投影的契约见 [seam README](../../packages/session/session-telemetry/README.md)。 -源码:[`packages/telemetry/session-telemetry/src/index.ts`](../../packages/telemetry/session-telemetry/src/index.ts) +源码:[`packages/session/session-telemetry/src/index.ts`](../../packages/session/session-telemetry/src/index.ts) ## 逻辑记录 @@ -12,7 +12,7 @@ /** * Severity of a telemetry record, pre-mapped at capture so a receiver can * alert with zero configuration: `error` for events whose own outcome flag - * says so (`tool/result.isError`, `turn/end` error reasons) and for + * says so (the tool-result block's `isError`, `turn/end` error reasons) and for * `agent-error` operational records. Captured events otherwise default to * `info`; `warn` remains available to `telemetry/record` policies and * backends. @@ -69,9 +69,10 @@ interface TelemetryBackend { /** * Hand one record to the backend's pipeline. MUST be a non-blocking * enqueue — the coordinator calls this synchronously from the - * `session/event` hot path, so anything slower than a queue push would tax - * the agent loop. Errors thrown here are contained by the coordinator and - * logged; they never reach the loop. + * `session/event` hot path or an explicit canonical-log capture, so anything + * slower than a queue push would tax the agent loop or feedback handling. + * Errors thrown here are contained by the coordinator and logged; they + * never reach the loop. * @param record - the logical record to report; owned by the backend after the call. */ emit(record: TelemetryRecord): void @@ -96,6 +97,8 @@ interface TelemetryBackend { * coordinator emits its dispose-time `shutdown` markers immediately before * calling this). Awaited by the coordinator's dispose; a rejection is * logged as a warning and never fails application teardown. + * The coordinator captures dispose-time shutdown markers immediately before + * this call for live capture; on-demand capture creates no ops records. * @returns resolves when the backend's pipeline has quiesced. */ shutdown(): Promise diff --git a/docs/subsystems/workspace.i18n.yaml b/docs/subsystems/workspace.i18n.yaml index a50df35f91..6a6488206c 100644 --- a/docs/subsystems/workspace.i18n.yaml +++ b/docs/subsystems/workspace.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/subsystems/workspace.md -workspace.md: bade3526964055d5bf4448265c5898f1e66a1fce -workspace.zh.md: b0437e42b644c1ce11d9a667972f7b236cf4caa1 +workspace.md: 5614631e828e51f3b515ae4af17451bd9eda1dbb +workspace.zh.md: 11e8f0207a8db8b7612d6672cfe744523490aef7 diff --git a/docs/subsystems/workspace.md b/docs/subsystems/workspace.md index bade352696..5614631e82 100644 --- a/docs/subsystems/workspace.md +++ b/docs/subsystems/workspace.md @@ -68,7 +68,9 @@ interface Workspace { /** * Prepend a session to this workspace's candidate account. An already - * accounted id resolves without writing. A new id's live or persisted + * accounted id resolves without writing, aside from the durable + * filtered-candidate prune every accepted mutation performs. A new id's + * live or persisted * header cwd must resolve to an existing directory equal to {@link path}; * unknown ids, missing or invalid cwd values, and mismatches reject without * writing. @@ -82,7 +84,9 @@ interface Workspace { * with an anchor the session lands before it, without one it appends to the * end. Only the moved id changes position. A session or anchor absent from * the account rejects without writing; a move to the current position - * resolves without writing (decided on the domain write chain). + * resolves without writing, aside from the durable filtered-candidate + * prune every accepted mutation performs; decided on the domain write + * chain. * @param sessionId - The accounted session to move. * @param beforeSessionId - Accounted anchor to insert before; omitted appends. * @returns resolution after durability. @@ -91,8 +95,9 @@ interface Workspace { /** * Remove a session from this workspace's account. Idempotent: an id not on - * the account resolves without writing (decided on the domain write chain, - * like attach). Never touches the session's own stored log. + * the account resolves without writing, aside from the durable + * filtered-candidate prune every accepted mutation performs; decided on + * the domain write chain like attach. Never touches the session's own stored log. * @param sessionId - The session to remove. * @returns resolution after durability. */ diff --git a/docs/subsystems/workspace.zh.md b/docs/subsystems/workspace.zh.md index b0437e42b6..11e8f0207a 100644 --- a/docs/subsystems/workspace.zh.md +++ b/docs/subsystems/workspace.zh.md @@ -68,7 +68,9 @@ interface Workspace { /** * Prepend a session to this workspace's candidate account. An already - * accounted id resolves without writing. A new id's live or persisted + * accounted id resolves without writing, aside from the durable + * filtered-candidate prune every accepted mutation performs. A new id's + * live or persisted * header cwd must resolve to an existing directory equal to {@link path}; * unknown ids, missing or invalid cwd values, and mismatches reject without * writing. @@ -82,7 +84,9 @@ interface Workspace { * with an anchor the session lands before it, without one it appends to the * end. Only the moved id changes position. A session or anchor absent from * the account rejects without writing; a move to the current position - * resolves without writing (decided on the domain write chain). + * resolves without writing, aside from the durable filtered-candidate + * prune every accepted mutation performs; decided on the domain write + * chain. * @param sessionId - The accounted session to move. * @param beforeSessionId - Accounted anchor to insert before; omitted appends. * @returns resolution after durability. @@ -91,8 +95,9 @@ interface Workspace { /** * Remove a session from this workspace's account. Idempotent: an id not on - * the account resolves without writing (decided on the domain write chain, - * like attach). Never touches the session's own stored log. + * the account resolves without writing, aside from the durable + * filtered-candidate prune every accepted mutation performs; decided on + * the domain write chain like attach. Never touches the session's own stored log. * @param sessionId - The session to remove. * @returns resolution after durability. */ diff --git a/packages/typert/generator/tests/cordis-catalog-contract.spec.ts b/packages/typert/generator/tests/cordis-catalog-contract.spec.ts index 30ef60e8fe..1b6589b6cf 100644 --- a/packages/typert/generator/tests/cordis-catalog-contract.spec.ts +++ b/packages/typert/generator/tests/cordis-catalog-contract.spec.ts @@ -152,11 +152,11 @@ describe.skip('gen-cordis-catalog collectEvents', { timeout: 60_000 }, () => { it('accepts linked, foundation, generic-parameter, and explicitly exempt signature types', () => { const events = collectEvents(make( - ' /**\n * Carry linked and foundation types.\n * @param value - the linked value.\n * @param assembly - assembly result documented outside the subsystems catalog.\n * @param signal - cancellation.\n * @mode parallel\n */\n \'fix/typed\'(value: Readonly, assembly: PromptAssembly, signal: AbortSignal): Promise', + ' /**\n * Carry linked and foundation types.\n * @param value - the linked value.\n * @param preset - deployment metadata documented outside the subsystems catalog.\n * @param signal - cancellation.\n * @mode parallel\n */\n \'fix/typed\'(value: Readonly, preset: PresetSpec, signal: AbortSignal): Promise', )) expect(events).toHaveLength(1) expect(renderEvents(events)).toContain('Types: [SessionEvent](../subsystems/core.md)') - expect(renderEvents(events)).not.toContain('[PromptAssembly]') + expect(renderEvents(events)).not.toContain('[PresetSpec]') }) it('aggregates every unclassified signature type with its source and remediation', () => { diff --git a/packages/workspace/workspace/README.i18n.yaml b/packages/workspace/workspace/README.i18n.yaml index 7ba60b4109..63d5ce0e8e 100644 --- a/packages/workspace/workspace/README.i18n.yaml +++ b/packages/workspace/workspace/README.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/workspace/workspace/README.md README.md: 057765e38de9cc700210eb8edeb1ddc7ffc861ff -README.zh.md: f3be9611eba45db3719ce76ad6625f4787fab9ac +README.zh.md: 7416875dbf2ee1652f6e1fa1663144d7407a1ae7 diff --git a/packages/workspace/workspace/README.zh.md b/packages/workspace/workspace/README.zh.md index f3be9611eb..7416875dbf 100644 --- a/packages/workspace/workspace/README.zh.md +++ b/packages/workspace/workspace/README.zh.md @@ -12,6 +12,7 @@ DeepSeek Harness 的 Workspace 实体注册表(`ctx.workspace`):通过领 - `ctx.workspace.get(id)`/`list()`/`resolveByPath(path)`:由缓存提供的查找。`list()` 为同步操作,并遵循持久注册表顺序;`resolveByPath` 为异步操作,因为它采用相同的 `realpath` 规范化方式,并会拒绝缺失路径,而不是创建路径。 - `ctx.workspace.delete(id)`:只移除 Workspace 注册记录、对应的持久顺序条目及会话归属记录。未知 id 返回 `false`,成功移除记录则返回 `true`。目录、用户文件、活跃会话和持久化会话日志绝不受影响,因此相关会话会进入 Ungrouped。表写入失败时会恢复原顺序和此前发布的实体。 - `Workspace.attachSession(id)`:对照 workspace 路径验证实时或已持久化的会话头 cwd,并将新 id 前置。未知会话、缺失/无法解析/非目录的 cwd 值和不匹配情况都会在不写入的前提下被拒绝。`detachSession` 只移除候选索引条目。 +- `Workspace.insertSessionBefore(id, before?)`:在手动顺序内移动一个已记账的会话,语义类似 DOM 的 insertBefore:插到锚点之前,省略锚点则追加到末尾。会话或锚点不在记账中时拒绝且不写入;移动到当前位置时直接完成且不写入。Workspace 顺序绝不改变。 - `ctx.workspace.archiveSession(id)`/`archivedSessionIds`:覆盖在 workspace 记账之上的注册表级全局归档集合:被归档的会话从各分组视图中消失,但其会话日志和 `sessionIds` 席位保持不变,未来取消归档时可恢复原位置。归档接受任何实时或已持久化的会话(无论已记账还是 Ungrouped),对已归档的 id 直接完成而不写入,并拒绝未知 id。在该字段出现之前写入的状态解析为一个空集合。 - `Workspace.sessionIds`:按持久候选顺序提供同步 id 加规范 cwd 成员投影。缺失头部、无效 cwd 值和不匹配情况都被过滤;下一次 workspace 变更会剪除它们。如果同一存储介质将一个会话索引到两个 workspace 下、用两条记录声明同一路径,或偏离持久 workspace 顺序,启动会被拒绝。 - `Workspace.status()`:未缓存的目录检查,返回 `'ok' | 'missing-dir'`;目录缺失绝不会改动记录。 diff --git a/packages/workspace/workspace/src/types.ts b/packages/workspace/workspace/src/types.ts index 09d37213cc..555e94d45d 100644 --- a/packages/workspace/workspace/src/types.ts +++ b/packages/workspace/workspace/src/types.ts @@ -59,7 +59,9 @@ export interface Workspace { /** * Prepend a session to this workspace's candidate account. An already - * accounted id resolves without writing. A new id's live or persisted + * accounted id resolves without writing, aside from the durable + * filtered-candidate prune every accepted mutation performs. A new id's + * live or persisted * header cwd must resolve to an existing directory equal to {@link path}; * unknown ids, missing or invalid cwd values, and mismatches reject without * writing. @@ -73,7 +75,9 @@ export interface Workspace { * with an anchor the session lands before it, without one it appends to the * end. Only the moved id changes position. A session or anchor absent from * the account rejects without writing; a move to the current position - * resolves without writing (decided on the domain write chain). + * resolves without writing, aside from the durable filtered-candidate + * prune every accepted mutation performs; decided on the domain write + * chain. * @param sessionId - The accounted session to move. * @param beforeSessionId - Accounted anchor to insert before; omitted appends. * @returns resolution after durability. @@ -82,8 +86,9 @@ export interface Workspace { /** * Remove a session from this workspace's account. Idempotent: an id not on - * the account resolves without writing (decided on the domain write chain, - * like attach). Never touches the session's own stored log. + * the account resolves without writing, aside from the durable + * filtered-candidate prune every accepted mutation performs; decided on + * the domain write chain like attach. Never touches the session's own stored log. * @param sessionId - The session to remove. * @returns resolution after durability. */ diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index b3fdfb7546..3cc277152e 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -1,32 +1,25 @@ /** - * Generate the Cordis event and service catalogs from static declarations. - * The walk enforces event modes, JSDoc parameter/return completeness, and - * signature type-link coverage; inherited Cordis services come from the - * curated table below. `--check` verifies both committed artifacts. + * Generate committed Cordis artifacts from the Typert catalog projector and + * the independent vendored-core projector. */ -import { globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' -import { dirname, resolve, sep } from 'node:path' -import ts from 'typescript' +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import { + projectCordisCatalog, + renderEvents, + renderServices, +} from '@deepseek-ai/dsh-typert-generator' +import type { CordisCatalogPolicy } from '@deepseek-ai/dsh-typert-generator' 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' const root = resolve(import.meta.dirname, '..') const OUT_EVENTS = 'docs/cordis-catalog/events.md' const OUT_SERVICES = 'docs/cordis-catalog/services.md' +const OUT_RUNTIME_API = 'packages/self-modification/tool-cordis/src/api-catalog.ts' -/** The fenced-block info string for generated signature blocks (skipped by - * doc-typecheck, since a bare signature fragment is not standalone-compilable). */ -const FENCE = 'ts cordis-catalog' - -/** - * One primary subsystems page per project type used by a generated - * signature. This stays curated because union names intentionally do not - * reuse the type-equivalence manifest's map-symbol entries and some symbols - * appear on more than one page. - */ -export const LINK_MAP: Record = { +/** One primary subsystems page per project type used by a generated signature. */ +export const LINK_MAP: Readonly> = { Agent: 'core.md', AgentCancelCause: 'core.md', AgentOptions: 'core.md', @@ -35,6 +28,7 @@ export const LINK_MAP: Record = { ContinuationDecision: 'core.md', ContinuationStop: 'core.md', GenerateOptions: 'core.md', + InboxItem: 'core.md', InboxPlacement: 'core.md', MessageId: 'core.md', HookContext: 'core.md', @@ -57,9 +51,7 @@ export const LINK_MAP: Record = { UserMessage: 'session.md', PreStepDecision: 'core.md', PreStepContext: 'core.md', - PromptDecision: 'core.md', RequestErrorAction: 'core.md', - RequestError: 'core.md', RequestFailureContext: 'core.md', PreparedReferencedMessage: 'session-reference.md', SessionReferenceCandidate: 'session-reference.md', @@ -116,6 +108,7 @@ export const LINK_MAP: Record = { PreparedLlmCall: 'llm-streaming.md', LlmService: 'llm-streaming.md', StreamChunk: 'llm-streaming.md', + SkillProviderControl: 'skills.md', CreateSessionOptions: 'persistence.md', PrepareSessionOptions: 'persistence.md', SessionHeader: 'persistence.md', @@ -168,9 +161,11 @@ export const LINK_MAP: Record = { SessionTitleObservationResult: 'session-query.md', SessionTitleProvider: 'session-title.md', SessionTitleSnapshot: 'session-title.md', + SkillCatalogSnapshot: 'skills.md', SkillDefinition: 'skills.md', SkillLookupOptions: 'skills.md', SkillProvider: 'skills.md', + SkillProviderObservation: 'skills.md', SkillRegistration: 'skills.md', SkillSummary: 'skills.md', SaveTextSpill: 'spill.md', @@ -237,8 +232,6 @@ export const LINK_MAP: Record = { WebSearchRequest: 'web.md', WebSearchResult: 'web.md', WorkflowRun: 'workflow.md', - WorkflowRunInfo: 'workflow.md', - WorkflowStartRequest: 'workflow.md', PresetOption: 'permission.md', PresetSpec: 'permission.md', InvariantInstaller: 'invariants.md', @@ -253,14 +246,17 @@ export const LINK_MAP: Record = { WorkspaceId: 'workspace.md', WebBootGraph: 'client-modules.md', TelemetryRecord: 'telemetry.md', + WorkflowRunInfo: 'workflow.md', + WorkflowStartRequest: 'workflow.md', } -/** TypeScript lib and pinned framework types that have no repository-owned data page. */ -const FOUNDATION_TYPE_NAMES = new Set([ +/** TypeScript lib and pinned framework types with no repository-owned data page. */ +export const FOUNDATION_TYPE_NAMES: ReadonlySet = new Set([ 'AbortSignal', 'AsyncIterable', 'Context', 'Error', + 'Map', 'Partial', 'Pick', 'Promise', @@ -269,7 +265,7 @@ const FOUNDATION_TYPE_NAMES = new Set([ ]) /** Project types deliberately documented outside the subsystems catalog. */ -const TYPE_LINK_EXEMPTIONS: Readonly> = { +export const TYPE_LINK_EXEMPTIONS: Readonly> = { AgentFactory: 'agent creation seam is owned by packages/core/agent/README.md', z: 'schemastery schema constructor is owned by vendor/schemastery (vendored upstream)', BeginCommandRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts', @@ -281,6 +277,7 @@ const TYPE_LINK_EXEMPTIONS: Readonly> = { BashEnvVariableInfo: 'service-local metadata type is owned by packages/bash/tool-bash/src/index.ts', CompactAgentContext: 'compaction service input is owned by packages/compact/compact/src/index.ts', ManualCompactAgentContext: 'manual compaction service input is owned by packages/compact/compact/src/index.ts', + DirectoryPickerCapability: 'picker interaction contract is owned by packages/host/directory-picker/README.md', CreateAgentOptions: 'agent creation contract is owned by packages/core/agent/README.md', DomainImpl: 'domain implementation contract is owned by packages/storage/storage-domain/README.md', ProjectionDefinition: 'projection unit contract is owned by packages/session/session-projection/README.md', @@ -318,401 +315,51 @@ const TYPE_LINK_EXEMPTIONS: Readonly> = { WorkflowResultInfo: 'event-local snapshot is owned by packages/workflow/workflow/src/index.ts', } -/** Collect named references from parameter, generic-constraint/default, and return types. */ -function signatureTypeNames(member: ts.MethodSignature | ts.MethodDeclaration, sf: ts.SourceFile): string[] { - const declared = new Set(member.typeParameters?.map(parameter => parameter.name.text) ?? []) - const referenced = new Set() - const visit = (node: ts.Node): void => { - if (ts.isTypeReferenceNode(node)) referenced.add(node.typeName.getText(sf)) - if (ts.isTypeQueryNode(node)) referenced.add(node.exprName.getText(sf)) - ts.forEachChild(node, visit) - } - for (const parameter of member.typeParameters ?? []) { - if (parameter.constraint) visit(parameter.constraint) - if (parameter.default) visit(parameter.default) - } - for (const parameter of member.parameters) { - if (parameter.type) visit(parameter.type) - } - if (member.type) visit(member.type) - return [...referenced].filter(name => !declared.has(name)).sort() +/** Repository data policy consumed by the Cordis catalog projector. */ +export const CORDIS_CATALOG_POLICY: CordisCatalogPolicy = { + linkedTypePages: LINK_MAP, + foundationTypeNames: FOUNDATION_TYPE_NAMES, + typeLinkExemptions: TYPE_LINK_EXEMPTIONS, + inheritedEvents: [ + { name: 'internal/plugin', summary: 'A plugin fiber was created.', source: 'vendor/cordis/src/events.ts:328' }, + { name: 'internal/status', summary: 'A fiber changed lifecycle state.', source: 'vendor/cordis/src/events.ts:330' }, + { name: 'internal/service', summary: 'Interception hook for a service binding (no core producer).', source: 'vendor/cordis/src/events.ts:332' }, + { name: 'internal/update', summary: 'Waterfall: a fiber config update is being applied.', source: 'vendor/cordis/src/events.ts:334' }, + { name: 'internal/get', summary: 'Waterfall: a service is being read from the store.', source: 'vendor/cordis/src/events.ts:336' }, + { name: 'internal/set', summary: 'Waterfall: a service is being written to the store.', source: 'vendor/cordis/src/events.ts:338' }, + { name: 'internal/listener', summary: 'A listener was registered.', source: 'vendor/cordis/src/events.ts:340' }, + { name: 'internal/dispatch', summary: 'An event is being dispatched to listeners.', source: 'vendor/cordis/src/events.ts:342' }, + { name: 'hmr/change', summary: 'A watched source file changed on disk.', source: 'vendor/hmr/src/index.ts:20' }, + { name: 'hmr/reload', summary: 'Plugins are being reloaded after a change.', source: 'vendor/hmr/src/index.ts:21' }, + { name: 'exit', summary: 'The process is exiting on a signal.', source: 'vendor/loader/src/index.ts:23' }, + { name: 'loader/config-update', summary: 'The loader config tree changed.', source: 'vendor/loader/src/index.ts:24' }, + { name: 'loader/entry-init', summary: 'A config entry is being initialized.', source: 'vendor/loader/src/index.ts:25' }, + { name: 'loader/partial-dispose', summary: 'An entry is being partially disposed on reload.', source: 'vendor/loader/src/index.ts:26' }, + { name: 'loader/patch-context', summary: 'A context is being patched during a reload.', source: 'vendor/loader/src/index.ts:27' }, + ], + inheritedServices: [ + { name: 'ctx.on / ctx.once', summary: 'Register an event listener (disposable).', source: 'vendor/cordis/src/events.ts:34' }, + { name: 'ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall', summary: 'Dispatch an event (sync / awaited / first-bail / veto-chain).', source: 'vendor/cordis/src/events.ts:34' }, + { name: 'ctx.plugin / ctx.inject', summary: 'Load a plugin / declare required services.', source: 'vendor/cordis/src/registry.ts:164' }, + { name: 'ctx.effect', summary: 'Register a disposable side effect tied to the fiber.', source: 'vendor/cordis/src/fiber.ts:9' }, + { name: 'ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin', summary: 'Low-level service-store access and binding.', source: 'vendor/cordis/src/reflect.ts:7' }, + { name: 'ctx.extend / ctx.isolate / ctx.intercept', summary: 'Derive a child context (scoped services / isolation / interception).', source: 'vendor/cordis/src/context.ts:42' }, + { name: 'ctx.root / ctx.scope / ctx.fiber / ctx.registry / ctx.reflect / ctx.events / ctx.logger', summary: 'Ambient handles onto the running context graph.', source: 'vendor/cordis/src/context.ts:16' }, + { name: 'ctx.timer (+ interval / timeout / throttle / debounce / setTimeout / setInterval)', summary: 'Disposable timer helpers. The `timer` key is provided at runtime; the six helpers are mixed onto ctx directly (declared via Pick).', source: 'vendor/timer/src/index.ts:4' }, + { name: 'ctx.loader', summary: 'The config Loader that booted the app (present under the loader).', source: 'vendor/loader/src/index.ts:30' }, + { name: 'ctx.hmr', summary: 'The hot-module-reload watcher (present under the hmr plugin).', source: 'vendor/hmr/src/index.ts:15' }, + ], } -/** Append fail-closed signature type-link violations with actionable ownership choices. */ -function checkTypeLinks( - where: string, - member: ts.MethodSignature | ts.MethodDeclaration, - sf: ts.SourceFile, - violations: string[], -): void { - for (const name of signatureTypeNames(member, sf)) { - if (Object.hasOwn(LINK_MAP, name) - || FOUNDATION_TYPE_NAMES.has(name) - || Object.hasOwn(TYPE_LINK_EXEMPTIONS, name)) continue - violations.push( - `${where} references unclassified type '${name}'. Add it to LINK_MAP with its subsystems page, ` - + 'to FOUNDATION_TYPE_NAMES if TypeScript or Cordis owns it, or to TYPE_LINK_EXEMPTIONS with ' - + 'the non-catalog documentation owner.', - ) - } -} - -/** Throw one aggregated diagnostic for every unclassified signature type. */ -function reportTypeLinkViolations(gate: string, violations: string[]): void { - if (violations.length === 0) return - throw new Error( - `${gate}: ${violations.length} signature type-link coverage violation(s):\n` - + violations.map(violation => ` ${violation}`).join('\n'), - ) -} - -/** One harness event, extracted from an `interface Events` block. */ -interface EventEntry { - /** Scoped name, e.g. `agent/request`. */ - name: string - /** The scope prefix, e.g. `agent` (everything before the first `/`). */ - scope: string - /** Full signature text (the method-signature member, JSDoc stripped). */ - signature: string - /** Original declaration JSDoc, dedented from its containing interface. */ - jsDoc: string - /** Dispatch mode from the `@mode` tag. */ - mode: Mode - /** Description prose (JSDoc minus the `@mode` tag), one line per paragraph. */ - doc: string - /** Source pointer `packages/…/file.ts:line` of the declaration. */ - source: string -} - -/** One public service method and the source contract attached to it. */ -interface ServiceMethodEntry { - /** Public method signature (body stripped). */ - signature: string - /** Original method JSDoc, dedented from its containing class. */ - jsDoc: string -} - -/** One harness service, extracted from an `interface Context` block. */ -interface ServiceEntry { - /** The `ctx.` name, e.g. `llm`. */ - key: string - /** The service class/interface name, e.g. `LlmService`. */ - type: string - /** Whether the service class is abstract (a seam interface). */ - abstract: boolean - /** Class-level JSDoc prose, one line per paragraph. */ - doc: string - /** Public methods (bodies stripped), in source order. */ - methods: ServiceMethodEntry[] - /** Source pointer of the class declaration. */ - source: string -} - -/** A terse inherited-tier entry (pinned vendor surface). */ -interface InheritedEntry { - name: string - summary: string - /** Source pointer `vendor/…:line`. */ - source: string -} - -// cordisModuleBody / eventMembers / serviceClasses live in cordis-walk.ts. - -/** The signature text of a method-signature member (everything but a body). */ -function memberSignature(member: ts.TypeElement | ts.ClassElement, sf: ts.SourceFile): string { - const full = member.getText(sf) - const body = (member as { body?: ts.Node }).body - const sig = body ? full.slice(0, full.length - body.getText(sf).length) : full - return sig.replace(/\s*;?\s*$/, '').replace(/\s+/g, ' ').trim() -} - -/** - * Copy a node's original JSDoc while removing only the indentation imposed by - * its containing interface or class. +/** CLI entry: default writes every artifact; `--check` reports stale files. + * @returns nothing; writes files or reports freshness through the process. */ -function jsDocText(text: string, sf: ts.SourceFile, node: ts.Node): string { - const raw = rawJsDoc(text, node) - if (!raw) return '' - const start = text.lastIndexOf(raw, node.getStart(sf)) - const { line } = sf.getLineAndCharacterOfPosition(start) - const lineStart = sf.getPositionOfLineAndCharacter(line, 0) - const indent = text.slice(lineStart, start) - return raw.split('\n') - .map((lineText, index) => index > 0 && lineText.startsWith(indent) ? lineText.slice(indent.length) : lineText) - .join('\n') -} - -/** Walk every harness `interface Events` block and extract its events, hard- - * erroring (aggregated) on any JSDoc-completeness violation: a missing/ - * contradicted `@mode`, missing description prose, or an undocumented payload - * parameter. `scanRoot` defaults to the repo root; tests pass a fixture dir. */ -export function collectEvents(scanRoot: string = root): EventEntry[] { - const entries: EventEntry[] = [] - const violations: string[] = [] - const typeLinkViolations: string[] = [] - for (const rel of globSync('packages/*/*/src/*.ts', { cwd: scanRoot }).map(s => s.split(sep).join('/')).sort()) { - const abs = resolve(scanRoot, rel) - const text = readFileSync(abs, 'utf8') - if (!text.includes('interface Events')) continue - const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true) - const body = cordisModuleBody(sf) - if (!body) continue - for (const { name, member } of eventMembers(body, sf)) { - const signature = memberSignature(member, sf) - const raw = rawJsDoc(text, member) - const { doc, mode } = parseJsDoc(raw) - const src = pointer(rel, sf, member) - const where = `event '${name}' (${src})` - checkTypeLinks(where, member, sf, typeLinkViolations) - if (!mode) { - violations.push(`${where} is missing an @mode tag. Add '@mode emit|waterfall|parallel|serial|bail' to its JSDoc (see AGENTS.md).`) - } - // Conclusive structural check: a trailing `next: () => …` parameter is a - // waterfall. (emit vs parallel vs serial is not structurally - // distinguishable, so it is trusted from the tag.) - const last = member.parameters.at(-1) - const hasNext = !!last && last.name.getText(sf) === 'next' - if (mode && hasNext && mode !== 'waterfall') { - violations.push(`${where} has a trailing 'next' parameter (structurally a waterfall) but is tagged '@mode ${mode}'. Fix the tag or the signature.`) - } - if (mode && !hasNext && mode === 'waterfall') { - violations.push(`${where} is tagged '@mode waterfall' but has no trailing 'next' parameter. A waterfall delegates via next().`) - } - if (!doc) violations.push(`${where} has no description prose. Say what happened / what a listener may do, above the block tags.`) - // Payload parameters need a non-empty @param. The `this` receiver is not - // payload, and a waterfall's trailing `next` is covered by its mode. - const { params } = parseTags(raw) - checkParams(where, 'event', member.parameters, params, sf, - p => (ts.isIdentifier(p.name) && p.name.text === 'this') || (hasNext && p === last), violations) - if (mode) entries.push({ name, scope: name.split('/')[0] ?? name, signature, jsDoc: jsDocText(text, sf, member), mode, doc, source: src }) - } - } - reportViolations('gen-cordis-catalog', violations) - reportTypeLinkViolations('gen-cordis-catalog', typeLinkViolations) - return entries -} - -/** Walk every harness `interface Context` block + its service class, hard- - * erroring (aggregated) on any JSDoc-completeness violation: a class or public - * method without JSDoc prose, an undocumented parameter, a stale `@param`, a - * missing `@returns` on a non-void method, or an inferred (unannotated) return - * type the pure-AST walk cannot classify. - * `scanRoot` defaults to the repo root; tests pass a fixture dir. */ -export function collectServices(scanRoot: string = root): ServiceEntry[] { - const entries: ServiceEntry[] = [] - const violations: string[] = [] - const typeLinkViolations: string[] = [] - for (const rel of globSync('packages/*/*/src/index.ts', { cwd: scanRoot }).map(s => s.split(sep).join('/')).sort()) { - const abs = resolve(scanRoot, rel) - const text = readFileSync(abs, 'utf8') - if (!text.includes('interface Context')) continue - const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true) - const body = cordisModuleBody(sf) - if (!body) continue - // Resolve each ctx key to its service class (shared walk) and emit an entry. - for (const { key, type, cls, abstract, doc: clsDoc } of serviceClasses(body, sf, rel, violations)) { - const methods: ServiceMethodEntry[] = [] - for (const member of cls.members) { - if (!ts.isMethodDeclaration(member)) continue - // Only instance methods callable through `ctx.` are surface; - // private, protected, and static methods are not. - const nonPublic = member.modifiers?.some(m => - m.kind === ts.SyntaxKind.PrivateKeyword - || m.kind === ts.SyntaxKind.ProtectedKeyword - || m.kind === ts.SyntaxKind.StaticKeyword) - || ts.isPrivateIdentifier(member.name) - if (nonPublic) continue - const memberName = member.name.getText(sf) - if (memberName.startsWith('[')) continue // computed/symbol members - const where = `service method ctx.${key}.${memberName} (${pointer(rel, sf, member)})` - checkTypeLinks(where, member, sf, typeLinkViolations) - const raw = rawJsDoc(text, member) - methods.push({ signature: memberSignature(member, sf), jsDoc: jsDocText(text, sf, member) }) - if (!raw) { violations.push(`${where} has no JSDoc.`); continue } - if (!parseJsDoc(raw).doc) violations.push(`${where} has no description prose above its block tags.`) - const { params, returns } = parseTags(raw) - // Every parameter needs a non-empty @param (`this` receiver exempt), - // and a non-void ANNOTATED result needs a non-empty @returns — the - // shared checkers carry the exact contract. - checkParams(where, 'service', member.parameters, params, sf, - p => ts.isIdentifier(p.name) && p.name.text === 'this', violations) - checkReturns(where, member.type, returns, sf, violations) - } - entries.push({ - key, - type, - abstract, - doc: clsDoc, - methods, - source: pointer(rel, sf, cls), - }) - } - } - reportViolations('gen-cordis-catalog', violations) - reportTypeLinkViolations('gen-cordis-catalog', typeLinkViolations) - return entries.sort((a, b) => a.key.localeCompare(b.key)) -} - -/** - * The inherited tier — cordis core + loader/hmr/timer. Curated, terse, and - * hand-summarized because (a) it is pinned vendor source that changes only on a - * deliberate vendor sync, (b) the cordis-core `Context` mixes true ctx members - * with non-service fields (`root`, `baseUrl`, `logger`) that a blind walk would - * wrongly surface as services, and (c) the internal/* events carry no JSDoc to - * render. Source pointers are verified against vendor by `verify-md-links`' - * sibling check is N/A; keep them current on a vendor bump. - */ -const INHERITED_EVENTS: InheritedEntry[] = [ - { name: 'internal/plugin', summary: 'A plugin fiber was created.', source: 'vendor/cordis/src/events.ts:328' }, - { name: 'internal/status', summary: 'A fiber changed lifecycle state.', source: 'vendor/cordis/src/events.ts:330' }, - { name: 'internal/service', summary: 'Interception hook for a service binding (no core producer).', source: 'vendor/cordis/src/events.ts:332' }, - { name: 'internal/update', summary: 'Waterfall: a fiber config update is being applied.', source: 'vendor/cordis/src/events.ts:334' }, - { name: 'internal/get', summary: 'Waterfall: a service is being read from the store.', source: 'vendor/cordis/src/events.ts:336' }, - { name: 'internal/set', summary: 'Waterfall: a service is being written to the store.', source: 'vendor/cordis/src/events.ts:338' }, - { name: 'internal/listener', summary: 'A listener was registered.', source: 'vendor/cordis/src/events.ts:340' }, - { name: 'internal/dispatch', summary: 'An event is being dispatched to listeners.', source: 'vendor/cordis/src/events.ts:342' }, - { name: 'hmr/change', summary: 'A watched source file changed on disk.', source: 'vendor/hmr/src/index.ts:20' }, - { name: 'hmr/reload', summary: 'Plugins are being reloaded after a change.', source: 'vendor/hmr/src/index.ts:21' }, - { name: 'exit', summary: 'The process is exiting on a signal.', source: 'vendor/loader/src/index.ts:23' }, - { name: 'loader/config-update', summary: 'The loader config tree changed.', source: 'vendor/loader/src/index.ts:24' }, - { name: 'loader/entry-init', summary: 'A config entry is being initialized.', source: 'vendor/loader/src/index.ts:25' }, - { name: 'loader/partial-dispose', summary: 'An entry is being partially disposed on reload.', source: 'vendor/loader/src/index.ts:26' }, - { name: 'loader/patch-context', summary: 'A context is being patched during a reload.', source: 'vendor/loader/src/index.ts:27' }, -] - -export const INHERITED_SERVICES: InheritedEntry[] = [ - { name: 'ctx.on / ctx.once', summary: 'Register an event listener (disposable).', source: 'vendor/cordis/src/events.ts:34' }, - { name: 'ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall', summary: 'Dispatch an event (sync / awaited / first-bail / veto-chain).', source: 'vendor/cordis/src/events.ts:34' }, - { name: 'ctx.plugin / ctx.inject', summary: 'Load a plugin / declare required services.', source: 'vendor/cordis/src/registry.ts:164' }, - { name: 'ctx.effect', summary: 'Register a disposable side effect tied to the fiber.', source: 'vendor/cordis/src/fiber.ts:9' }, - { name: 'ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin', summary: 'Low-level service-store access and binding.', source: 'vendor/cordis/src/reflect.ts:7' }, - { name: 'ctx.extend / ctx.isolate / ctx.intercept', summary: 'Derive a child context (scoped services / isolation / interception).', source: 'vendor/cordis/src/context.ts:42' }, - { name: 'ctx.root / ctx.scope / ctx.fiber / ctx.registry / ctx.reflect / ctx.events / ctx.logger', summary: 'Ambient handles onto the running context graph.', source: 'vendor/cordis/src/context.ts:16' }, - { name: 'ctx.timer (+ interval / timeout / throttle / debounce / setTimeout / setInterval)', summary: 'Disposable timer helpers. The `timer` key is provided at runtime; the six helpers are mixed onto ctx directly (declared via Pick).', source: 'vendor/timer/src/index.ts:4' }, - { name: 'ctx.loader', summary: 'The config Loader that booted the app (present under the loader).', source: 'vendor/loader/src/index.ts:30' }, - { name: 'ctx.hmr', summary: 'The hot-module-reload watcher (present under the hmr plugin).', source: 'vendor/hmr/src/index.ts:15' }, -] - -/** Render the cross-link "Types:" line for a signature, or '' if none apply. */ -function typeLinks(signature: string): string { - const seen = new Set() - for (const name of Object.keys(LINK_MAP)) { - if (new RegExp(`\\b${name}\\b`).test(signature)) seen.add(name) - } - if (seen.size === 0) return '' - const links = [...seen].sort().map(n => `[${n}](../subsystems/${LINK_MAP[n]})`) - return `Types: ${links.join(' · ')}` -} - -/** Render one harness event entry. */ -function renderEvent(e: EventEntry): string[] { - const out = [`### \`${e.name}\` — ${e.mode}`, ''] - if (e.doc) out.push(e.doc, '') - out.push('```' + FENCE, e.jsDoc, e.signature, '```', '') - const links = typeLinks(e.signature) - if (links) out.push(links, '') - out.push(`Source: [\`${e.source}\`](../../${e.source.split(':')[0]})`, '') - return out -} - -/** Render one harness service entry. */ -function renderService(s: ServiceEntry): string[] { - const kind = s.abstract ? ' (abstract seam)' : '' - const out = [`## \`ctx.${s.key}\` — \`${s.type}\`${kind}`, ''] - if (s.doc) out.push(s.doc, '') - if (s.methods.length) { - const declarations = s.methods.flatMap((method, index) => [ - ...(index > 0 ? [''] : []), - method.jsDoc, - method.signature, - ]) - out.push('```' + FENCE, ...declarations, '```', '') - const links = typeLinks(s.methods.map(method => method.signature).join('\n')) - if (links) out.push(links, '') - } - out.push(`Source: [\`${s.source}\`](../../${s.source.split(':')[0]})`, '') - return out -} - -/** The shared generated-file banner comment. */ -const BANNER = [ - '', - '', -] - -/** The shared GENERATED + freshness-gate + fence notice paragraph. */ -const GATE_NOTICE = '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.' - -/** Render the events catalog (pure, deterministic given sorted inputs). */ -export function renderEvents(events: EventEntry[]): string { - const lines: string[] = [ - ...BANNER, - '# Cordis Events Catalog', - '', - 'Every cordis event a plugin can listen to: exact signature, dispatch mode, and original declaration JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.` surface is the sibling [services catalog](services.md), and [subsystems/](../subsystems/core.md) catalogs the *data structures* these signatures move around.', - '', - 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 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`), **bail** (synchronous in-order dispatch until one listener returns a bail value; the scoped input-mutation events use it for an applied/not-applied answer).', - '', - ] - const scopes = [...new Set(events.map(e => e.scope))].sort() - for (const scope of scopes) { - lines.push(`## \`${scope}/*\``, '') - for (const e of events.filter(x => x.scope === scope).sort((a, b) => a.name.localeCompare(b.name))) { - lines.push(...renderEvent(e)) - } - } - lines.push( - '## Inherited events (cordis core + loader/hmr/timer)', - '', - 'The framework events every plugin also sees, beyond the harness vocabulary above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of the event bus, without elevating framework internals to the harness tier\'s prominence.', - '', - ) - for (const e of INHERITED_EVENTS) { - lines.push(`- \`${e.name}\` — ${e.summary} ([\`${e.source}\`](../../${e.source.split(':')[0]}))`) - } - lines.push('') - return lines.join('\n') -} - -/** Render the services catalog (pure, deterministic given sorted inputs). */ -export function renderServices(services: ServiceEntry[]): string { - const lines: string[] = [ - ...BANNER, - '# Cordis Services Catalog', - '', - 'Every `ctx.` service a plugin can call: the exact public interface with original method JSDoc, plus the class JSDoc. This is one axis of the **wiring** reference a plugin author works against — the events a plugin listens to are the sibling [events catalog](events.md), and [subsystems/](../subsystems/core.md) catalogs the *data structures* these signatures move around. An abstract seam (e.g. `ctx.bash`) is implemented by a separate package; the interface is what consumers code against.', - '', - 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. 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)) - lines.push( - '## Inherited `ctx` members (cordis core + loader/hmr/timer)', - '', - 'The framework `ctx` surface every plugin also sees, beyond the harness services above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of what `ctx` offers, without elevating framework internals to the harness tier\'s prominence.', - '', - ) - for (const s of INHERITED_SERVICES) { - lines.push(`- \`${s.name}\` — ${s.summary} ([\`${s.source}\`](../../${s.source.split(':')[0]}))`) - } - lines.push('') - return lines.join('\n') -} - -/** CLI entry: `--write` (default) writes both catalogs, `--check` fails if - * either is stale. Guarded behind an entry-point check so importing this module - * for tests neither regenerates the committed files nor calls process.exit. */ -function main(): void { +export function main(): void { + const { projector, model } = projectCordisCatalog(root, CORDIS_CATALOG_POLICY) const outputs: [string, string][] = [ - [OUT_EVENTS, renderEvents(collectEvents())], - [OUT_SERVICES, renderServices(collectServices())], + [OUT_EVENTS, renderEvents([...model.events], CORDIS_CATALOG_POLICY)], + [OUT_SERVICES, renderServices([...model.services], CORDIS_CATALOG_POLICY)], + [OUT_RUNTIME_API, projector.renderRuntimeApi(model)], ...renderCordisCoreApiPages(), ] if (process.argv.includes('--check')) { @@ -722,9 +369,7 @@ function main(): void { try { committed = readFileSync(resolve(root, out), 'utf8') } catch { - // Only ENOENT (not yet generated) is expected; a present-but-unreadable - // file is not a state this repo produces. Either way the remedy is the - // same — regenerate — so treat a read failure as "stale". + // Only ENOENT is expected; either read failure has the same remedy. committed = null } if (committed !== content) stale.push(out) @@ -745,7 +390,4 @@ function main(): void { console.log(`gen-cordis-catalog: wrote ${outputs.length} generated file(s).`) } -// Run only when invoked as a script, not when imported by a test. -if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) { - main() -} +if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) main() diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index e1f80b96b6..2bc097c88c 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -199,7 +199,7 @@ { "doc": "docs/subsystems/goal.md", "symbol": "GoalView", - "source": "packages/goal/goal/src/domain.ts" + "source": "packages/goal/goal/src/types.ts" }, { "doc": "docs/subsystems/goal.md", @@ -219,12 +219,12 @@ { "doc": "docs/subsystems/goal.md", "symbol": "CreateGoalRequest", - "source": "packages/goal/goal/src/domain.ts" + "source": "packages/goal/goal/src/types.ts" }, { "doc": "docs/subsystems/goal.md", "symbol": "EditGoalRequest", - "source": "packages/goal/goal/src/domain.ts" + "source": "packages/goal/goal/src/types.ts" }, { "doc": "docs/subsystems/goal.md", @@ -1508,7 +1508,7 @@ { "doc": "docs/subsystems/permission.md", "symbol": "PresetOption", - "source": "packages/interaction/permission/src/index.ts" + "source": "packages/interaction/permission/src/types.ts" }, { "doc": "docs/subsystems/plan.md", @@ -1575,31 +1575,6 @@ "symbol": "DomainChanged", "source": "packages/storage/storage-domain/src/events.ts" }, - { - "doc": "docs/subsystems/tui.md", - "symbol": "TuiOverlayRequest", - "source": "packages/interaction/tui/src/extension.ts" - }, - { - "doc": "docs/subsystems/tui.md", - "symbol": "TuiOverlayHost", - "source": "packages/interaction/tui/src/extension.ts" - }, - { - "doc": "docs/subsystems/tui.md", - "symbol": "TuiOverlaySession", - "source": "packages/interaction/tui/src/extension.ts" - }, - { - "doc": "docs/subsystems/tui.md", - "symbol": "TuiOverlayCloseReason", - "source": "packages/interaction/tui/src/extension.ts" - }, - { - "doc": "docs/subsystems/tui.md", - "symbol": "TuiOverlayOutcome", - "source": "packages/interaction/tui/src/extension.ts" - }, { "doc": "docs/subsystems/workspace.md", "symbol": "WorkspaceId", @@ -1623,17 +1598,17 @@ { "doc": "docs/subsystems/telemetry.md", "symbol": "TelemetrySeverity", - "source": "packages/telemetry/session-telemetry/src/index.ts" + "source": "packages/session/session-telemetry/src/index.ts" }, { "doc": "docs/subsystems/telemetry.md", "symbol": "TelemetryRecord", - "source": "packages/telemetry/session-telemetry/src/index.ts" + "source": "packages/session/session-telemetry/src/index.ts" }, { "doc": "docs/subsystems/telemetry.md", "symbol": "TelemetryBackend", - "source": "packages/telemetry/session-telemetry/src/index.ts" + "source": "packages/session/session-telemetry/src/index.ts" }, { "doc": "docs/subsystems/typert.md",