Merge worktree/ci-native-windows-20260808 into worktree/ci-native-windows-coverage-20260808
This commit is contained in:
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md
|
||||
2026-06-24-web-capability-seam.md: b705236690859961ed69b307dbb59ebefcbd65ac
|
||||
2026-06-24-web-capability-seam.zh.md: e3dc836004bf785c6811e4c4014e105266dbaade
|
||||
2026-06-24-web-capability-seam.zh.md: 15a16b12119f69cec632e219585600d261fda54f
|
||||
|
||||
@@ -322,6 +322,8 @@ SSRF/私有网络防护(阻断私有、回环、链路本地、多播及其他
|
||||
|
||||
**大量 web 内容可能损害上下文质量。** 提供方强制执行字节/字符上限并报告 `truncated`;`tool-web` 格式化有界的模型输出,附带清晰的继续或后续引导。
|
||||
|
||||
<a id="deferred-work"></a>
|
||||
|
||||
## 推迟工作
|
||||
|
||||
- `web_fetch` 的 SSRF/私有网络防护:阻断私有、回环、链路本地、多播及其他非公开目的地,使 `web_fetch` 不再是 SSRF 原语。正确实现不仅仅是 URL 字符串检查——需要先 DNS 解析再连接到已验证的 IP(防御 DNS rebinding/TOCTOU)、跨重定向的每跳重新验证,以及 IPv6 边缘处理(私有范围、IPv4 映射地址)。所调研的参考实现均未做 IP 级阻断(OpenCode 做前缀检查后直接 fetch;Claude Code 依赖集中式主机名黑名单加「私有 URL 会失败」的提示词),因此没有可复制的实现,且这是 harness 唯一的 SSRF 防线——值得一次专门的设计/spike。在其落地之前,`web_fetch` 只能在无法触达敏感内部目标的部署中启用。
|
||||
|
||||
@@ -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 .agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.md
|
||||
2026-07-30-static-repository-plugin-format.md: c9d755b925a6ea05eed71e75803397d2672df9f4
|
||||
2026-07-30-static-repository-plugin-format.zh.md: 361de64d2e98b9fb4ac42963e4ae48e77fbc7016
|
||||
2026-07-30-static-repository-plugin-format.md: c66ee111eb0cac9e0d6c54581855ffc18efc8611
|
||||
2026-07-30-static-repository-plugin-format.zh.md: 969d7eb536158137807826eba2313670a7d37580
|
||||
|
||||
@@ -6,27 +6,27 @@ English | [中文](2026-07-30-static-repository-plugin-format.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A repository that already contains reusable skills or an MCP server declaration should be usable by standalone Harness applications without becoming a Harness SDK project or rewriting its existing layout. Popular repositories must be able to add one `.dsh-plugin` directory while keeping their current skills and `.mcp.json` elsewhere in the tree. At the same time, treating an arbitrary repository entry point as a Cordis Plugin would make every repository a new unrestricted runtime extension surface and would bypass the existing skill and MCP lifecycle owners.
|
||||
A repository that already contains reusable skills or an MCP server declaration should be usable by standalone Harness applications without becoming a Harness SDK project or rewriting its existing layout. Popular repositories must be able to add one `.dsh-plugin` directory while keeping their current skills and `.mcp.json` elsewhere in the tree. These portable static contributions still need to reuse the existing skill and MCP lifecycle owners when the same trusted package also carries native Cordis code.
|
||||
|
||||
The [package-manager-native repository cache](2026-07-30-package-manager-native-repository-cache.md) prepares an exact package source but intentionally knows nothing about DSH formats. This layer therefore needs a package-manager-compatible authoring format, a deterministic prepared artifact, and a Cordis composition that stays transactional under Loader disposal and replacement.
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-repository-plugin` owns a restricted `.dsh-plugin` package format with two contribution kinds only: skill roots and one common `.mcp.json`. Its package metadata uses `package.json#dsh.skills` for relative skill-root paths and `package.json#dsh.mcpServers` for the relative MCP document path. At least one is required. Each path may leave `.dsh-plugin` to reuse repository content but must remain beneath the directory containing that `.dsh-plugin`; a nested selectable Plugin therefore owns the adjacent subtree above its package without gaining access to unrelated host paths.
|
||||
`@deepseek-ai/dsh-repository-plugin` owns the static contribution subformat inside a `.dsh-plugin` package: skill roots and one common `.mcp.json`. Its package metadata uses `package.json#dsh.skills` for relative skill-root paths and `package.json#dsh.mcpServers` for the relative MCP document path. Each path may leave `.dsh-plugin` to reuse repository content but must remain beneath the directory containing that `.dsh-plugin`; a nested selectable Plugin therefore owns the adjacent subtree above its package without gaining access to unrelated host paths. The package may additionally declare the explicit code entry owned by the [trusted repository package decision](2026-08-08-trusted-repository-package-code.md), and at least one code or static contribution is required.
|
||||
|
||||
The `.dsh-plugin` package declares `dsh-plugin-prepare` as its ordinary package-manager `prepare` script. The helper validates metadata and source types, strictly parses `.mcp.json`, copies static assets into `dsh-plugin-assets`, and writes `dsh-plugin.mjs`. The `.mjs` extension avoids imposing `type: module` on repository-authored package metadata. The generated module is a fixed import-free template containing only a normalized manifest, an `inject` list derived from it (`loader`, plus `skills` and/or `tools` per the declared capabilities, so the wrapper fiber gates on the services its children need), and delegation to the `dsh-repository-plugin` Loader builtin. Preparation never discovers, transpiles, bundles, or preserves a custom repository entry point.
|
||||
The `.dsh-plugin` package declares the published `@deepseek-ai/dsh-repository-plugin` package as a development dependency and a non-empty `scripts.prepack` that invokes its `dsh-plugin-prepare` executable. During Git installation, pnpm installs that dependency from the selected package's own manifest; `prepack` runs after dependency installation and before pnpm packs a selected subdirectory, including a Plugin nested inside another package-manager workspace. The package may build its code first. The helper validates metadata and source types, strictly parses `.mcp.json`, copies static assets into `dsh-plugin-assets`, and writes `dsh-plugin.mjs`; the source loader revalidates the installed package's helper-bearing lifecycle metadata before importing that wrapper. A static-only package still receives an import-free wrapper containing its normalized manifest, service-derived `inject` list, and delegation to the `dsh-repository-plugin` Loader builtin. The dependency and workspace-isolation rationale is in the [Git source preparation repair](../bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md).
|
||||
|
||||
Loading the DSH package registers that builtin as an effect. A generated wrapper mounts the builtin as its child with `import.meta.url`, so all contributions belong to the wrapper fiber and disappear on Loader removal or rollback. The builtin revalidates the prepared manifest and path containment before reading assets. It composes the existing implementations rather than registering skills or MCP tools itself.
|
||||
|
||||
Each prepared skill set mounts `dsh-skill-local` with a unique `repository:<package-name>` provider name, only the copied custom roots, and watching disabled. `dsh-skill-local` therefore gains two general configuration fields: `providerName` and `includeDefaultRoots`. Their defaults preserve its existing single local provider; repository instances set a distinct name and exclude project/user roots so multiple instances neither collide nor duplicate host-local discovery.
|
||||
|
||||
Each `.mcp.json` server becomes one existing `dsh-mcp-client` child. The adapter accepts the common root `{ "mcpServers": ... }`; stdio definitions allow only optional `type: "stdio"`, `command`, `args`, and `env`, while HTTP definitions allow only `type: "http"`, `url`, and `headers`. Exact `${NAME}` process-environment references expand at runtime, after cache preparation; missing names fail Plugin load. HTTP maps to the client's Streamable HTTP transport, and stdio uses the prepared package directory as `cwd`. The existing client alone owns connection attempts, failure logging, remote tool synchronization, tool calls, and disconnects. Consequently an MCP connection failure keeps its established successful-plugin/no-tools behavior and is not reclassified as a repository preparation or Loader failure.
|
||||
Each `.mcp.json` server becomes one existing `dsh-mcp-client` child. The adapter accepts the common root `{ "mcpServers": ... }`; stdio definitions allow only optional `type: "stdio"`, `command`, `args`, and `env`, while HTTP definitions allow only `type: "http"`, `url`, and `headers`. Exact `${NAME}` process-environment references expand at runtime, after cache preparation; missing names fail Plugin load. HTTP maps to the client's Streamable HTTP transport, and stdio uses the prepared package directory as `cwd`. The existing client alone owns connection attempts, failure logging, remote tool synchronization, tool calls, and disconnects. Repository instances enable strict startup, so an initial connection, discovery, or tool-registration failure rejects the repository Loader generation; non-strict standalone clients retain the logged successful-plugin/no-tools behavior.
|
||||
|
||||
Unknown MCP fields reject. This intentionally excludes OAuth, `auth` objects, `CLAUDE_PLUGIN_ROOT`, and a broader Claude compatibility contract. Hooks, commands, agents, apps, arbitrary Cordis code, marketplaces, and discovery are also unsupported. Repository subdirectory selection and GitHub source configuration belong to the [standalone app integration](../feature/2026-07-30-config-only-repository-plugins.md), not this format package.
|
||||
Unknown MCP fields reject. This intentionally excludes OAuth, `auth` objects, `CLAUDE_PLUGIN_ROOT`, and a broader Claude compatibility contract. Commands, hooks, agents, rules, and other foreign manifest conventions are not inferred from static repository layout; DSH-native behavior uses the explicit trusted Cordis entry. Repository subdirectory selection and GitHub source configuration belong to the [standalone app integration](../feature/2026-07-30-config-only-repository-plugins.md), not this static adapter.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Load a repository's own Cordis entry point.** Rejected because it makes the advertised static format an unrestricted code-loading API, requires repository authors to depend on Harness internals, and duplicates the ordinary SDK/plugin-dependency path.
|
||||
**Discover an entry from `main`, `exports`, or repository layout.** Rejected because static assets do not imply that a package's ordinary entry is a Cordis Plugin. Trusted code loading is explicit through `dsh.entry` and remains outside this static adapter's ownership.
|
||||
|
||||
**Teach generated wrappers to implement skills and MCP directly.** Rejected because copied runtime code would drift from `dsh-skill-local` and `dsh-mcp-client`, especially their provider invalidation, tool synchronization, failure, and teardown contracts.
|
||||
|
||||
@@ -34,16 +34,16 @@ Unknown MCP fields reject. This intentionally excludes OAuth, `auth` objects, `C
|
||||
|
||||
**Watch prepared repository assets.** Rejected because an exact repository cache generation is immutable. Ref, subdirectory, or configuration changes select a new generation; a second watcher would create an unowned refresh identity.
|
||||
|
||||
**Treat MCP connect failures as Loader update failures.** Rejected because the existing MCP client deliberately contains connect failures and exposes no tools. Changing that semantic only for repository sources would create two failure contracts for the same server configuration.
|
||||
**Make every MCP connect failure a Loader update failure.** Rejected because optional standalone MCP clients deliberately contain startup failures and expose no tools. The MCP client instead owns an explicit strict-startup option, which repository adapters enable for their declared servers.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Existing skill/MCP repositories can add a small `.dsh-plugin/package.json` without relocating their assets or adopting an SDK project.
|
||||
- Prepared output is deterministic static glue, while the configured repository and its dependency lifecycle remain trusted executable package-manager input rather than a sandbox.
|
||||
- Prepared static output is deterministic glue, while an optional `dsh.entry` and the configured repository lifecycle remain trusted executable package-manager input rather than a sandbox.
|
||||
- Multiple repository Plugins coexist through provider names and ordinary MCP server-name uniqueness; duplicate names fail through their existing registries and participate in Loader rollback.
|
||||
- Cached source edits do not appear live. Another exact source/ref/path/config selection is required.
|
||||
- Adding another contribution kind requires an explicit format and DSH-owned runtime consumer; it cannot arrive as repository JavaScript by accident.
|
||||
- Adding another portable static contribution kind requires an explicit format and DSH-owned runtime consumer; DSH-native behavior uses the separate explicit code entry.
|
||||
|
||||
## Testing
|
||||
|
||||
Focused tests prepare skills and MCP metadata, prove the emitted wrapper contains no imports, reject Work IQ-style OAuth fields, map Expo-style HTTP and DataJunction-style stdio plus environment values, and exercise missing variables. A real Loader test mounts a generated wrapper through the registered builtin, reads its skill through `ctx.skills`, removes the Loader entry, and observes provider cleanup. The keyless headless example loads a checked-in prepared wrapper through its real `cordis.yml` and snapshots the repository skill's logged model catalog row.
|
||||
Focused tests prepare skills and MCP metadata, prove a static-only wrapper contains no imports, reject Work IQ-style OAuth fields, map Expo-style HTTP and DataJunction-style stdio plus environment values, and exercise missing variables. A real Loader test mounts a generated wrapper through the registered builtin, reads its skill through `ctx.skills`, removes the Loader entry, and observes provider cleanup. The CI built-entry acceptance invokes `dsh run` with a GitHub source pinned to the pull request head and observes the copied skill alongside the trusted code and MCP proofs owned by the superseding decision.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Note:静态 repository Plugin 格式
|
||||
# Agent Note: 静态 repository Plugin 格式
|
||||
|
||||
状态:已实现
|
||||
|
||||
@@ -6,27 +6,27 @@
|
||||
|
||||
## 问题
|
||||
|
||||
一个已经包含可复用 skills 或 MCP server 声明的仓库,应当能被独立 Harness 应用使用,而不必先变成 Harness SDK 项目,也不应被迫改写现有布局。常见仓库只需新增一个 `.dsh-plugin` 目录,同时仍可把原有 skills 与 `.mcp.json` 放在仓库其他位置。与此同时,如果把任意仓库入口都当作 Cordis Plugin,就会让每个仓库成为新的无限制运行时扩展表面,并绕过现有的 skill 与 MCP 生命周期所有者。
|
||||
一个已经包含可复用 skills 或 MCP server 声明的仓库,应当能被独立 Harness 应用使用,而不必先变成 Harness SDK 项目,也不应被迫改写现有布局。常见仓库只需新增一个 `.dsh-plugin` 目录,同时仍可把原有 skills 与 `.mcp.json` 放在仓库其他位置。当同一个受信任包还携带原生 Cordis 代码时,这些可移植静态贡献仍需复用现有的 skill 与 MCP 生命周期所有者。
|
||||
|
||||
[Package-manager-native repository cache](2026-07-30-package-manager-native-repository-cache.md) 会准备一个精确 package source,但有意不了解任何 DSH 格式。因此本层需要一种兼容 package manager 的创作格式、确定性的已准备产物,以及在 Loader dispose 和替换期间仍保持事务性的 Cordis 组合。
|
||||
|
||||
## 决策
|
||||
|
||||
`@deepseek-ai/dsh-repository-plugin` 负责一个受限的 `.dsh-plugin` package 格式,且只允许两类贡献:skill 根和一个通用 `.mcp.json`。Package metadata 使用 `package.json#dsh.skills` 声明相对 skill 根路径,使用 `package.json#dsh.mcpServers` 声明相对 MCP 文档路径;两者至少需要一个。路径可以离开 `.dsh-plugin` 以复用仓库内容,但必须留在包含该 `.dsh-plugin` 的目录之下;因此,一个嵌套且可选择的 Plugin 可以拥有其 package 上方相邻的子树,却不能访问无关宿主路径。
|
||||
`@deepseek-ai/dsh-repository-plugin` 负责 `.dsh-plugin` 包内的静态贡献子格式:skill 根和一个通用 `.mcp.json`。其包元数据使用 `package.json#dsh.skills` 声明相对 skill 根路径,使用 `package.json#dsh.mcpServers` 声明相对 MCP 文档路径。每条路径都可以离开 `.dsh-plugin` 以复用仓库内容,但必须留在包含该 `.dsh-plugin` 的目录之下;因此,一个嵌套且可选择的插件可以拥有其包上方相邻的子树,却不能访问无关宿主路径。该包还可以声明由[受信任 repository 包决策](2026-08-08-trusted-repository-package-code.md)负责的显式代码入口,并且至少需要一种代码或静态贡献。
|
||||
|
||||
`.dsh-plugin` package 把 `dsh-plugin-prepare` 声明为普通 package-manager `prepare` 脚本。Helper 会校验 metadata 与源码类型,严格解析 `.mcp.json`,把静态资源复制到 `dsh-plugin-assets`,并写入 `dsh-plugin.mjs`。`.mjs` 扩展名避免强迫仓库作者在 package metadata 中设置 `type: module`。生成模块来自固定、无 import 的模板,只包含规范化 manifest、由 manifest 派生的 `inject` 列表(`loader`,加上按声明能力加入的 `skills`/`tools`,使包装 fiber 在其子插件所需服务上门控),以及对 `dsh-repository-plugin` Loader builtin 的委托。准备阶段永远不会发现、转译、打包或保留自定义仓库入口。
|
||||
`.dsh-plugin` 包将已发布的 `@deepseek-ai/dsh-repository-plugin` 包声明为开发依赖,并声明非空 `scripts.prepack` 来调用其 `dsh-plugin-prepare` 可执行文件。在 Git 安装期间,pnpm 会按所选包自身的 manifest(元数据清单)安装该依赖;`prepack` 会在依赖安装后、pnpm 打包选定子目录前运行,即使插件嵌套在另一个包管理器工作区内也不例外。包可以先构建其代码。该辅助程序会校验元数据与源码类型,严格解析 `.mcp.json`,把静态资源复制到 `dsh-plugin-assets`,并写入 `dsh-plugin.mjs`;源码 loader 会在导入该包装层前重新校验已安装包的生命周期元数据是否包含辅助命令。仅含静态贡献的包仍会获得无 import 包装层,其中包含规范化 manifest、由服务派生的 `inject` 列表,以及对 `dsh-repository-plugin` Loader builtin 的委托。依赖与 workspace 隔离的设计依据见[Git 源准备修复](../bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md)。
|
||||
|
||||
加载 DSH package 会以 effect 方式注册该 builtin。生成的包装模块使用 `import.meta.url` 把 builtin 挂载为自己的子级,因此所有贡献都归属于包装 fiber,并在 Loader 移除或回滚时消失。Builtin 会在读取资源前重新校验已准备 manifest 与路径包含关系。它只组合现有实现,而不自行注册 skills 或 MCP 工具。
|
||||
|
||||
每份已准备 skill 集合都会挂载 `dsh-skill-local`,使用唯一的 `repository:<package-name>` 提供方名称、仅包含复制后的自定义根,并禁用监视。因此 `dsh-skill-local` 新增两个通用配置字段:`providerName` 和 `includeDefaultRoots`。默认值保持原有单一本地提供方行为;repository 实例设置不同名称并排除项目/用户根,使多个实例既不冲突,也不会重复宿主本地发现。
|
||||
|
||||
`.mcp.json` 中的每个 server 都变成一个现有 `dsh-mcp-client` 子级。适配层接受通用根对象 `{ "mcpServers": ... }`;stdio 定义只允许可选的 `type: "stdio"`、`command`、`args` 与 `env`,HTTP 定义只允许 `type: "http"`、`url` 与 `headers`。严格的 `${NAME}` 进程环境变量引用在运行时、cache 准备之后展开;缺失变量会使 Plugin 加载失败。HTTP 映射到 client 的 Streamable HTTP transport,stdio 使用已准备 package 目录作为 `cwd`。只有现有 client 负责连接尝试、失败日志、远端工具同步、工具调用和断开。因此 MCP 连接失败会继续沿用“Plugin 成功但不注册工具”的既有行为,不会被重新分类为 repository 准备或 Loader 失败。
|
||||
`.mcp.json` 中的每个 server 都变成一个现有 `dsh-mcp-client` 子级。适配层接受通用根对象 `{ "mcpServers": ... }`;stdio 定义只允许可选的 `type: "stdio"`、`command`、`args` 与 `env`,HTTP 定义只允许 `type: "http"`、`url` 与 `headers`。严格的 `${NAME}` 进程环境变量引用在运行时、cache 准备之后展开;缺失变量会使 Plugin 加载失败。HTTP 映射到 client 的 Streamable HTTP transport,stdio 使用已准备 package 目录作为 `cwd`。只有现有 client 负责连接尝试、失败日志、远端工具同步、工具调用和断开。Repository 实例会启用严格启动,因此初始连接、发现或工具注册失败会拒绝 repository Loader generation;非严格的独立 client 则保留“记录日志、Plugin 成功但不注册工具”的行为。
|
||||
|
||||
未知 MCP 字段会被拒绝。这里有意排除 OAuth、`auth` 对象、`CLAUDE_PLUGIN_ROOT` 和更广泛的 Claude 兼容契约。Hooks、commands、agents、apps、任意 Cordis 代码、marketplace 和发现同样不受支持。Repository 子目录选择与 GitHub 源配置属于[独立应用集成](../feature/2026-07-30-config-only-repository-plugins.md),而不是本格式 package。
|
||||
未知 MCP 字段会被拒绝。这里有意排除 OAuth、`auth` 对象、`CLAUDE_PLUGIN_ROOT` 和更广泛的 Claude 兼容契约。命令、hook、agent(智能体)、规则和其他外来 manifest 约定不会从静态 repository 布局中推断出来;DSH 原生行为使用显式的受信任 Cordis 入口。Repository 子目录选择与 GitHub 源配置属于[独立应用集成](../feature/2026-07-30-config-only-repository-plugins.md),而不是本静态适配器。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**加载仓库自己的 Cordis 入口。** 拒绝,因为这会把宣传为静态的格式变成无限制代码加载 API,要求仓库作者依赖 Harness 内部实现,并重复普通 SDK/Plugin dependency 路径。
|
||||
**从 `main`、`exports` 或 repository 布局中发现入口。** 拒绝,因为静态资源并不表示包的普通入口就是 Cordis 插件。受信任代码通过 `dsh.entry` 显式加载,不属于该静态适配器的职责。
|
||||
|
||||
**让生成包装模块直接实现 skills 和 MCP。** 拒绝,因为复制的运行时代码会与 `dsh-skill-local` 和 `dsh-mcp-client` 漂移,尤其是提供方失效、工具同步、失败和 teardown 契约。
|
||||
|
||||
@@ -34,16 +34,16 @@
|
||||
|
||||
**监视已准备 repository 资源。** 拒绝,因为一个精确 repository cache generation 是不可变的。Ref、子目录或配置变化会选择新 generation;第二套 watcher 会创造一套没有所有者的刷新身份。
|
||||
|
||||
**把 MCP 连接失败当作 Loader 更新失败。** 拒绝,因为现有 MCP client 有意收束连接失败并不暴露工具。只对 repository source 改变该语义,会让同一 server 配置拥有两套失败契约。
|
||||
**把每次 MCP 连接失败都当作 Loader 更新失败。** 拒绝,因为可选的独立 MCP client 会有意收束启动失败,并且不暴露工具。MCP client 改为自行提供显式的严格启动选项,由 repository 适配器为其声明的 server 启用。
|
||||
|
||||
## 后果
|
||||
|
||||
- 现有 skill/MCP 仓库可以新增一个很小的 `.dsh-plugin/package.json`,无需移动资源或采用 SDK 项目。
|
||||
- 已准备输出是确定性的静态胶水;已配置仓库及其依赖生命周期仍是受信任的可执行 package-manager 输入,而非 sandbox。
|
||||
- 已准备的静态输出是确定性胶水;可选的 `dsh.entry` 和已配置的 repository 生命周期仍是受信任的可执行包管理器输入,而非沙箱。
|
||||
- 多个 repository Plugin 通过提供方名称和普通 MCP server-name 唯一性共存;重复名称经现有 registry 失败,并参与 Loader 回滚。
|
||||
- Cache 内的源码编辑不会实时出现;必须选择另一个精确 source/ref/path/config。
|
||||
- 新增贡献类型必须提供显式格式和 DSH 自有运行时消费方;它不能意外以 repository JavaScript 形式进入。
|
||||
- 新增可移植静态贡献类型必须提供显式格式和 DSH 自有运行时消费方;DSH 原生行为使用独立的显式代码入口。
|
||||
|
||||
## 测试
|
||||
|
||||
聚焦测试会准备 skills 与 MCP metadata,证明生成包装模块不含 import,拒绝 Work IQ 风格的 OAuth 字段,映射 Expo 风格 HTTP 与 DataJunction 风格 stdio 及环境变量,并覆盖缺失变量。真实 Loader 测试通过已注册 builtin 挂载生成包装模块,经 `ctx.skills` 读取其 skill,移除 Loader 条目并观察提供方清理。Keyless headless 示例通过真实 `cordis.yml` 加载一份签入的已准备包装模块,并快照 repository skill 写入日志的模型目录行。
|
||||
聚焦测试会准备 skill 与 MCP 元数据,证明仅含静态贡献的包装模块不含 import,拒绝 Work IQ 风格的 OAuth 字段,映射 Expo 风格 HTTP 与 DataJunction 风格 stdio 及环境变量,并覆盖缺失变量。真实 Loader 测试通过已注册 builtin 挂载生成包装模块,经 `ctx.skills` 读取其 skill,移除 Loader 条目并观察提供方清理。CI 构建入口验收会使用锁定到 PR(Pull Request)head 的 GitHub 源调用 `dsh run`,并观察已复制的 skill,以及由取代本决策的新决策所负责的受信任代码与 MCP 验证证据。
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-08-trusted-repository-package-code.md
|
||||
2026-08-08-trusted-repository-package-code.md: 387479b3b36a8bc5e145641ae40802b3090ced70
|
||||
2026-08-08-trusted-repository-package-code.zh.md: aff34d4485553f076c5dd1b71f0a3c5b5c62bc8f
|
||||
@@ -0,0 +1,51 @@
|
||||
# Agent Note: Trusted repository packages load Cordis code
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-08-trusted-repository-package-code.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The standalone repository format already installs a selected Git package and runs its dependency and lifecycle code with host authority, but it exposed only copied skills and MCP metadata to DSH. Forbidding a Cordis entry did not create a security boundary: package installation remained trusted executable code while the restriction prevented the package from contributing the Plugin behavior that the Harness architecture is designed to compose.
|
||||
|
||||
A repository author also needs to keep an ordinary TypeScript npm package shape. Requiring publication to npm, pre-generated JavaScript in Git, or a DSH-owned TypeScript compiler would make a Git source less capable than the same package installed through a developer-owned SDK project. The first model request must observe any MCP tools that this package starts; background-only initial discovery makes a successful installation nondeterministic at the application boundary.
|
||||
|
||||
## Decision
|
||||
|
||||
A configured repository package is trusted code. Its `.dsh-plugin/package.json` may declare `dsh.entry` as a relative path to a compiled ESM Cordis Plugin inside that package, alongside or instead of `dsh.skills` and `dsh.mcpServers`. At least one contribution is required. The entry may use namespace exports or a default export and retains ordinary Cordis semantics for `name`, `inject`, `Config`, registrations, startup failure, and effect-scoped teardown.
|
||||
|
||||
The package owns its npm dependencies and build toolchain. It declares the published `@deepseek-ai/dsh-repository-plugin` package to obtain the `dsh-plugin-prepare` executable. `scripts.prepack` is a non-empty package-authored command that must invoke that dependency-provided helper, but it may first run `tsc`, `tsdown`, or any other build. DSH neither injects the helper, parses the shell program, nor compiles repository source. The helper validates the metadata after the preceding build, requires the configured entry to resolve to a file within `.dsh-plugin`, validates and copies declared static assets, and writes the prepared `dsh-plugin.mjs` wrapper. The installed package must retain a `prepack` declaration containing that helper command; a missing dependency, wrapper, or build output fails before a cache generation becomes usable.
|
||||
|
||||
The generated wrapper first mounts the DSH-owned static runtime for skills and MCP definitions, then dynamically imports and unwraps the explicit entry and mounts it as a child. The wrapper statically declares dependencies implied by the prepared manifest; an entry module's additional `inject` is discovered only when mounted and must already be available in the host composition. Both children must reach Cordis `ACTIVE`; an unsatisfied `inject` or startup exception rejects the repository Loader transaction instead of committing an inert generation. Loader removal, failed replacement, and parent disposal unwind the entry, skill providers, MCP clients, and their effects together.
|
||||
|
||||
`dsh-mcp-client` resolves its initial connection and tool synchronization promise as part of Plugin application. Its entry is an `async function`, not an ordinary function returning a Promise: Cordis identifies prototype-bearing ordinary functions as constructors and does not treat a constructor's returned Promise as startup work. A valid server's tools therefore exist before its parent repository wrapper activates and before a one-shot application starts its first model request. Its `failOnStartupError` config preserves optional standalone servers by default while letting repository adapters require their declared servers. Repository-translated MCP clients enable that mode, so initial connection, discovery, or tool-registration failure rejects the candidate generation and rollback still closes the transport.
|
||||
|
||||
## Trust boundary
|
||||
|
||||
Exact refs, source containment, credential-shaped environment scrubbing, prepared manifests, and immutable cache keys protect identity and composition integrity; they do not sandbox executable package input. Repository lifecycle scripts, transitive npm dependencies, the compiled entry, and spawned MCP servers can exercise the authority available to the DSH process and the Cordis services they receive. Users must therefore trust the selected repository and should pin immutable refs and grant Git only the narrow read credential needed for acquisition.
|
||||
|
||||
Model-visible behavior remains governed by the owning DSH seam. A repository entry may register tools, prompt sections, policies, commands, agents, or other effects, but anything reaching a model request still needs the corresponding logged DSH representation and lifecycle cleanup. The repository format grants code loading; it does not weaken those service contracts.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep code forbidden while allowing arbitrary package lifecycles.** Rejected because installation already executes trusted repository code, so the restriction added no isolation and forced Plugin authors to publish or maintain a second integration path.
|
||||
|
||||
**Have DSH compile repository TypeScript.** Rejected because compiler choice, module layout, generated chunks, native dependencies, and package metadata belong to the npm package. Running the package's declared build preserves the same boundary as other Git dependencies.
|
||||
|
||||
**Import `main`, `exports`, or another discovered entry implicitly.** Rejected because an npm package may contain utilities or an MCP executable that is not a Cordis Plugin. The explicit `dsh.entry` field makes code activation reviewable and lets preparation validate the packed path.
|
||||
|
||||
**Add a closed manifest field for every future DSH contribution.** Rejected as the universal extension mechanism. Skills and common MCP files retain useful portable static adapters, while DSH-native behavior composes through the existing Cordis Plugin and service contracts.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A TypeScript DSH Plugin can live in a GitHub repository, install ordinary npm dependencies, compile during `prepack`, and run without publishing the Plugin package to npm.
|
||||
- Static-only repository packages remain valid and retain import-free wrappers; adding `dsh.entry` opts that package into runtime code import.
|
||||
- A package build, dependency install, entry import, unmet service, or Plugin startup failure prevents the candidate generation from replacing the last good configuration.
|
||||
- Initial MCP synchronization can lengthen application startup by the MCP SDK's per-request timeout, and a repository-declared server that is unavailable or cannot publish its complete tool generation prevents that candidate generation from activating.
|
||||
- Repository code receives host authority, so source review and immutable pinning are operational security requirements rather than optional hardening.
|
||||
|
||||
## Testing
|
||||
|
||||
Repository-format tests prepare and mount default-export code entries through the real Loader, observe an entry-owned service, remove the Loader row, and observe cleanup; they also retain skill/MCP preparation, containment, damaged-package, pending-service, and rollback coverage. MCP lifecycle tests require `apply` to settle only after initial tool publication, preserve opt-in contained startup failure, and prove strict connection or tool-registration rejection still closes the client.
|
||||
|
||||
The Node 24 consumer acceptance uses the actual built `dsh run` command with a fresh DSH home and an authenticated private GitHub source pinned to the pull request's exact head SHA. The test packs the current repository Plugin build with the same private-field removal and workspace-dependency pinning used for publication, serves its packument and tarball from a job-local npm registry, and directs the Git package's ordinary scoped npm resolution there. That repository package obtains `dsh-plugin-prepare` from the simulated published dependency, installs its other pinned runtime and development dependencies, type-checks and bundles TypeScript during `prepack`, prepares a skill plus a stdio MCP server and `dsh.entry`, exposes the skill and MCP schema in the first real model request, executes the MCP tool, and lets the compiled Cordis entry append a second marker to the result observed in the following request. Registry and cache assertions require npm resolution to reach the simulated publication, source files to be absent from the packed installation, and both built modules, their installed dependency, copied assets, and generated wrapper to be present.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Agent Note: 受信任 repository 包加载 Cordis 代码
|
||||
|
||||
状态:已实现
|
||||
|
||||
[English](2026-08-08-trusted-repository-package-code.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
独立 repository 格式已经会安装选定的 Git 包,并以宿主权限运行其依赖和生命周期代码,但它向 DSH 暴露的只有复制后的 skill(技能)和 MCP 元数据。禁止 Cordis 入口并未建立安全边界:包安装过程仍会执行受信任代码,而这项限制却阻止包贡献 Harness 架构本就用于组合的插件行为。
|
||||
|
||||
仓库作者还需要保持普通 TypeScript NPM 包的结构。如果要求发布到 NPM、把预生成的 JavaScript 签入 Git,或使用 DSH 自有的 TypeScript 编译器,Git 源的能力就会弱于通过开发者自有 SDK 项目安装的同一个包。首个模型请求必须看到该包启动的所有 MCP 工具;仅在后台进行初始发现,会让一次成功安装在应用边界上具有不确定性。
|
||||
|
||||
## 决策
|
||||
|
||||
已配置的 repository 包是受信任代码。其 `.dsh-plugin/package.json` 可以连同 `dsh.skills` 和 `dsh.mcpServers` 声明 `dsh.entry`,也可以用它取代二者;`dsh.entry` 是指向该包内已编译 ESM Cordis 插件的相对路径。至少需要一种贡献。入口可以使用 namespace 导出或 default export,并沿用 Cordis 对 `name`、`inject`、`Config`、注册、启动失败和 effect 作用域清理的常规语义。
|
||||
|
||||
包自行负责其 NPM 依赖和构建工具链。它声明已发布的 `@deepseek-ai/dsh-repository-plugin` 包以取得 `dsh-plugin-prepare` 可执行文件。`scripts.prepack` 是由包作者编写的非空命令,必须调用该依赖提供的辅助程序,但可以先运行 `tsc`、`tsdown` 或其他任意构建。DSH 不会注入辅助程序,也不会解析该 shell 程序或编译 repository 源码。辅助程序会在前序构建之后校验元数据,要求已配置入口解析到 `.dsh-plugin` 内的文件,校验并复制已声明的静态资源,再写入已准备的 `dsh-plugin.mjs` 包装层。已安装包必须保留包含该辅助命令的 `prepack` 声明;依赖、包装层或构建输出缺失会在缓存 generation 可用前导致失败。
|
||||
|
||||
生成的包装层先挂载 DSH 自有的静态运行时来处理 skill 和 MCP 定义,再动态导入显式入口、解包其导出并将其挂载为子级。包装层会静态声明已准备 manifest(元数据清单)所隐含的依赖;入口模块的额外 `inject` 只有在挂载时才会被发现,并且此时必须已存在于宿主组合中。两个子级都必须进入 Cordis `ACTIVE`;无法满足的 `inject` 或启动异常会拒绝 repository Loader 事务,而不会提交未激活的 generation。Loader 移除、替换失败和父级 dispose(资源释放)会一并撤销入口、skill 提供方、MCP client 及其 effect。
|
||||
|
||||
`dsh-mcp-client` 会在插件应用期间完成其初始连接和工具同步 promise。其入口必须是 `async function`,而不是返回 Promise 的普通函数:Cordis 会把带 prototype 的普通函数识别为 constructor,不会把 constructor 返回的 Promise 当作启动工作。因此,有效 server 的工具会在父级 repository 包装层激活前、一次性应用发起首个模型请求前就已存在。其 `failOnStartupError` 配置默认保留独立可选 server 的行为,同时允许 repository adapter 要求已声明 server 必须可用。Repository 转换出的 MCP client 会启用该模式,因此初始连接、发现或工具注册失败会拒绝候选 generation,回滚仍会关闭 transport。
|
||||
|
||||
## 信任边界
|
||||
|
||||
精确 ref、源路径包含约束、清除名称符合凭据模式的环境变量、已准备的 manifest 和不可变缓存键,可以保护身份与组合完整性;它们不会为可执行包输入提供沙箱隔离。Repository 生命周期脚本、传递性 NPM 依赖、已编译入口和 spawn 的 MCP server 可以行使 DSH 进程可用的权限,以及它们所获 Cordis 服务授予的权限。因此,用户必须信任所选仓库,应当固定不可变 ref,并只授予 Git 获取源码所需的最小只读凭据。
|
||||
|
||||
模型可见行为仍由所属 DSH seam 管理。repository 入口可以注册工具、提示词段落、策略、命令、agent(智能体)或其他 effect,但任何进入模型请求的内容仍须具有对应的 DSH 日志表示和生命周期清理。repository 格式授予代码加载能力;它不会削弱这些服务契约。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**继续禁止代码,但允许任意包生命周期。** 拒绝,因为安装过程本就执行受信任的 repository 代码,所以该限制没有提供隔离,反而迫使插件作者发布或维护第二条集成路径。
|
||||
|
||||
**由 DSH 编译 repository TypeScript。** 拒绝,因为编译器选择、模块布局、生成分片、原生依赖和包元数据属于 NPM 包。运行包所声明的构建,可以保持与其他 Git 依赖相同的边界。
|
||||
|
||||
**隐式导入 `main`、`exports` 或其他发现的入口。** 拒绝,因为 NPM 包可能包含并非 Cordis 插件的实用工具或 MCP 可执行文件。显式 `dsh.entry` 字段使代码激活可供评审,并让准备阶段校验打包后的路径。
|
||||
|
||||
**为未来每种 DSH 贡献添加封闭 manifest 字段。** 不采用它作为通用扩展机制。skill 和通用 MCP 文件仍保留有用的可移植静态适配器;DSH 原生行为则通过现有 Cordis 插件与服务契约组合。
|
||||
|
||||
## 后果
|
||||
|
||||
- TypeScript DSH 插件可以存放在 GitHub 仓库中,安装普通 NPM 依赖,在 `prepack` 期间完成编译,并在无需把插件包发布到 NPM 的情况下运行。
|
||||
- 仅含静态贡献的 repository 包仍然有效,并保留无 import 包装层;添加 `dsh.entry` 会使该包选择启用运行时代码导入。
|
||||
- 包构建、依赖安装、入口导入、所需服务未满足或插件启动失败,都会阻止候选 generation 替换最后一个可用配置。
|
||||
- 初始 MCP 同步可能因 MCP SDK 的单次请求超时而延长应用启动时间;repository 声明的 server 不可用或无法发布完整工具 generation 时,该候选 generation 无法激活。
|
||||
- Repository 代码获得宿主权限,因此源码评审和锁定不可变 ref 是运行安全要求,而不是可选加固措施。
|
||||
|
||||
## 测试
|
||||
|
||||
repository 格式测试通过真实 Loader 准备并挂载使用 default export 的代码入口,观察入口自有服务,移除 Loader 配置项,再观察清理;测试还保留针对 skill/MCP 准备、路径包含约束、包损坏、等待服务和回滚的覆盖。MCP 生命周期测试要求 `apply` 只在初始工具发布后完成,保留可选择启用的启动失败收束行为,并证明严格连接拒绝或工具注册拒绝仍会关闭 client。
|
||||
|
||||
Node 24 消费方验收使用实际构建的 `dsh run` 命令、全新 DSH 主目录,以及锁定到 PR(Pull Request)的精确 head SHA 且经过认证的私有 GitHub 源。测试会采用发布时相同的移除 `private` 字段和固定 workspace 依赖版本流程,对当前 repository 插件构建进行打包;再由作业本地 NPM 注册表提供其 `packument` 与 tarball,并把 Git 包的常规 scoped NPM 解析指向该注册表。该 repository 包从模拟发布的依赖取得 `dsh-plugin-prepare`,安装其他固定版本的运行时依赖与开发依赖,在 `prepack` 期间对 TypeScript 进行类型检查和打包,准备一个 skill、一个 stdio MCP server 及 `dsh.entry`,在首个真实模型请求中暴露 skill 与 MCP schema,执行 MCP 工具,并让已编译 Cordis 入口向结果追加第二个标记,供后续请求观察。注册表与缓存断言要求 NPM 解析必须命中模拟发布,打包安装中不存在源码文件,同时必须存在两个已构建模块、其已安装依赖、复制资源和生成包装层。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-09-cordis-event-walk-backstop.md
|
||||
2026-08-09-cordis-event-walk-backstop.md: 1d833858f9471c98f54b3793edd12e4710b8c0fc
|
||||
2026-08-09-cordis-event-walk-backstop.zh.md: 0cb0f3bd738b6cee703cee71f1c24741554df87a
|
||||
@@ -0,0 +1,38 @@
|
||||
# Agent Note: An independent Events backstop closes the cordis-surface exhaustiveness gap
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-09-cordis-event-walk-backstop.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`gen-cordis-catalog` renders every service and event the Typert host-face projection discovers, and fail-closed page maps (`SERVICE_PAGE`, `EVENT_SCOPE_PAGE`) guarantee each discovered key or scope lands on exactly one `docs/subsystems/` page ([per-subsystem regions decision](../process/2026-07-28-per-subsystem-cordis-surface-regions.md) owns the page-region mechanism). Discovery itself was only backstopped for services: an independent AST scan read every `declare module 'cordis'` Context merge and demanded each declared key be rendered or carry a named `SERVICE_WALK_EXEMPTIONS` reason.
|
||||
|
||||
Events had no such backstop. The projection walks only files reachable from host-face package exports, so an `interface Events` merge in client-face code — or in any file the host analyzer cannot reach — vanished with no trace: 12 declared events (`slash/input-*`, `theme/change`, `locale/change`, and the client runtime's `*/changed` invalidation signals) were documented nowhere generated and nothing would ever notice a thirteenth. The services scan also globbed only `packages/*/*/src/*.ts`, so 13 client-face Context keys declared in nested files (`src/client/**`) were invisible to the very scan meant to prevent silent vanishing.
|
||||
|
||||
## Decision
|
||||
|
||||
Events get the exact mirror of the services backstop, and both scans read the full package source tree.
|
||||
|
||||
`scripts/cordis-walk.ts` gains `eventNameList` (every member name of an `interface Events` merge, read from method and property members alike so a shape the projector would reject still enters the scan); the scan yields every `declare module 'cordis'` block in a file (the Typert analyzer reads them all, so stopping at the first would hide a second block's face), and its quote-agnostic prefilter matches the `declare module` heads instead of the literal text `interface Context`, so an Events-only or double-quoted merge file is not skipped. The scan glob in `gen-cordis-catalog` deepens from `packages/*/*/src/*.ts` to `packages/*/*/src/**/*.{ts,tsx}` (two patterns). A third partition direction guards the scan itself: every rendered service key and event name must also be visible to the scan, so a scan regression (glob, prefilter, block walk) is a hard error rather than a silent backstop decay.
|
||||
|
||||
A new curated `EVENT_WALK_EXEMPTIONS` map names every declared event the projection cannot see, with the reason and the package README that owns its surface. Keys are full event names, not scopes: client-face events share scopes with rendered host events (`commands/changed` beside the host `commands/*` family), so a scope-level exemption would mask a host-face regression. The partition check is fail-closed in both directions, exactly like the service maps: an unexempted invisible event, an exemption for an event that renders, and an exemption no merge declares are all hard errors.
|
||||
|
||||
The partition judgment moved out of `computeOutputs` into the pure `walkPartitionProblems(input, maps)` so every acceptance path is provable by unit test without running the Typert projection; `computeOutputs` feeds it the rendered model plus the independent scan and keeps aggregating page-splice errors as before.
|
||||
|
||||
The audit that motivated this found the host face already complete: 48 rendered services + 10 walk exemptions covered all 58 host-visible Context keys, all 49 host events rendered, and every type name in every rendered signature is classified by the existing fail-closed `LINK_MAP`/`FOUNDATION_TYPE_NAMES`/`TYPE_LINK_EXEMPTIONS` check. The 25 findings (12 events, 13 keys) were all client-face; each now carries a named exemption pointing at its owning README, consistent with the existing `appShell`/`connection` precedent.
|
||||
|
||||
## Verification
|
||||
|
||||
`scripts/gen-cordis-catalog-partition.spec.ts` proves each acceptance path: the green partition, an invisible unexempted event (named with its declaring file), a stale rendered-event exemption, a stale never-declared exemption, the service mirror of each, unmapped rendered surface in both page maps, rendered surface the scan cannot see (the third direction), and the scan reaching nested Events-only merges, every block of a multi-block file, double-quoted heads, and `.tsx` sources. Deleting one live exemption from the real tree makes `gen-cordis-catalog` fail loud with the event's name and declaring file; restoring it returns the generator to a byte-identical no-op regeneration (85 artifacts, 0 written), which also proves the new exemptions exactly cover today's surface. `verify-cordis-catalog` in doc-sync executes the partition on every run.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Render the client face instead of exempting it.** Analyzing `faces: ['host', 'client']` and giving client services/events generated regions is the real fix for the underlying blind spot, but it changes what the subsystems catalog IS (host-tier reference) and requires page decisions for browser-only surfaces; the existing `TODO(cordis-catalog-interface-services)` already tracks widening the projection. The backstop is the guarantee; rendering is an upgrade behind it.
|
||||
- **Scope-level event exemptions.** Smaller map, but `commands/changed` (client) shares the `commands` scope with rendered host events, so exempting a scope would swallow a future host-face event silently — the exact failure mode this note removes.
|
||||
- **Deriving exhaustiveness from Typert instead of a raw AST scan.** The projection and the backstop must fail independently: a Typert reachability bug is precisely what the backstop exists to catch, so the scan deliberately stays a plain `ts.createSourceFile` walk with no shared machinery.
|
||||
- **Gating the transitive type closure of rendered signatures.** Measured before deciding: every type name reachable in rendered signatures is already classified, and deeper field-of-field types are owned by the pages' hand-curated `type-equiv` pastes and package READMEs; a closure gate would force page homes for internals without a reader-facing need.
|
||||
|
||||
## Consequences
|
||||
|
||||
A new cordis event — host or client, any file depth — must either render onto a subsystems page or name itself in `EVENT_WALK_EXEMPTIONS` with its documentation owner; deleting one must retire its exemption. The same now holds for Context keys declared anywhere under `src/`. The curated maps grew by 25 client-face entries whose reasons all point at package READMEs, keeping the subsystems catalog a host-tier reference. `walkPartitionProblems` is the single home of the partition judgment; future backstop dimensions (e.g. rendering the client face, schema surfaces) extend it and its spec rather than re-inlining checks into `computeOutputs`.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Agent Note(agent 决策记录):独立的 Events 兜底扫描补上 cordis 表面完备性缺口
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-09-cordis-event-walk-backstop.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`gen-cordis-catalog` 渲染 Typert host face 投影发现的每个服务与事件,fail-closed 的页面映射(`SERVICE_PAGE`、`EVENT_SCOPE_PAGE`)保证每个被发现的 key 或 scope 恰好落在一个 `docs/subsystems/` 页面上(页面区块机制归[按子系统区块决定](../process/2026-07-28-per-subsystem-cordis-surface-regions.md)所有)。但"发现"本身此前只对服务有兜底:一条独立的 AST 扫描读取每个 `declare module 'cordis'` Context merge,要求每个声明的 key 要么被渲染、要么在 `SERVICE_WALK_EXEMPTIONS` 中给出具名理由。
|
||||
|
||||
事件没有这样的兜底。投影只遍历从 host face 包导出可达的文件,因此 client face 代码——或 host 分析器无法触及的任何文件——里的 `interface Events` merge 会无声消失:12 个已声明事件(`slash/input-*`、`theme/change`、`locale/change`,以及 client runtime 的 `*/changed` 失效信号)不出现在任何生成文档中,而且再多一个也不会有任何机制察觉。服务扫描的 glob 也只有 `packages/*/*/src/*.ts`,于是声明在嵌套文件(`src/client/**`)中的 13 个 client face Context key 恰恰对这条为防止无声消失而存在的扫描不可见。
|
||||
|
||||
## Decision
|
||||
|
||||
事件获得与服务兜底完全对称的机制,且两条扫描都读取完整的包源码树。
|
||||
|
||||
`scripts/cordis-walk.ts` 新增 `eventNameList`(`interface Events` merge 的每个成员名,方法与属性成员一并读取,使投影器会拒绝的形状也进入扫描);扫描产出文件中每一个 `declare module 'cordis'` 块(Typert 分析器读取全部块,止步于第一个会藏起第二个块的表面),其对引号风格不敏感的预过滤匹配 `declare module` 头部而非字面文本 `interface Context`,从而不再跳过只含 Events 或使用双引号的 merge 文件。`gen-cordis-catalog` 的扫描 glob 从 `packages/*/*/src/*.ts` 加深为 `packages/*/*/src/**/*.{ts,tsx}`(两个 pattern)。分区新增第三个方向守住扫描自身:投影渲染的每个服务 key 与事件名也必须对扫描可见,使扫描回归(glob、预过滤、块遍历)成为硬错误而非兜底的无声退化。
|
||||
|
||||
新的人工维护映射 `EVENT_WALK_EXEMPTIONS` 为投影看不到的每个已声明事件命名,附理由与拥有其表面的包 README。键是完整事件名而非 scope:client face 事件与已渲染的 host 事件共享 scope(`commands/changed` 与 host 的 `commands/*` 家族并存),scope 级豁免会无声吞掉未来的 host face 回归。分区检查与服务映射一样双向 fail-closed:未豁免的不可见事件、已渲染事件的豁免、无任何 merge 声明的豁免,皆为硬错误。
|
||||
|
||||
分区判定从 `computeOutputs` 中提取为纯函数 `walkPartitionProblems(input, maps)`,使每条验收路径都能以单元测试证明而无需运行 Typert 投影;`computeOutputs` 向它馈送渲染模型加独立扫描结果,页面拼接错误的聚合方式保持不变。
|
||||
|
||||
促成本决定的审计发现 host face 本已完备:48 个渲染服务 + 10 条 walk 豁免覆盖全部 58 个 host 可见 Context key,49 个 host 事件全部渲染,且每个渲染签名中的每个类型名都已被既有的 fail-closed `LINK_MAP`/`FOUNDATION_TYPE_NAMES`/`TYPE_LINK_EXEMPTIONS` 检查分类。25 条发现(12 事件、13 key)全部在 client face;现在每条都带指向其所属 README 的具名豁免,与既有的 `appShell`/`connection` 先例一致。
|
||||
|
||||
## Verification
|
||||
|
||||
`scripts/gen-cordis-catalog-partition.spec.ts` 证明每条验收路径:绿色分区、不可见且未豁免的事件(报出声明文件)、已渲染事件的陈旧豁免、从未声明的陈旧豁免、服务侧的对称路径、两个页面映射中未映射的已渲染表面、扫描看不到的已渲染表面(第三方向),以及扫描触达嵌套的仅含 Events 的 merge、多块文件的每个块、双引号头部与 `.tsx` 源文件。在真实源码树上删除一条现役豁免会让 `gen-cordis-catalog` 以事件名与声明文件大声失败;恢复后生成器回到字节相同的 no-op 再生成(85 个 artifact,0 写入),这同时证明新豁免恰好覆盖当下表面。doc-sync 中的 `verify-cordis-catalog` 每次运行都会执行该分区检查。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **渲染 client face 而非豁免。** 以 `faces: ['host', 'client']` 分析并给 client 服务/事件生成区块才是对盲区的根治,但它改变子系统目录的定位(host 层参考),并要求为纯浏览器表面做页面归属决策;既有的 `TODO(cordis-catalog-interface-services)` 已跟踪拓宽投影。兜底是保证,渲染是其上的升级。
|
||||
- **scope 级事件豁免。** 映射更小,但 `commands/changed`(client)与已渲染的 host 事件共享 `commands` scope,豁免整个 scope 会无声吞掉未来的 host face 事件——正是本决定要消除的失败模式。
|
||||
- **用 Typert 推导完备性而非原始 AST 扫描。** 投影与兜底必须独立失败:Typert 的可达性 bug 恰是兜底要捕获的对象,因此扫描刻意保持为不共享机制的朴素 `ts.createSourceFile` 遍历。
|
||||
- **对渲染签名的传递类型闭包设门。** 决定前先测量:渲染签名中可达的每个类型名都已分类,更深的字段套字段类型由页面手工维护的 `type-equiv` 粘贴与包 README 拥有;闭包门会在没有读者需求的情况下强迫内部类型认领页面。
|
||||
|
||||
## Consequences
|
||||
|
||||
新的 cordis 事件——host 或 client、任意文件深度——必须渲染到某个子系统页面,或在 `EVENT_WALK_EXEMPTIONS` 中以其文档所有者具名;删除事件时必须一并退役其豁免。声明在 `src/` 下任意位置的 Context key 现在同样如此。人工维护映射增加了 25 条 client face 条目,理由全部指向包 README,使子系统目录保持 host 层参考的定位。`walkPartitionProblems` 是分区判定的唯一居所;未来的兜底维度(如渲染 client face、schema 表面)应扩展它及其 spec,而非把检查重新内联进 `computeOutputs`。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md
|
||||
2026-08-08-npm-backed-git-repository-plugin-preparation.md: 958b932f82f4da3cf63aa911260411855e514409
|
||||
2026-08-08-npm-backed-git-repository-plugin-preparation.zh.md: 4986c55787f291ba9e0d4594854c15eb3c892c72
|
||||
@@ -0,0 +1,48 @@
|
||||
# Agent Note: npm-backed preparation makes GitHub repository Plugins self-contained
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-08-npm-backed-git-repository-plugin-preparation.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The repository Plugin authoring contract requires `scripts.prepack` to invoke `dsh-plugin-prepare`. Supplying that executable from the running DSH installation made a source package appear valid even when its own manifest could not obtain the helper. It therefore did not prove the behavior users need after `@deepseek-ai/dsh-repository-plugin` is published: an ordinary Git-hosted npm package must be installable and preparable from only its declared dependencies.
|
||||
|
||||
A selectable `.dsh-plugin` inside a pnpm workspace has a second isolation requirement. pnpm prepares a Git-hosted package by running the repository's preferred package manager before packing the selected subdirectory. A nested `pnpm install` can join the containing workspace; when the root lockfile does not list `.dsh-plugin` as an importer, pnpm can report success without installing dependencies declared only by that package. Its TypeScript build or prepare command then fails, or a pre-generated artifact hides the missing dependency.
|
||||
|
||||
The checked-in headless fixture mounts an already prepared wrapper. It proves runtime composition, not GitHub acquisition, npm resolution, or package-owned preparation.
|
||||
|
||||
## Decision
|
||||
|
||||
The `.dsh-plugin` package declares `@deepseek-ai/dsh-repository-plugin` as an ordinary development dependency and invokes its published `dsh-plugin-prepare` executable from `scripts.prepack`. The package may declare any other build and runtime dependencies and run arbitrary compilation before the helper. The repository Plugin package marks its Cordis and DSH peers optional so a helper-only development install resolves only the helper's actual `zod` runtime dependency; an application composition still supplies the peers used by the package's Cordis entry.
|
||||
|
||||
DSH does not materialize or prepend a prepare executable. `RepositoryCache` supplies only a transaction-owned `pnpm` wrapper: the outer install runs the pinned pnpm entry directly, while pnpm's hard-coded Git-package `pnpm install` reinvokes the same entry with `--ignore-workspace`. The selected package therefore owns dependency resolution even beneath another pnpm lockfile, and normal package-manager lifecycle `PATH` construction exposes `node_modules/.bin/dsh-plugin-prepare`. The temporary pnpm wrapper disappears after the child settles. The repository remains trusted package-manager input: all dependency and lifecycle code executes under the existing trust contract.
|
||||
|
||||
The Node 24 consumer lane passes an exact source derived from the pull request head repository and SHA. It uses the existing private DeepSeek Harness repository rather than creating another repository per run. A job-scoped Git configuration gives the read-only job token access to that exact private source and rewrites pnpm's SSH fallback to authenticated HTTPS.
|
||||
|
||||
The built-entry acceptance also creates an in-process npm registry. It stages the current built `@deepseek-ai/dsh-repository-plugin` as a publication artifact by removing `private`, replacing workspace protocols with the release version, and packing the declared files. The registry serves the resulting packument and tarball, while a job-local npm config directs only the `@deepseek-ai` scope to it. The real built `dsh run` child then fetches the exact Git source; that package resolves the helper through npm, type-checks and bundles a TypeScript Cordis entry and MCP server, prepares the adjacent skill, and loads all three contributions. A deliberately failing host `PATH` command proves the lifecycle selected the dependency-local executable. The acceptance also requires registry resolution and inspects the immutable prepared cache, so restoring a host-injected helper cannot satisfy it.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Inject `dsh-plugin-prepare` from the running DSH installation.** Rejected because it lets an incomplete repository manifest pass and tests a host-only path that npm consumers cannot reproduce.
|
||||
|
||||
**Publish the source fixture itself to npm.** Rejected because the product contract is specifically that the DSH Plugin remains Git-hosted; only the reusable preparation helper is an npm dependency.
|
||||
|
||||
**Create a new private GitHub repository in every CI run.** Rejected because the pull request repository at its exact head SHA is already a real authenticated private Git remote. Per-run repository mutation would add credentials, cleanup, and eventual-consistency failure modes without changing the acquisition path.
|
||||
|
||||
**Prepare after `RepositoryCache` installs the selected package.** Rejected because pnpm's packed subdirectory no longer contains sibling source assets referenced by paths such as `../skills`; preparation must happen before packlist.
|
||||
|
||||
**Clone GitHub repositories in DSH and bypass pnpm's Git fetcher.** Rejected because it would duplicate ref resolution, subdirectory selection, dependency installation, packlist behavior, and cache integrity already owned by the pinned package manager.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A repository author can commit a `.dsh-plugin` package, TypeScript source, skills, and MCP definitions to GitHub without publishing that Plugin package to npm. The package must declare the published preparation dependency.
|
||||
- Private GitHub sources use the host's standard Git authentication. CI proves that path with a temporary read-only configuration rather than persistent runner credentials.
|
||||
- `prepack`, not `prepare`, is part of the authoring format. It may contain arbitrary package-owned build steps but must invoke the dependency-provided helper; missing dependency or lifecycle metadata fails before a cache generation is usable.
|
||||
- A selected package in a pnpm repository installs from its own manifest rather than an enclosing workspace. It cannot rely on workspace-only hoisting; ordinary registry and relative `file:` dependencies remain package-owned inputs.
|
||||
- Exact source strings identify immutable cache generations; a changed ref or source configuration selects another generation.
|
||||
- Package dependencies, compilation, preparation, and the trusted `dsh.entry` contribution remain owned by the repository package and the [trusted-code decision](../architecture/2026-08-08-trusted-repository-package-code.md).
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/boot/app-boot/tests/repository-cache.spec.ts` runs a package excluded from its source repository's root pnpm lockfile through a local Git subpath and requires relative `file:` dependencies to provide both its build command and `dsh-plugin-prepare`; it also proves that visible environment survives while credential-shaped variables are scrubbed. `packages/self-modification/repository-plugin/tests/repository-plugin.spec.ts` pins helper-bearing `prepack` metadata and preparation output. `examples/headless-agent/tests/keyless-smoke.e2e.ts` keeps the checked-in prepared fixture on that source contract. `apps/cli/tests/github-repository-plugin.built.e2e.ts` is the product acceptance: simulated published helper package, job-local npm registry, fresh DSH home, exact authenticated private GitHub source, actual built `dsh run`, package-owned TypeScript build, real MCP execution, code-entry transformation, mock LLM request observation, and prepared cache inspection.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Agent Note: 基于 NPM 的准备机制使 GitHub repository 插件自包含
|
||||
|
||||
状态:已实现
|
||||
|
||||
[English](2026-08-08-npm-backed-git-repository-plugin-preparation.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
repository 插件创作契约要求 `scripts.prepack` 调用 `dsh-plugin-prepare`。如果由正在运行的 DSH 安装提供该可执行文件,即使源包自身的 manifest(元数据清单)无法取得辅助程序,它也会显得有效。因此,这并未证明 `@deepseek-ai/dsh-repository-plugin` 发布后用户所需的行为:普通 Git 托管 NPM 包必须只依靠自身声明的依赖即可安装和准备。
|
||||
|
||||
pnpm workspace 内可选择的 `.dsh-plugin` 还有另一项隔离要求。pnpm 会在打包所选子目录前运行仓库首选的包管理器,以准备 Git 托管包。嵌套的 `pnpm install` 可能加入外层 workspace;当根 lockfile 未把 `.dsh-plugin` 列为 importer 时,pnpm 可能报告成功,却未安装仅由该包声明的依赖。随后,其 TypeScript 构建或准备命令会失败;也可能因为存在预生成产物,依赖缺失被掩盖。
|
||||
|
||||
签入仓库的 headless fixture(测试前置数据)挂载的是已准备好的包装层。它证明运行时组合,而不证明 GitHub 获取、NPM 解析或包自有准备。
|
||||
|
||||
## 决策
|
||||
|
||||
`.dsh-plugin` 包将已发布的 `@deepseek-ai/dsh-repository-plugin` 声明为普通开发依赖,并在 `scripts.prepack` 中调用其已发布的 `dsh-plugin-prepare` 可执行文件。该包可以声明其他任意构建依赖与运行时依赖,并在辅助程序前执行任意编译。repository 插件包把 Cordis 与 DSH 对等依赖(peer dependency)标为可选,因此仅为使用辅助程序而进行的开发安装只会解析辅助程序实际依赖的 `zod` 运行时依赖;应用组合仍会提供该包 Cordis 入口所使用的对等依赖。
|
||||
|
||||
DSH 不会生成准备阶段可执行文件,也不会将其前置到 `PATH`。`RepositoryCache` 只提供一个由事务持有的 `pnpm` 包装脚本:外层安装直接运行锁定的 pnpm 入口,而 pnpm 为 Git 包硬编码的 `pnpm install` 会以 `--ignore-workspace` 重新调用同一入口。因此,即使位于另一个 pnpm lockfile 之下,所选包仍自行负责依赖解析,正常的包管理器生命周期 `PATH` 构造会暴露 `node_modules/.bin/dsh-plugin-prepare`。临时 pnpm 包装脚本会在子进程结算后消失。repository 仍是受信任的包管理器输入:所有依赖与生命周期代码都按既有信任契约执行。
|
||||
|
||||
Node 24 消费方 CI 任务会传入从 PR(Pull Request)head 仓库与 SHA 派生的精确源。它复用现有私有 DeepSeek Harness 仓库,而不会为每次运行新建仓库。作业作用域的 Git 配置允许只读作业 token 访问该精确私有源,并把 pnpm 的 SSH 回退改写为已认证 HTTPS。
|
||||
|
||||
构建入口验收还会创建一个进程内 NPM 注册表。它通过移除 `private`、将 workspace protocol 替换为发布版本并打包声明的文件,把当前已构建的 `@deepseek-ai/dsh-repository-plugin` 暂存为发布产物。注册表会提供由此生成的 `packument` 与 tarball,作业本地 NPM 配置则只把 `@deepseek-ai` scope 指向它。实际构建的 `dsh run` 子进程随后获取精确 Git 源;该包通过 NPM 解析辅助程序,对 TypeScript Cordis 入口和 MCP server 进行类型检查与打包,准备相邻的 skill(技能),并加载全部三类贡献。一个刻意设为失败的宿主 `PATH` 命令可以证明,该生命周期选中的是依赖内的可执行文件。验收还要求经过注册表解析并检查不可变的已准备缓存,因此恢复宿主注入的辅助程序也无法通过。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**从正在运行的 DSH 安装注入 `dsh-plugin-prepare`。** 拒绝,因为这会让 manifest 不完整的 repository 包通过,并测试 NPM 消费方无法复现的纯宿主路径。
|
||||
|
||||
**把源 fixture 本身发布到 NPM。** 拒绝,因为产品契约明确要求 DSH 插件仍托管在 Git;只有可复用的准备辅助程序是 NPM 依赖。
|
||||
|
||||
**在每次 CI 运行中创建新的私有 GitHub 仓库。** 拒绝,因为 PR 仓库的精确 head SHA 已是经过认证的真实私有 Git remote。每次运行的仓库变更会增加凭据、清理和最终一致性失败模式,却不改变获取路径。
|
||||
|
||||
**在 `RepositoryCache` 安装所选包后再准备。** 拒绝,因为 pnpm 打包后的子目录不再包含 `../skills` 等路径所引用的同仓库相邻资源;准备必须在生成 packlist 前完成。
|
||||
|
||||
**在 DSH 中克隆 GitHub 仓库并绕过 pnpm 的 Git 获取器。** 拒绝,因为这会重复实现已由锁定包管理器负责的 ref 解析、子目录选择、依赖安装、packlist 行为和缓存完整性。
|
||||
|
||||
## 后果
|
||||
|
||||
- 仓库作者可以把 `.dsh-plugin` 包、TypeScript 源码、skill 与 MCP 定义提交到 GitHub,而无需把该插件包发布到 NPM。该包必须声明已发布的准备依赖。
|
||||
- 私有 GitHub 源使用宿主的标准 Git 认证。CI 使用临时的只读配置而非运行器上的持久凭据来验证该路径。
|
||||
- 创作格式使用 `prepack` 而不是 `prepare`。其中可以包含任意包自有构建步骤,但必须调用依赖提供的辅助程序;依赖或生命周期元数据缺失时,会在缓存 generation 可用前失败。
|
||||
- pnpm 仓库中的所选包按自身 manifest 安装,而不继承外层 workspace。它不能依赖仅由 workspace 提升而可见的包;普通注册表依赖和相对 `file:` 依赖仍是包自有输入。
|
||||
- 精确源字符串标识不可变缓存 generation;改变 ref 或源配置会选择另一个 generation。
|
||||
- 包依赖、编译、准备和受信任的 `dsh.entry` 贡献仍由 repository 包和[受信任代码决策](../architecture/2026-08-08-trusted-repository-package-code.md)负责。
|
||||
|
||||
## 测试
|
||||
|
||||
`packages/boot/app-boot/tests/repository-cache.spec.ts` 会通过本地 Git 子路径运行一个未列入源仓库根 pnpm lockfile 的包,并要求相对 `file:` 依赖同时提供构建命令与 `dsh-plugin-prepare`;该测试还证明可见环境变量得以保留,而名称符合凭据模式的变量会被清除。`packages/self-modification/repository-plugin/tests/repository-plugin.spec.ts` 锁定包含辅助命令的 `prepack` 元数据与准备输出。`examples/headless-agent/tests/keyless-smoke.e2e.ts` 使签入仓库的已准备 fixture 继续符合该源格式契约。`apps/cli/tests/github-repository-plugin.built.e2e.ts` 是产品验收测试:模拟发布的辅助程序包、作业本地 NPM 注册表、全新 DSH 主目录、精确且经过认证的私有 GitHub 源、实际构建的 `dsh run`、包自有 TypeScript 构建、真实 MCP 执行、代码入口转换、mock LLM(大语言模型)请求观测,以及已准备缓存检查。
|
||||
@@ -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 .agents/notes/implemented/feature/2026-07-30-config-only-repository-plugins.md
|
||||
2026-07-30-config-only-repository-plugins.md: 2057125fc78596dd4e5eb153f77b828f83d9ceff
|
||||
2026-07-30-config-only-repository-plugins.zh.md: 6e741b46be716e21a11c2f508fb5e6d0505c76d0
|
||||
2026-07-30-config-only-repository-plugins.md: 35327a30e03c51311f634e05ade209ab93ae0155
|
||||
2026-07-30-config-only-repository-plugins.zh.md: e2ef728cadf33f12da0e96a71fd984b694f497b0
|
||||
|
||||
@@ -6,25 +6,25 @@ English | [中文](2026-07-30-config-only-repository-plugins.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A standalone `dsh` user has no developer-owned SDK project whose `package.json`, lockfile, and `cordis.yml` can carry an external Plugin dependency. Requiring an install command or another state file would make “use this repository” a multi-step workflow, while loading arbitrary repository code would bypass the restricted [static repository Plugin format](../architecture/2026-07-30-static-repository-plugin-format.md). Long-running TUI and Web processes also need a failed edit to preserve their usable Plugin generation and tell observers why the candidate was rejected.
|
||||
A standalone `dsh` user has no developer-owned SDK project whose `package.json`, lockfile, and `cordis.yml` can carry an external Plugin dependency. Requiring an install command or another state file would make “use this repository” a multi-step workflow, while trusted repository code still needs an exact-source, transactional lifecycle owned by the [repository package format](../architecture/2026-08-08-trusted-repository-package-code.md). Long-running TUI and Web processes also need a failed edit to preserve their usable Plugin generation and tell observers why the candidate was rejected.
|
||||
|
||||
## Decision
|
||||
|
||||
The shipped TUI and Web/headless `cordis.yml` trees contain an empty `repository-plugins` entry. A user changes only `$DSH_HOME/config.yaml`, replacing that entry's config with a `repositories` list. Each item uses `github:owner/repository#<ref>` plus an optional `&path:/.../.dsh-plugin`; omission selects `/.dsh-plugin`. An explicit ref is mandatory, paths are absolute within the repository and end in `.dsh-plugin`, and duplicate normalized specifiers reject before installation. There is no marketplace, discovery index, HTTPS URL vocabulary, or implicit latest generation.
|
||||
|
||||
`@deepseek-ai/dsh-repository-plugin` validates and normalizes each source, then resolves it through the generic vendored [`RepositoryCache`](../architecture/2026-07-30-package-manager-native-repository-cache.md). The default cache is `$DSH_HOME/cache/repository-plugins`; `cacheDir` is the explicit deployment override. Bundled pnpm selects the configured repository subpackage, runs its ordinary lifecycle including `prepare`, and atomically publishes the exact specifier. The DSH host imports only the generated `dsh-plugin.mjs` wrapper and mounts it as a child fiber, so skills and MCP retain the owners, failure contracts, and teardown defined by the format package.
|
||||
`@deepseek-ai/dsh-repository-plugin` validates and normalizes each source, then resolves it through the generic vendored [`RepositoryCache`](../architecture/2026-07-30-package-manager-native-repository-cache.md). The default cache is `$DSH_HOME/cache/repository-plugins`; `cacheDir` is the explicit deployment override. Bundled pnpm selects the configured repository subpackage, installs its dependencies, runs its package-authored `prepack`, and atomically publishes the exact specifier. The selected package's direct development dependency on `@deepseek-ai/dsh-repository-plugin` supplies `dsh-plugin-prepare` through package-local `node_modules/.bin`; the lifecycle invokes it after any package-owned build. The DSH host imports the generated `dsh-plugin.mjs` wrapper and mounts it as a child fiber; that wrapper composes static skill and MCP owners plus an explicit trusted Cordis entry when declared.
|
||||
|
||||
## Live update and failure
|
||||
|
||||
`dsh-app-boot` mounts the root Include through one helper that retains its exact Loader `Entry`. The TUI and Web register `$DSH_HOME/config.yaml` through Cordis HMR; headless reads the same file at startup without retaining a watcher. A watcher update rebuilds the Include patch list as immutable app-owned patches followed by the newly parsed personal patches, so Web-generated port, session-root, trust, and frontend values survive every personal edit unless a later personal patch deliberately replaces that row.
|
||||
|
||||
Cordis serializes and coalesces exact-path changes. Include and Loader reconcile a candidate transactionally: success commits the new source list, while fetch, preparation, wrapper import, format, or child-Plugin failure rejects the candidate and retains or restores the last good tree. HMR normalizes the caught value to `Error`, logs it, and broadcasts the parallel `hmr/config-update-failed(filename, error)` event; observer failures cannot break refresh processing. MCP transport connection failure remains the existing MCP client's contained successful-Plugin/no-tools result and therefore is not reclassified as a config-update failure.
|
||||
Cordis serializes and coalesces exact-path changes. Include and Loader reconcile a candidate transactionally: success commits the new source list, while fetch, preparation, wrapper import, format, or child-Plugin failure rejects the candidate and retains or restores the last good tree. HMR normalizes the caught value to `Error`, logs it, and broadcasts the parallel `hmr/config-update-failed(filename, error)` event; observer failures cannot break refresh processing. Repository MCP servers use strict startup, so an initial connection, discovery, or tool-registration failure rejects the candidate and becomes a config-update failure; non-strict standalone MCP clients retain their contained successful-Plugin/no-tools behavior.
|
||||
|
||||
An identical specifier permanently reuses its cache generation. HMR watches configuration, not cached repository code; the user changes the ref, path, or source list to select another generation.
|
||||
|
||||
## Trust boundary
|
||||
|
||||
Configuring a repository authorizes package-manager lifecycle code from that repository and its dependencies to run with the user's filesystem authority. The pnpm child removes ambient environment variables whose names contain `KEY`, `PASSWORD`, `SECRET`, or `TOKEN`, but this is credential-exposure reduction rather than a sandbox. The fixed runtime wrapper prevents repository-authored Cordis entry points from becoming part of the supported Plugin format; it does not make package preparation untrusted-safe.
|
||||
Configuring a repository authorizes package-manager lifecycle code, dependencies, the explicit `dsh.entry`, and spawned MCP servers from that repository to run with the user's filesystem authority. The pnpm child removes ambient environment variables whose names contain `KEY`, `PASSWORD`, `SECRET`, or `TOKEN`, but this is credential-exposure reduction rather than a sandbox. The prepared wrapper validates composition boundaries and lifecycle state; it does not make repository code safe to run when the source is untrusted.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -43,7 +43,7 @@ Configuring a repository authorizes package-manager lifecycle code from that rep
|
||||
- A repository that adds `.dsh-plugin/package.json` can reach standalone users through one personal-config edit without changing its existing skills or `.mcp.json` layout.
|
||||
- Long-running apps can add, replace, or remove configured generations without restart; rejected candidates retain the last good runtime and produce one generic Cordis event.
|
||||
- First use may require Git/network access and preparation time. Later starts reuse the exact prepared cache; old generations consume disk until a separate cache-management policy exists.
|
||||
- Only skills and common MCP definitions are supported. Hooks, commands, agents, apps, arbitrary Cordis code, compatibility shims, OAuth-bearing MCP definitions, and marketplaces remain intentionally absent.
|
||||
- Skills and common MCP definitions retain portable static adapters, while an explicit `dsh.entry` can contribute DSH-native Cordis behavior. Format-specific compatibility shims, OAuth-bearing MCP definitions, and marketplaces remain intentionally absent.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -6,25 +6,25 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
独立 `dsh` 用户没有开发者自有的 SDK 项目,无法由其 `package.json`、lockfile 和 `cordis.yml` 承载外部插件依赖。若要求运行安装命令或维护另一份状态文件,「使用这个仓库」就会变成多步骤流程;若加载任意仓库代码,又会绕过受限的[静态仓库插件格式](../architecture/2026-07-30-static-repository-plugin-format.md)。长时间运行的 TUI 和 Web 进程还必须在编辑失败时保留仍可使用的插件版本,并向观察者说明候选配置被拒绝的原因。
|
||||
独立 `dsh` 用户没有开发者自有的 SDK 项目,无法由其 `package.json`、lockfile 和 `cordis.yml` 承载外部插件依赖。若要求运行安装命令或维护另一份状态文件,「使用这个仓库」就会变成多步骤流程;受信任的 repository 代码仍需要由[repository 包格式](../architecture/2026-08-08-trusted-repository-package-code.md)负责一套锁定精确来源且具事务性的生命周期。长时间运行的 TUI 和 Web 进程还必须在编辑失败时保留仍可使用的插件版本,并向观察者说明候选配置被拒绝的原因。
|
||||
|
||||
## 决策
|
||||
|
||||
已交付的 TUI 和 Web/无头 `cordis.yml` 配置树包含一个空的 `repository-plugins` 配置项。用户只需修改 `$DSH_HOME/config.yaml`,用 `repositories` 列表替换该配置项的配置。每一项采用 `github:owner/repository#<ref>`,并可追加 `&path:/.../.dsh-plugin`;省略时选择 `/.dsh-plugin`。必须显式指定 ref;路径是仓库内的绝对路径,并以 `.dsh-plugin` 结尾;重复的规范化说明符在安装前即被拒绝。不提供插件市场、发现索引、HTTPS URL 词汇或隐式的最新版本。
|
||||
|
||||
`@deepseek-ai/dsh-repository-plugin` 校验并规范化每个源,再通过 vendor 中的通用 [`RepositoryCache`](../architecture/2026-07-30-package-manager-native-repository-cache.md) 解析。默认缓存位于 `$DSH_HOME/cache/repository-plugins`;`cacheDir` 是显式的部署覆盖项。随应用提供的 pnpm 选择配置的仓库子包(package),运行包括 `prepare` 在内的普通生命周期,并原子发布该精确说明符。DSH 宿主只导入生成的 `dsh-plugin.mjs` 包装模块并将其挂载为子 fiber,因此 skill(技能)与 MCP 仍沿用格式包定义的所有者、失败契约和清理行为。
|
||||
`@deepseek-ai/dsh-repository-plugin` 校验并规范化每个源,再通过 vendor 中的通用 [`RepositoryCache`](../architecture/2026-07-30-package-manager-native-repository-cache.md) 解析。默认缓存位于 `$DSH_HOME/cache/repository-plugins`;`cacheDir` 是显式的部署覆盖项。随应用提供的 pnpm 选择已配置的 repository 子包,安装其依赖,运行包所定义的 `prepack`,并原子发布该精确说明符。所选包对 `@deepseek-ai/dsh-repository-plugin` 的直接开发依赖通过包内 `node_modules/.bin` 提供 `dsh-plugin-prepare`;该生命周期会在任何包自有构建完成后调用它。DSH 宿主会导入生成的 `dsh-plugin.mjs` 包装层并将其挂载为子 fiber;该包装层组合静态 skill(技能)与 MCP 所有者,并在声明时组合显式的受信任 Cordis 入口。
|
||||
|
||||
## 实时更新与失败
|
||||
|
||||
`dsh-app-boot` 通过一个辅助函数挂载根 Include,并保留其确切的 Loader `Entry`。TUI 和 Web 通过 Cordis HMR(热模块替换)注册 `$DSH_HOME/config.yaml`;无头界面在启动时读取同一文件,但不保留监视器。监视器更新会重新构建 Include 补丁列表,先放置不可变的应用自有补丁,再放置新解析的个人补丁。因此,Web 生成的端口、会话根目录、信任和前端值会在每次个人编辑后保留,除非后续个人补丁有意替换相应配置项。
|
||||
|
||||
Cordis 会串行处理并合并该确切路径上的变更。Include 与 Loader 以事务方式协调候选配置:成功时提交新源列表;拉取、准备、包装模块导入、格式或子插件失败时拒绝候选配置,并保留或恢复最后一个可用树。HMR 会把捕获的值规范化为 `Error`,记录错误,并广播并行的 `hmr/config-update-failed(filename, error)` 事件;观察者失败不会中断刷新处理。MCP 传输连接失败仍沿用现有 MCP 客户端所收束的「插件成功加载但无工具」结果,因此不会被重新分类为配置更新失败。
|
||||
Cordis 会串行处理并合并该确切路径上的变更。Include 与 Loader 以事务方式协调候选配置:成功时提交新源列表;拉取、准备、包装模块导入、格式或子插件失败时拒绝候选配置,并保留或恢复最后一个可用树。HMR 会把捕获的值规范化为 `Error`,记录错误,并广播并行的 `hmr/config-update-failed(filename, error)` 事件;观察者失败不会中断刷新处理。Repository MCP 服务器采用严格启动,因此初始连接、发现或工具注册失败会拒绝候选配置,并构成配置更新失败;非严格的独立 MCP 客户端仍保留其所收束的「插件成功加载但无工具」行为。
|
||||
|
||||
相同说明符会永久复用同一个缓存版本。HMR 监视配置,而非已缓存的仓库代码;用户必须改变 ref、路径或源列表,才能选择另一个版本。
|
||||
|
||||
## 信任边界
|
||||
|
||||
配置仓库即授权该仓库及其依赖中的包管理器生命周期代码以用户的文件系统权限运行。pnpm 子进程会移除名称中含有 `KEY`、`PASSWORD`、`SECRET` 或 `TOKEN` 的环境变量,但这只会减少凭据暴露,并非沙箱。固定的运行时包装模块会阻止仓库作者提供的 Cordis 入口成为受支持插件格式的一部分;它无法让包准备过程安全执行不受信任的代码。
|
||||
配置仓库即授权该仓库中的包管理器生命周期代码、依赖、显式 `dsh.entry` 和 spawn 的 MCP server 以用户的文件系统权限运行。pnpm 子进程会移除名称中含有 `KEY`、`PASSWORD`、`SECRET` 或 `TOKEN` 的环境变量,但这只会减少凭据暴露,并非沙箱。已准备的包装层会校验组合边界和生命周期状态;当来源不受信任时,它无法让 repository 代码变得可安全运行。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -43,7 +43,7 @@ Cordis 会串行处理并合并该确切路径上的变更。Include 与 Loader
|
||||
- 添加 `.dsh-plugin/package.json` 的仓库只需一次个人配置编辑即可供独立用户使用,无需改变现有 skill 或 `.mcp.json` 布局。
|
||||
- 长时间运行的应用无需重启即可新增、替换或移除已配置版本;被拒绝的候选配置会保留最后一个可用运行时,并产生一个通用 Cordis 事件。
|
||||
- 首次使用可能需要 Git/网络访问和准备时间。后续启动会复用这份精确的已准备缓存;在另行制定缓存管理政策之前,旧版本会持续占用磁盘空间。
|
||||
- 仅支持 skill 和通用 MCP 定义。钩子、命令、agent(智能体)、应用、任意 Cordis 代码、兼容 shim、带 OAuth 的 MCP 定义和插件市场均有意不提供。
|
||||
- skill 和通用 MCP 定义保留可移植静态适配器,而显式 `dsh.entry` 可以贡献 DSH 原生 Cordis 行为。格式专用的兼容 shim、带 OAuth 的 MCP 定义和插件市场仍有意不提供。
|
||||
|
||||
## 测试
|
||||
|
||||
|
||||
@@ -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 .agents/notes/implemented/process/2026-06-18-markdown-cross-link-lint.md
|
||||
2026-06-18-markdown-cross-link-lint.md: b8b1337e9d758da6a4cc0bb46a6b37906357f877
|
||||
2026-06-18-markdown-cross-link-lint.zh.md: 9b627ebb17a0567424ca0caaeac8edd9b36917f2
|
||||
2026-06-18-markdown-cross-link-lint.md: 21c6884d3fd891794a11125a9aa51ac2bcb29059
|
||||
2026-06-18-markdown-cross-link-lint.zh.md: 444cf6eb97d95049d4e6b7bb5f44138051f6577a
|
||||
|
||||
@@ -20,11 +20,11 @@ A fourth `doc-sync` gate, `verify-md-links` (`scripts/verify-md-links.ts`), mirr
|
||||
|
||||
Scope matches the other gates plus the AGENTS.md pair and the repo-authored agent-skill Markdown under `.agents/skills/` (those skill files cross-link into the docs tree, so this reorg rewrote links in them too): `README.md`, `docs/**/*.md`, `packages/*/README.md`, `AGENTS.md`, `packages/AGENTS.md`, `.agents/skills/**/*.md`, deduped by real path (the `CLAUDE.md` symlinks resolve onto the AGENTS.md files). It is wired into `doc-sync`, so relevant documentation changes and CI exercise the same broken-link check.
|
||||
|
||||
This gate checks *existence*, not anchor validity: a link to a real file with a `#wrong-heading` fragment still passes (the file resolves; the fragment is stripped).
|
||||
The gate now also checks `#fragment` anchors on Markdown targets — same-file anchors included — against heading slugs and explicit `<a id>`; the [fragment-anchor decision](2026-08-09-md-fragment-anchor-gate.md) owns that mechanism and the slug rules.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Anchor-level validity checking** — heavier and lower-value; file-level dead links are the failure that actually bit. The scope cut is deliberate: authors verify `#fragment` anchors themselves when linking to one.
|
||||
**Anchor-level validity checking** — deferred here as heavier and lower-value (file-level dead links were the failure that had actually bit), leaving authors to verify `#fragment` anchors themselves. That manual rule did not hold; the [fragment-anchor decision](2026-08-09-md-fragment-anchor-gate.md) later added the check.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ Status: implemented
|
||||
|
||||
检查范围与其他门禁一致,并额外包含 AGENTS.md 文件对以及 `.agents/skills/` 下仓库自有的 agent skill(技能)Markdown(这些 skill 文件会交叉链接到 docs 目录树,因此本次重组也改写了其中的链接):`README.md`、`docs/**/*.md`、`packages/*/README.md`、`AGENTS.md`、`packages/AGENTS.md`、`.agents/skills/**/*.md`。系统按真实路径去重(`CLAUDE.md` symlink 会解析到 AGENTS.md 文件)。该检查接入 `doc-sync`,因此相关文档变更与 CI 执行同一套断链检查。
|
||||
|
||||
本门禁检查的是*文件存在性*,而非锚点有效性:指向一个真实文件但带有 `#wrong-heading` 片段的链接仍会通过(文件路径可解析;片段被剥除)。
|
||||
本门禁现在也检查 Markdown 目标上的 `#fragment` 锚点——包括同文件锚点——对照标题 slug 与显式 `<a id>`;该机制与 slug 规则归 [fragment 锚点决定](2026-08-09-md-fragment-anchor-gate.md)所有。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**锚点级有效性检查**:更重且价值更低;实际造成问题的是文件级死链。这一范围裁剪是有意为之:作者在链接到某个锚点时自行验证 `#fragment`。
|
||||
**锚点级有效性检查**:当时以更重且价值更低为由推迟(实际咬过人的是文件级死链),把 `#fragment` 验证留给作者人工完成。该人工规则没有守住;[fragment 锚点决定](2026-08-09-md-fragment-anchor-gate.md)后来补上了这项检查。
|
||||
|
||||
## 后果
|
||||
|
||||
|
||||
@@ -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 .agents/notes/implemented/process/2026-07-28-per-subsystem-cordis-surface-regions.md
|
||||
2026-07-28-per-subsystem-cordis-surface-regions.md: ef65bfc4c7dadd7dafe1a38f41656e6ecc61ea50
|
||||
2026-07-28-per-subsystem-cordis-surface-regions.zh.md: 1df18d7260800b89c95a9d4aeb0301adb0ee56f6
|
||||
2026-07-28-per-subsystem-cordis-surface-regions.md: f6d4494d4195ba27f2898eecb27af32b433af88b
|
||||
2026-07-28-per-subsystem-cordis-surface-regions.zh.md: ee2a3c9730e6259e9f53d1b722b04b99819af65b
|
||||
|
||||
@@ -14,7 +14,7 @@ The [generated-catalog decision](../../archived/process/2026-06-20-generated-cor
|
||||
|
||||
`gen-cordis-catalog.ts` injects each subsystem's service and event reference INTO its own page, between `<!-- BEGIN GENERATED cordis-surface … -->` / `<!-- END GENERATED cordis-surface -->` markers, and the flat services/events catalogs are deleted. One page per subsystem now carries introduction, data structures, and the generated wiring surface.
|
||||
|
||||
- **Curated fail-loud partition.** `SERVICE_PAGE` maps every discovered `ctx.<key>` to exactly one page; `EVENT_SCOPE_PAGE` maps every event scope. The generator hard-errors in both directions — an unmapped discovered service/scope, and a mapped key/scope the walk no longer discovers — so the partition cannot drift from the source surface. An independent scan of EVERY `declare module 'cordis'` Context merge backstops the rendering walk's blind spot (it only sees a root `index.ts` with a same-named service class): a declared key the walk cannot render must carry a named `SERVICE_WALK_EXEMPTIONS` reason (today: the `ctx.agent` DX accessor, plus the interface-typed or non-index-declared lsp/apiProxy/appShell/tuiPrompt/tuiResumeHost), and stale exemptions hard-error; a `TODO(cordis-catalog-interface-services)` marks teaching the walk to render them.
|
||||
- **Curated fail-loud partition.** `SERVICE_PAGE` maps every discovered `ctx.<key>` to exactly one page; `EVENT_SCOPE_PAGE` maps every event scope. The generator hard-errors in both directions — an unmapped discovered service/scope, and a mapped key/scope the walk no longer discovers — so the partition cannot drift from the source surface. Independent AST scans of every `declare module 'cordis'` merge block under `packages/*/*/src/**` backstop the projection's blind spots for services AND events: a declared Context key or Events member the projection cannot render must carry a named `SERVICE_WALK_EXEMPTIONS`/`EVENT_WALK_EXEMPTIONS` reason, stale exemptions hard-error, and everything rendered must also be visible to the scan ([events-backstop decision](../architecture/2026-08-09-cordis-event-walk-backstop.md) owns the scan contract); a `TODO(cordis-catalog-interface-services)` marks teaching the projection to render the interface-typed entries.
|
||||
- **Byte-identical regions across the pair.** The generator writes the SAME English region bytes into `foo.md` and `foo.zh.md`, extending the existing rule that verbatim code fences match across a pair. `verify-translation-pairing` gained a dedicated region-identity check (`partitionGeneratedRegions` in `translation-pairing.ts` owns the marker grammar) that names a divergent or malformed region precisely; the whole-document structural signature still covers the region content a second time.
|
||||
- **Guarded pair auto-record.** A regeneration that changes region bytes would leave every touched pair out-of-sync, so the generator re-records a pair's `.i18n.yaml` itself — but ONLY when the write is region-confined: both sides' recorded blob hashes must match the pre-write bytes, and the region-STRIPPED content must be unchanged on both sides. Human-prose drift leaves the record stale so the pairing gate still forces the normal translation flow; a brand-new pair is never auto-recorded (the author's reviewed `--write` owns that). This keeps `.i18n.yaml` as plain `git hash-object` values — no stripped-hash semantics change.
|
||||
- **The inherited tier moved, not died.** The vendor `ctx` members and `internal/*`/loader/hmr/timer events render to `docs/cordis-api/inherited.md`, next to the relocated Cordis core API pages (`docs/cordis-catalog/core/` → `docs/cordis-api/`). Framework surface lives under a framework home; the harness pages stay repository-owned vocabulary.
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
`gen-cordis-catalog.ts` 把每个子系统的服务与事件参考注入到该子系统自己的页面内部,置于 `<!-- BEGIN GENERATED cordis-surface … -->` / `<!-- END GENERATED cordis-surface -->` 标记之间;平铺的 services/events 目录随之删除。现在每个子系统由一个页面同时承载介绍、数据结构和生成的接线表面。
|
||||
|
||||
- **人工维护、响亮失败的划分。** `SERVICE_PAGE` 把发现的每个 `ctx.<key>` 映射到恰好一个页面;`EVENT_SCOPE_PAGE` 映射每个事件作用域。生成器在两个方向上都会直接报错(既有被发现却未映射的服务或作用域,也有已映射但遍历不再发现的键或作用域),因此划分不可能相对源码表面发生漂移。一个独立扫描会读取每一处 `declare module 'cordis'` 的 Context 合并,为渲染遍历的盲区(它只看得到根 `index.ts` 中同名服务类)兜底:遍历渲染不了的已声明键必须在 `SERVICE_WALK_EXEMPTIONS` 中带着点名理由(目前是 `ctx.agent` 这一 DX 访问器字段,加上接口类型或不在 index 声明的 lsp/apiProxy/appShell/tuiPrompt/tuiResumeHost),陈旧豁免同样直接报错;教会遍历渲染它们的后续工作由 `TODO(cordis-catalog-interface-services)` 标记。
|
||||
- **人工维护、响亮失败的划分。** `SERVICE_PAGE` 把发现的每个 `ctx.<key>` 映射到恰好一个页面;`EVENT_SCOPE_PAGE` 映射每个事件作用域。生成器在两个方向上都会直接报错(既有被发现却未映射的服务或作用域,也有已映射但遍历不再发现的键或作用域),因此划分不可能相对源码表面发生漂移。独立的 AST 扫描读取 `packages/*/*/src/**` 下每一个 `declare module 'cordis'` merge 块,为投影在服务与事件两侧的盲区兜底:投影渲染不了的已声明 Context key 或 Events 成员必须在 `SERVICE_WALK_EXEMPTIONS`/`EVENT_WALK_EXEMPTIONS` 中带着点名理由,陈旧豁免直接报错,且投影渲染的一切也必须对扫描可见(扫描契约归[事件兜底决定](../architecture/2026-08-09-cordis-event-walk-backstop.md)所有);教会投影渲染接口类型条目的后续工作由 `TODO(cordis-catalog-interface-services)` 标记。
|
||||
- **区块在配对两侧按字节一致。** 生成器把同一份英文区块字节写入 `foo.md` 和 `foo.zh.md`,是对「围栏代码块在配对两侧逐字节一致」这一既有规则的延伸。`verify-translation-pairing` 新增了专门的区块一致性检查(标记语法归 `translation-pairing.ts` 中的 `partitionGeneratedRegions` 所有),能精确点名出现分歧或格式错误的区块;整篇文档的结构签名仍会把区块内容再覆盖一遍。
|
||||
- **带防护的配对自动记录。** 一次改变区块字节的重新生成会让每个被触及的配对失去同步,因此生成器会自行重新记录配对的 `.i18n.yaml`,但仅限本次写入完全限定在区块内的情况:两侧记录的 blob hash 必须与写入前的字节相符,且两侧剥离区块后的内容必须没有变化。人工行文若有漂移,记录就保持陈旧,配对门禁因此仍会强制走正常翻译流程;全新的配对绝不自动记录(那归作者经评审的 `--write` 所有)。这样 `.i18n.yaml` 保持为纯粹的 `git hash-object` 值:不引入任何「剥离后 hash」的语义变化。
|
||||
- **继承层搬了家,而非消亡。** vendor 的 `ctx` 成员与 `internal/*`/loader/hmr/timer 事件渲染到 `docs/cordis-api/inherited.md`,紧邻迁移后的 Cordis 核心 API 页面(`docs/cordis-catalog/core/` → `docs/cordis-api/`)。框架表面落在框架自己的归属之下;harness 页面仍是仓库自有的词汇。
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-09-md-fragment-anchor-gate.md
|
||||
2026-08-09-md-fragment-anchor-gate.md: e02a917bedd9649a2326e3fb1f53072ac05c88a8
|
||||
2026-08-09-md-fragment-anchor-gate.zh.md: dcc0644ae064c161ef5232654993f0e6c872064c
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note: verify-md-links validates fragment anchors, closing the last dead-link class
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-09-md-fragment-anchor-gate.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`verify-md-links` proved a relative link's target file exists but never looked at the `#fragment`, and the documentation standard compensated with a manual rule: grep anchors yourself before renaming a heading. A corpus sweep found 15 links whose fragments named no anchor in their target — three distinct decay modes: a heading reworded after the link was written (`#security-and-authority-are-explicit-non-goals` vs the note's current `Security and authority are non-goals`), a contract relocated to a different owning document (`tool-fs` linking the seam README for the no-timeout rule that now lives in the group README), and zh pair sides linking English slugs their Chinese headings never produce (`#deferred-work` against `## 推迟工作`). None of these fail any gate, and each silently strands the reader at the top of the target page.
|
||||
|
||||
## Decision
|
||||
|
||||
`verify-md-links` now resolves fragments too (superseding the deferred scope cut in the [cross-link decision](2026-06-18-markdown-cross-link-lint.md)). For every relative link whose target is a Markdown file — same-file `#anchor` links included, which the old checker skipped entirely — the fragment must name a real anchor in the target: a heading's GitHub slug or an explicit `<a id>` in real HTML flow (code samples and commented-out anchors register nothing). Slugs are computed from the RENDERED heading text via the repository's own `markdownHeadingLines`, so links, inline code, and emphasis inside a heading slug as GitHub renders them; underscores survive (`#showcase-web_fetch`); repeated slugs get GitHub's occupied-set `-1`, `-2`, … suffixes; matching is exact-case, since element ids are case-sensitive. Fragments onto non-Markdown targets (`file.ts#L10`) carry renderer-owned semantics and stay out of scope, as do external and root-absolute URLs. Anchor sets are collected lazily for any existing target (`anchorCache`), so links INTO archived notes and vendor documents are validated without making those files sources.
|
||||
|
||||
The slug function differs from `gen-cordis-catalog`'s region-anchor slugger (which drops underscores): the generator's headings are always reachable through its explicit `<a id>` anchors, so the two need not share one rule. Chinese pair sides follow the existing repository convention (`docs/glossary.zh.md`, `docs/cordis-primer.zh.md`): keep the English fragment in the link and place an explicit `<a id>` before the Chinese heading, so both language sides expose identical anchors.
|
||||
|
||||
The 15 broken fragments are fixed in the same change: stale slugs retargeted to the current headings, the relocated no-timeout contract now linked at its owning group README, and four zh documents given explicit anchors. `docs/AGENTS.md` and the `dsh-doc-standards` skill no longer prescribe the manual anchor grep for Markdown links; it survives only for anchors cited from TypeScript strings whose output never reaches gate-scanned Markdown (today's three all render into scanned pages, so the gate covers them through the committed output).
|
||||
|
||||
## Verification
|
||||
|
||||
`scripts/verify-md-links.spec.ts` proves the acceptance paths: rendered-text slugging (backticks, punctuation, a linked heading, kept underscores), occupied-set repeat suffixes, `<a id>` ignored inside fences/inline code/comments, a resolving mixed-link document, dead same-file and cross-file fragments, a case-variant fragment, and a missing target still reported as `target` rather than `anchor`. The gate runs over the full corpus in doc-sync (`verify-md-links`) and passes only after the 15 fixes — the corpus itself is the red-to-green evidence for each decay mode.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Keep the manual-grep rule.** It demonstrably did not hold: the 15 fragments decayed under a gate-driven maintenance culture, because heading rewrites happen in PRs that never look at inbound links. A mechanical invariant belongs in an executed gate.
|
||||
- **Point zh links at Chinese-slug anchors.** GitHub slugs CJK headings fine, but the corpus convention is already explicit `<a id>` + English fragments (glossary, primer), which also survives renderers that strip non-ASCII; adopting a second convention would split the corpus.
|
||||
- **Share `githubSlug` with the typert generator.** A one-function import would couple a doc gate to a package build, and the two rules genuinely differ (the generator strips underscores; its anchors are explicit `<a id>`s the gate reads directly), so divergence is by design, not drift.
|
||||
- **Validate VitePress slugs as well.** The published site's dead-link check already runs in `website:build`; generated regions carry explicit anchors precisely so the two renderers agree, and hand headings that diverge would fail there.
|
||||
|
||||
## Consequences
|
||||
|
||||
Renaming a heading now breaks the build wherever a Markdown link cites its anchor, instead of stranding readers; authors fix the inbound links in the same change, exactly as they already must for file renames. Same-file anchors are no longer a blind spot, so zh pages must anchor any English fragment they use. The manual pre-rename grep survives only for anchors cited from TypeScript strings whose output never reaches gate-scanned Markdown.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note(agent 决策记录):verify-md-links 校验 fragment 锚点,关闭最后一类死链
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-09-md-fragment-anchor-gate.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`verify-md-links` 只证明相对链接的目标文件存在,从不检查 `#fragment`,文档标准以一条人工规则补偿:重命名标题前自己 grep 锚点。一次语料扫描发现 15 条链接的 fragment 在目标中没有对应锚点——三种衰变模式:链接写下后标题被改写(`#security-and-authority-are-explicit-non-goals` 对 note 现在的 `Security and authority are non-goals`)、契约搬迁到另一份属主文档(`tool-fs` 链到 seam README,而无超时规则现居 group README)、zh 侧链接其中文标题永远不会生成的英文 slug(`#deferred-work` 对 `## 推迟工作`)。这些都不触发任何 gate,且每条都把读者悄悄丢在目标页顶部。
|
||||
|
||||
## Decision
|
||||
|
||||
`verify-md-links` 现在也解析 fragment(取代[跨链接决定](2026-06-18-markdown-cross-link-lint.md)中被推迟的范围裁剪)。对每条目标为 Markdown 文件的相对链接——包括旧检查器完全跳过的同文件 `#anchor` 链接——fragment 必须命名目标中的真实锚点:标题的 GitHub slug,或真实 HTML 流中的显式 `<a id>`(代码示例与注释掉的锚点不注册任何东西)。slug 由仓库自有的 `markdownHeadingLines` 从**渲染后**的标题文本计算,因此标题内的链接、行内代码与强调都按 GitHub 的渲染结果 slug;下划线保留(`#showcase-web_fetch`);重复 slug 获得 GitHub 的占用集 `-1`、`-2`……后缀;匹配区分大小写,因为元素 id 本就区分大小写。指向非 Markdown 目标的 fragment(`file.ts#L10`)语义归渲染器所有,不在范围内;外部与根绝对 URL 同样不检查。锚点集合对任意存在的目标惰性收集(`anchorCache`),因此链入归档 note 与 vendor 文档的链接照常校验,而这些文件不会因此成为扫描源。
|
||||
|
||||
slug 函数与 `gen-cordis-catalog` 的区块锚点 slugger 不同(后者丢弃下划线):生成器的标题总能通过其显式 `<a id>` 锚点到达,两者无需共享一条规则。中文侧沿用既有语料惯例(`docs/glossary.zh.md`、`docs/cordis-primer.zh.md`):链接保留英文 fragment,在中文标题前放置显式 `<a id>`,使两个语言侧暴露相同的锚点。
|
||||
|
||||
15 条坏 fragment 在同一变更中修复:陈旧 slug 重定向到当前标题,搬迁的无超时契约改链其属主 group README,四份中文文档补上显式锚点。`docs/AGENTS.md` 与 `dsh-doc-standards` skill 不再为 Markdown 链接开人工 grep 锚点的处方;人工 grep 只对输出从不进入受检 Markdown 的 TypeScript 字符串锚点保留(当下三处全部渲染进受检页面,gate 经由提交的产物覆盖它们)。
|
||||
|
||||
## Verification
|
||||
|
||||
`scripts/verify-md-links.spec.ts` 证明各验收路径:渲染文本 slug 化(反引号、标点、含链接标题、保留下划线)、占用集重复后缀、围栏/行内代码/注释中的 `<a id>` 不注册、全部可解析的混合链接文档、死的同文件与跨文件 fragment、大小写变体 fragment、以及缺失目标仍报 `target` 而非 `anchor`。gate 在 doc-sync 中跑完整语料(`verify-md-links`),且只有在 15 条修复之后才通过——语料本身就是每种衰变模式由红转绿的证据。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **保留人工 grep 规则。** 它被证明守不住:15 条 fragment 在 gate 驱动的维护文化下仍然衰变,因为改写标题的 PR 从不会去看入链。可机械检查的不变式应进入被执行的 gate。
|
||||
- **让中文链接指向中文 slug 锚点。** GitHub 对 CJK 标题的 slug 没问题,但语料惯例已是显式 `<a id>` + 英文 fragment(glossary、primer),且它在剥离非 ASCII 的渲染器下也存活;引入第二种惯例会割裂语料。
|
||||
- **与 typert 生成器共享 `githubSlug`。** 为一个函数引入包构建耦合不值得,且两条规则确实不同(生成器剥离下划线;其锚点是 gate 直接读取的显式 `<a id>`),分歧是设计使然而非漂移。
|
||||
- **同时校验 VitePress slug。** 发布站点的死链检查已在 `website:build` 中运行;生成区块正是为两种渲染器一致而携带显式锚点,手写标题若有分歧会在那里失败。
|
||||
|
||||
## Consequences
|
||||
|
||||
重命名标题现在会在任何 Markdown 链接引用其锚点处使构建失败,而非把读者丢在页顶;作者须在同一变更中修复入链,与文件重命名的既有义务完全一致。同文件锚点不再是盲区,中文页面使用英文 fragment 时必须补锚点。人工的重命名前 grep 只对输出从不进入受检 Markdown 的 TypeScript 字符串锚点保留。
|
||||
Reference in New Issue
Block a user