diff --git a/.agents/notes/implemented/architecture/2026-10-23-plugin-contract-publishing.md b/.agents/notes/implemented/architecture/2026-10-23-plugin-contract-publishing.md index cda130c132..b7022359b2 100644 --- a/.agents/notes/implemented/architecture/2026-10-23-plugin-contract-publishing.md +++ b/.agents/notes/implemented/architecture/2026-10-23-plugin-contract-publishing.md @@ -60,12 +60,12 @@ Store 的 `validate-index.mjs` 仍然实现索引规则。它的 CI 原来比较 ### Schema 地址固定为 docs 仓库的 raw 地址 -两份 schema 的 `$id` 是它在 docs 仓库里的地址: -`https://raw.githubusercontent.com/Lithe-IDEA/lithe-plugin-docs/main/contract/v1/<文件名>`。 +契约目录里每份 schema(包括子目录里的)的 `$id` 是它在 docs 仓库里的地址: +`https://raw.githubusercontent.com/Lithe-IDEA/lithe-plugin-docs/main/contract/v1/<目录内路径>`。 两边都对这个地址做严格检查: -- 主仓库的 `scripts/architecture/verify-shared-contracts.sh` 用常量 `published_plugin_schema_base` 要求 `$id` 完全相等。 +- 主仓库的 `scripts/architecture/verify-shared-contracts.sh` 用常量 `published_plugin_schema_base` 要求目录下每个 `*.schema.json` 的 `$id` 完全相等。 - docs 仓库的 `contract.mjs` 要求每份 schema 的 `$id` 等于它自己的发布地址。 因此,如果主仓库改了地址而 docs 仓库没跟上,或者 docs 仓库里的文件不是从主仓库同步来的,CI 都会失败。 @@ -74,11 +74,15 @@ Store 的 `validate-index.mjs` 仍然实现索引规则。它的 CI 原来比较 副本只包含契约目录,`../` 开头的相对链接在 docs 仓库里会失效。所以契约 README 指向 `rust-core-api.md`、Agent Notes 的链接都写成 `https://github.com/1lck/Lithe-IDEA/blob/preview/...`,指向 SDK 的链接写成 lithe-plugin-docs 的地址。 +这条规则适用于契约目录里的每个 Markdown 文件,不只是顶层 README。相对链接只能指向目录里的文件;指向目录(例如 `fixtures/`)也不行,因为 raw 地址取不到目录。要提到目录,写成代码文本即可。 + 两边都检查这条规则: -- 主仓库的 `verify-shared-contracts.sh` 拒绝离开目录或指向不存在文件的相对链接。 +- 主仓库的 `verify-shared-contracts.sh` 检查目录下每个 `.md`,拒绝离开目录、指向目录或指向不存在文件的相对链接。 - docs 仓库的 `contract.mjs` 在同步和校验时也拒绝这类链接。 +主仓库的检查必须和 docs 的同步规则一样严格。2026-11-01 同步 #1581 时,`proposals/toolchains/README.md` 里指向 `fixtures/` 的链接被 docs 拒绝:主仓库当时只检查顶层 README,而且允许链接指向目录,所以这个问题在合并前没被发现,只能再提一个 PR 修复后重新同步。 + ### `plugin.json` 里不能写 `$schema` Core 解析清单时拒绝未知字段(serde `deny_unknown_fields`),schema 本身也是 `additionalProperties: false`。如果开发者按习惯加一行 `"$schema"`,插件会因为 `plugin.manifest.invalid` 装不上。所以指南要求在编辑器里按文件名关联 schema,例如 VS Code 的 `json.schemas`。是否在以后的契约版本里允许 `$schema` 另行决定:那需要提高能力版本,老版本 Lithe 会拒绝新格式。 diff --git a/scripts/architecture/verify-shared-contracts.sh b/scripts/architecture/verify-shared-contracts.sh index a1908b46a2..e8416f22c7 100755 --- a/scripts/architecture/verify-shared-contracts.sh +++ b/scripts/architecture/verify-shared-contracts.sh @@ -168,11 +168,19 @@ done abort "native UI schema ID mismatch" unless native_schema.fetch("$id") == "#{published_base}/native-ui.schema.json" abort "declarative manifests must use schemaVersion 2" unless schema.fetch("properties").fetch("schemaVersion").fetch("const") == 2 readme = File.read(File.join(root, "README.md"), encoding: "UTF-8") - # The mirror only holds this directory, so a relative link must resolve here. - readme.scan(/\]\(([^)\s]+)\)/).flatten.each do |target| - next if target.start_with?("#") || target.match?(%r{\A[a-z][a-z0-9+.-]*:}i) - path = File.expand_path(target.split("#").first, root) - abort "contract README link #{target} must be absolute or stay inside #{root}" unless path.start_with?("#{File.expand_path(root)}/") && File.exist?(path) + # The mirror only holds this directory and its sync rejects the same things: a relative + # link in any Markdown file must name a file here, and every schema names its mirror URL. + base = "#{File.expand_path(root)}/" + Dir[File.join(root, "**", "*.md")].sort.each do |document| + File.read(document, encoding: "UTF-8").scan(/\]\(([^)\s]+)\)/).flatten.each do |target| + next if target.start_with?("#") || target.match?(%r{\A[a-z][a-z0-9+.-]*:}i) + path = File.expand_path(target.split("#").first, File.dirname(document)) + abort "#{document}: link #{target} must be absolute or name a file inside #{root}" unless path.start_with?(base) && File.file?(path) + end + end + Dir[File.join(root, "**", "*.schema.json")].sort.each do |path| + expected = "#{published_base}/#{File.expand_path(path).delete_prefix(base)}" + abort "#{path}: $id must be #{expected}" unless JSON.parse(File.read(path))["$id"] == expected end documented = readme.scan(/`(plugin\.[a-zA-Z.]+)`/).flatten manifests = Dir[File.join(root, "fixtures", "*.plugin.json")] diff --git a/shared/contracts/plugins/v1/proposals/toolchains/README.md b/shared/contracts/plugins/v1/proposals/toolchains/README.md index 27b5fccdb0..73b601954d 100644 --- a/shared/contracts/plugins/v1/proposals/toolchains/README.md +++ b/shared/contracts/plugins/v1/proposals/toolchains/README.md @@ -46,7 +46,7 @@ not authorize replacing unrelated host commands. | [query.schema.json](query.schema.json) | Query request/reply shapes | | [resolution.schema.json](resolution.schema.json) | Resolver outcomes and structured reasons | | [local.schema.json](local.schema.json) | Proposed extension of the machine-local document with directory pins | -| [fixtures/](fixtures/) | Illustrative contributions, queries, resolution outcomes, pins, and rejections | +| `fixtures/` | Illustrative contributions, queries, resolution outcomes, pins, and rejections | Schemas describe JSON shape. Signature verification, duplicate-definition registration, enabled-plugin ownership, path containment, executable evidence,