diff --git a/README.md b/README.md index f80c2b3..457d6c0 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,7 @@ SDK 的源码都在本仓库的 `sdk/` 下,发布在[本仓库的 Releases](ht | [`plugin-manifest.schema.json`](contract/v1/plugin-manifest.schema.json) | `plugin.json` 的 JSON Schema | | [`plugin-index.schema.json`](contract/v1/plugin-index.schema.json) | `lithe-plugin-index.json` 的 JSON Schema | | [`native-ui.schema.json`](contract/v1/native-ui.schema.json) | `ui` 3 宿主渲染的原生内容的 JSON Schema | -| [`toolchains/`](contract/v1/toolchains/README.md) | 插件提供的工具链:工具定义、版本管理器提供方、查询与回复校验、固定值(pin)和四层解析。Core 已实现;macOS 设置页以只读方式显示结果,macOS 构建和提供者运行另有独立的解析适配器,已有的 Java 和语言服务器仍用原来的方式;Windows 不支持 | -| [`toolchains/proposal/`](contract/v1/toolchains/proposal/README.md) | 工具链的完整设计草案,包括还没实现的部分;对应示例在 `fixtures/toolchains/proposal/`(含 Java 工具定义和 mise 提供方),只用于评审,不能直接安装 | +| [`toolchains/`](contract/v1/toolchains/README.md) | 插件提供的工具链:工具定义、版本管理器提供方、提供方后端协商(`litheToolchains`)、查询与回复校验(含稳定失败码)、固定值(pin)和四层解析。Core 已实现;macOS 设置页以只读方式显示结果,macOS 构建和提供者运行另有独立的解析适配器,已有的 Java 和语言服务器仍用原来的方式;Windows 不支持 | | [`fixtures/`](contract/v1/fixtures) | 标准用例,按功能分目录(`manifest`、`languages`、`backend`、`build-systems`、`build-providers`、`marketplace` 等),和 [reference 功能页](reference/README.md#功能页)一一对应:合法和非法的清单、命令和构建规划、插件函数协议、市场索引,以及 Lithe 对每个用例的预期结果 | fixture 放在 `fixtures/<功能>/` 下,文件按演示的能力命名,例如 `build-systems/build-command-stdout.plugin.json` 演示“解码命令的标准输出”。它们借 Cargo、Go、Gradle、Maven 等真实工具作为输入,但不是这些插件本身;要参考完整插件,看[示例插件](#示例插件)。每个目录的文件和用例数量见对应功能页的“示例与用例”。 @@ -367,8 +366,7 @@ The plugin contract defines the meaning, validation rules, and error codes of ev | [`plugin-manifest.schema.json`](contract/v1/plugin-manifest.schema.json) | JSON Schema for `plugin.json` | | [`plugin-index.schema.json`](contract/v1/plugin-index.schema.json) | JSON Schema for `lithe-plugin-index.json` | | [`native-ui.schema.json`](contract/v1/native-ui.schema.json) | JSON Schema for host-rendered native content (`ui` 3) | -| [`toolchains/`](contract/v1/toolchains/README.md) | Plugin-owned toolchains: tool definitions, version manager providers, query and reply validation, pins, and four-layer resolution. Core implements it; the macOS settings pages show results read-only, macOS Build and Provider Run have a separate resolution adapter, and existing Java and language-server consumers keep their previous behavior; Windows does not support it | -| [`toolchains/proposal/`](contract/v1/toolchains/proposal/README.md) | The complete toolchain design draft, including unimplemented parts; its examples live in `fixtures/toolchains/proposal/` (including a Java tool definition and a mise provider), are for review only, and cannot be installed | +| [`toolchains/`](contract/v1/toolchains/README.md) | Plugin-owned toolchains: tool definitions, version manager providers, provider backend negotiation (`litheToolchains`), query and reply validation (with stable failure codes), pins, and four-layer resolution. Core implements it; the macOS settings pages show results read-only, macOS Build and Provider Run have a separate resolution adapter, and existing Java and language-server consumers keep their previous behavior; Windows does not support it | | [`fixtures/`](contract/v1/fixtures) | Conformance cases in one directory per feature (`manifest`, `languages`, `backend`, `build-systems`, `build-providers`, `marketplace`, and so on), matching the [reference feature pages](reference/README.md#feature-pages): valid and invalid manifests, command and build plans, plugin function protocol, marketplace indexes, and Lithe's expected result for each case | Fixtures live under `fixtures//` and are named for the capability they demonstrate; for example `build-systems/build-command-stdout.plugin.json` demonstrates decoding a command's standard output. They use real tools such as Cargo, Go, Gradle, and Maven as inputs but are not those plugins; for complete plugins, see the [example plugins](#example-plugins). Each feature page's "Examples and cases" section lists its files and case counts. diff --git a/contract.lock.json b/contract.lock.json index e410e91..0ceab3a 100644 --- a/contract.lock.json +++ b/contract.lock.json @@ -3,12 +3,12 @@ "source": { "repository": "https://github.com/1lck/Lithe-IDEA", "directory": "shared/contracts/plugins/v1", - "revision": "644afdaac9087b276cb8f7902658b85ae1792d6d" + "revision": "5d38352a1757a70fd5fa297d7aa3e43bb2e33607" }, "files": [ { "path": "README.md", - "sha256": "8bae09cb1277181d6598c9835a42f6fb9c2db27d2e4d0e60dd5e45e80d487e5d" + "sha256": "e5e938c1aa106dc1853e45b0dc20b493f645bbe16373336015cac1b627a336f1" }, { "path": "fixtures/backend/backend-workspace.plugin.json", @@ -216,7 +216,7 @@ }, { "path": "fixtures/manifest/invalid-manifests.json", - "sha256": "4f1b2c72d9b9156686f347ae083ba145d10319dcabfefe4c9eef6854858bb02a" + "sha256": "ea8d728d9e37a6ea6c3e3c0073692c87d1e55a2648d4bb8e74d5b7f583e5f979" }, { "path": "fixtures/marketplace/index-cases.json", @@ -246,13 +246,17 @@ "path": "fixtures/package-trust/signature-vectors.json", "sha256": "94dd446142eee59dd1d8422b004912161c13e9e281c8a754d8bb1f632fb8e6fa" }, + { + "path": "fixtures/project-generators/project-generator-tool-field.plugin.json", + "sha256": "f01f48c1b35dedb783f776672a54edd1d500c55f3ad4768dad06edf4c99cac9a" + }, { "path": "fixtures/project-generators/project-generator.plugin.json", "sha256": "87477231929291d6e6be351cf993f99971b3ddb3b81cce3f668df6e3a07f18a3" }, { "path": "fixtures/project-generators/project-plans.json", - "sha256": "ca8a469a844678af11f3ad5781a057e4ec987cea2bd42e7e7cba2fa90c2a13f2" + "sha256": "54e9bfb50b8e3ac926aae29aa027fac6470b19e8b35f735a5da6bfc3a6dde7fb" }, { "path": "fixtures/project-generators/templates/.gitattributes", @@ -271,32 +275,12 @@ "sha256": "5435c9be433e0a1cadbeccc701451b21d53d8519e1516c58960af944fe0bf616" }, { - "path": "fixtures/settings/settings-v2.plugin.json", - "sha256": "83d4c7faf7bfbb9c474639bfc0ef42dae8cd7d370d388f8527589f2afed12981" - }, - { - "path": "fixtures/toolchains/proposal/java-tool-definition.json", - "sha256": "23408937de3c5d77712657e8753d23d7cd07d91e73d5c3d0788d3ae283f56134" - }, - { - "path": "fixtures/toolchains/proposal/local.json", - "sha256": "0d24ed31d1b330cd45147274747206dada3dc660deae66eb72acdf359e2092ba" + "path": "fixtures/project-generators/templates/tool-field/README.md", + "sha256": "57d17750e2c3d0fed6158d0995b08cf782b745774e5a7e6bee3b7d3dbdb6a777" }, { - "path": "fixtures/toolchains/proposal/manifest-rejections.json", - "sha256": "de2ebb472c31eb8e5ff3fd9217572311dc65df1bb62a17736e4192d105576674" - }, - { - "path": "fixtures/toolchains/proposal/mise-provider.json", - "sha256": "55f9b992e8d4988afb3e8574025af2e593c71c799a98bd9b237b9d01c5fc474c" - }, - { - "path": "fixtures/toolchains/proposal/query-cases.json", - "sha256": "d1a7c3e8b8df18cba3d40cca437b712a787b8935d6c8eb7b567e74736d42c203" - }, - { - "path": "fixtures/toolchains/proposal/resolution-cases.json", - "sha256": "8292f41dc287e0d96638a3583324354f106cc0bd149c91ab76f80525893c5cab" + "path": "fixtures/settings/settings-v2.plugin.json", + "sha256": "83d4c7faf7bfbb9c474639bfc0ef42dae8cd7d370d388f8527589f2afed12981" }, { "path": "fixtures/toolchains/tool-definition.plugin.json", @@ -336,39 +320,11 @@ }, { "path": "plugin-manifest.schema.json", - "sha256": "3320d28b7c77d628164b25125a9c08f6fba86a4b4e261a37f052dd0d9502f819" + "sha256": "46739a6203dc7e250e28eb295e8693b57a668cda46be95fda5bd4c120da6eac3" }, { "path": "toolchains/README.md", - "sha256": "65b98b081650aac5b1208ab823c56ec5d335a2300c76867e6c814fca08b5b50e" - }, - { - "path": "toolchains/proposal/README.md", - "sha256": "b7b324bc688a218c94f7cbceb425a8976f024ac2ac18d049aac747887b99496b" - }, - { - "path": "toolchains/proposal/local.schema.json", - "sha256": "e875e0f90b2481f3b548c0f72be3fd6bd88ba6993f42011cb4a0f7441373d436" - }, - { - "path": "toolchains/proposal/manifest-contributions.schema.json", - "sha256": "6189f332f2bfab7d3e6ed527c6a0aed046ab71b137d9c8455584a7ac7eeb4b78" - }, - { - "path": "toolchains/proposal/manifest.md", - "sha256": "77e4e8a8e1b4d67c548edc9242afaa0f071466c431ea7e290c25993df218defb" - }, - { - "path": "toolchains/proposal/protocol.md", - "sha256": "5def30935a4e34da4f857f262a356d16df9311df504d9f535b6392bb853e90a9" - }, - { - "path": "toolchains/proposal/query.schema.json", - "sha256": "053aebb0a0a05f1dc8c9e8cf82c35f6e34dbf20b3fd5b20706c6ecab6741f58c" - }, - { - "path": "toolchains/proposal/resolution.schema.json", - "sha256": "8c2084680ff062b1a415cacd6ca72e4da4b5ed85055dce6a3cf403adc489692b" + "sha256": "3c5535140a3040753b6eab5e4e7032362820e397ac0a670a71e60e7b6a83ad82" } ] } diff --git a/contract/v1/README.md b/contract/v1/README.md index 54f53f4..412388f 100644 --- a/contract/v1/README.md +++ b/contract/v1/README.md @@ -15,7 +15,6 @@ This directory is the cross-platform source of truth: | `plugin-index.schema.json` | JSON shape of a marketplace source index | | `fixtures/` | Valid example manifests and golden cases, one folder per contract area (below). Manifests are named after the capability they exercise rather than a published plugin. Case files name their base manifest by file name, so file names are unique across the folders | | `toolchains/README.md` | Implemented Core foundation: plugin definitions/provider declarations, verified-origin registry, reply validation, directory pins, snapshot resolution, version matching and discovery plans; macOS uses it for its read-only settings pages and for Build and run tools, Windows is not connected | -| `toolchains/proposal/` | Broader design draft, including unimplemented query transport, native lifecycle/persistence, consumer migration, settings UI and manager plugins; examples are review projections, not installable manifests | | Fixture folder | Contents | | --- | --- | @@ -30,8 +29,8 @@ This directory is the cross-platform source of truth: | `fixtures/debug-adapters/` | Debug adapters (standalone, served by a language server, remote attach) and `debug-adapter-rejections.json` | | `fixtures/vcs/` | A version control provider | | `fixtures/keymap-import/` | Two independent shortcut import providers sharing one packaged backend | -| `fixtures/project-generators/` | Project generators: template files (`templates/`), a scaffolding command, and an archive download; `project-plans.json` holds golden `project.*` form evaluations, generation plans, and name checks | -| `fixtures/toolchains/` | Production-shape fixtures for tool definitions and a packaged toolchain provider; they require `toolDefinitions` and `toolchainProviders`, which only macOS advertises. `proposal/` holds the examples and cases of the toolchains draft | +| `fixtures/project-generators/` | Project generators: template files (`templates/`), a scaffolding command, and an archive download; `project-generator-tool-field.plugin.json` with `templates/tool-field/` picks tool installations with `tool` fields (`projectGenerators` 2); `project-plans.json` holds golden `project.*` form evaluations, generation plans, and name checks, and a case's optional `manifest` names the fixture it runs against (the version 1 generator by default) | +| `fixtures/toolchains/` | Production-shape fixtures for tool definitions and a packaged toolchain provider; they require `toolDefinitions` and `toolchainProviders`, which only macOS advertises | | `fixtures/package-trust/` | `publisher-bundled.plugin.json` and `signature-vectors.json`: public cross-platform vectors for the package publisher signature: the RFC 8032 test key, a fixed package with non-ASCII file names, its canonical payload, RFC 8032 deterministic signature, and content digest. Both platforms must build the same canonical payload and digest and accept the recorded signature; signers that add randomness, such as CryptoKit, check the signature by verification instead of byte comparison | | `fixtures/marketplace/` | Marketplace index, availability, source URL, and install review golden cases | @@ -52,8 +51,8 @@ fields are rejected; editors associate the schema by file name instead. ## Plugin-owned toolchains (Core foundation) -The [toolchains draft](toolchains/proposal/README.md) records the replacement -design discussed in [PR #1581](https://github.com/1lck/Lithe-IDEA/pull/1581#issuecomment-6541366112). +The design follows the rewrite plan in +[PR #1581](https://github.com/1lck/Lithe-IDEA/pull/1581#issuecomment-6541366112). All tool definitions belong to enabled plugins; the host supplies only generic query, validation, resolution, persistence, and presentation mechanisms. It does not add a built-in tool-definition table or interpret version-manager @@ -75,8 +74,8 @@ whose `toolchain` executable names a plugin-defined tool rather than a host toolchain; settings detections never grant launch authority. Windows does not advertise the capabilities. Saving pins, `toolchainConsumers`, generic runtime launches and live backend activation remain unsupported, and Java and language-server consumers keep their previous adapters. Version-manager -implementations such as mise belong to their own plugin packages. Proposal -envelopes remain separate from production manifests. See [the implemented slice](toolchains/README.md). +implementations such as mise belong to their own plugin packages. See +[the implemented slice](toolchains/README.md). ## Shortcut import providers (Core foundation) @@ -673,7 +672,7 @@ hosts. Level 3 versions each section on its own. A level 3 manifest lists in | `buildModelInputs` | Optional public model sharing between enabled workspace plugins | `1` | | `vcsProviders` | `vcsProviders[]` | `1` | | `debugAdapters` | [`debugAdapters[]`](#api-level-4-debug-adapters); v2 adds target arguments/environment | `1`, `2` | -| `projectGenerators` | [`projectGenerators[]`](#project-generators): entries of the New Project window | `1` | +| `projectGenerators` | [`projectGenerators[]`](#project-generators): entries of the New Project window; v2 adds the [`tool` field](#tool-fields) | `1`, `2` | | `backend` | [`backend`](#plugin-backend): the plugin's own long-lived process | `1`, `2` (adds [start without a page and workspace contexts](#backend-2-workspace-contexts)) | | `toolDefinitions` | [Tool identities, layouts, discovery and four-layer candidates](toolchains/README.md) | `1` (Core; macOS settings pages, Build and run tools) | | `toolchainProviders` | [Provider declarations and permission-bound answer validation](toolchains/README.md) | `1` (Core; macOS settings pages, Build and run tools) | @@ -2030,8 +2029,8 @@ process starts and what it learns about workspaces. ## Project generators -`projectGenerators` (requires `projectGenerators` 1 and the `project.generate` -permission) adds entries to the host's New Project window. The host owns the +`projectGenerators` (requires `projectGenerators` 1 or 2 and the +`project.generate` permission) adds entries to the host's New Project window. The host owns the window: the entry list and its search, the project name and location, the "create Git repository" option, the empty project, and the Create button. A generator declares only its own fields, the languages and build systems the @@ -2074,7 +2073,8 @@ generator ID. | `enum` | One option | `options` (1–16 `{value, title}`), `default` (the first option otherwise), `presentation` (`segmented` or `menu`), `maximumVersion` | | `boolean` | `true` or `false` | `default` | | `multiSelect` | A list of options, kept in declaration order | `options` (1–128, each with an optional `group` heading), `default` | -| `runtime` | A host-discovered installation | `runtime` (`java`), `minimumVersion`, `optional`. The host chooses the initial value. | +| `runtime` | A host-discovered installation (`projectGenerators` 1 only) | `runtime` (`java`), `minimumVersion`, `optional`. The host chooses the initial value. | +| `tool` | An installation of a [tool definition](toolchains/README.md), by home directory (`projectGenerators` 2 only) | `tool` (a tool definition ID), `minimumVersion`, `pinAs`, `optional`. See [Tool fields](#tool-fields). | `maximumVersion` is a template over earlier fields naming the highest version the value may be, compared numerically by dotted components. A target Java @@ -2093,21 +2093,61 @@ directory name: not empty, at most 128 characters, no `/`, `\`, `:`, or control characters, not `.` or `..`, no leading `-`, and no surrounding spaces. +### Tool fields + +A `tool` field (requires `projectGenerators` 2) lets the user pick one +installation of any tool that a plugin declares in +[`toolDefinitions`](toolchains/README.md), such as a Python interpreter, a Node.js +runtime, or a JDK. The generator names no host feature and no language: the +host lists installations through its generic tool discovery and the user's +tool settings, exactly as the tool's own settings page does. + +```json +{"id": "sdk", "type": "tool", "title": "Python", "tool": "python", + "minimumVersion": "3.11", "pinAs": "project"} +``` + +| Property | Meaning | +| --- | --- | +| `tool` | Required. A tool definition ID: the short ID of a verified official definition, otherwise `:`. There is no closed list; a tool nobody declares has no installations. | +| `minimumVersion` | Optional, one to three dotted numbers. An installation below it is not selectable. | +| `pinAs` | Optional. `project` saves the selection as the created project's own tool choice (the same per-directory pin the tool's settings page edits), so the project builds and runs with the installation picked here. Without it the selection only feeds the steps. | +| `optional` | Optional. Without a usable installation an optional field stays empty and leaves no pin; otherwise the form reports `required`. An installation below `minimumVersion` is not usable. | + +A tool field has no `default`; the host chooses the initial value. Core picks, +among the installations the host listed, the user's choice if it is still +listed and otherwise the newest version (the host's first among equal +versions). Versions compare as releases, so trailing zeros do not count: +`3.11`, `3.11.0`, and `3.11.0.0` are equal. An installation below +`minimumVersion` is marked in the field's `installations`, so the host can +show it disabled. When the selection is below `minimumVersion` the form +reports `toolTooOld`, except that an optional field the user has not set +stays empty rather than picking an installation that is too old. + +A generator with `projectGenerators` 2 must not declare a `runtime` field, and +a generator with `projectGenerators` 1 must not declare a `tool` field +(`plugin.manifest.invalid`). Version 1 generators keep working unchanged. +A host that lacks `projectGenerators` 2 refuses the whole manifest with +`plugin.api.unsupported` (`requires.projectGenerators`). + ### Conditions `when` and `visibleWhen` take one condition or a list that must all hold: `{"field": "", "equals" | "notEquals": }` or `{"field": "", "oneOf": [, …]}`. Values are option values for `enum`, `true`/`false` -for `boolean`, and strings for `text` and `runtime` (`notEquals: ""` tests -that an optional field is filled). A condition on a hidden field is false. +for `boolean`, and strings for `text`, `runtime`, and `tool` (`notEquals: ""` +tests that an optional field is filled; a tool field compares as its home +directory). A condition on a hidden field is false. ### Templates Strings in the manifest use `${…}`: `${project.name}`, `${field.}` (a -text or option value, `true`/`false`, a comma-separated list, or a runtime's -home directory), `${field..version}`, and `${field..major}` (`21` for -`21.0.4`, `8` for `1.8.0_412`). Modifiers follow `|` and apply in order: -`path` turns dots into `/`, `package` makes a valid package name +text or option value, `true`/`false`, a comma-separated list, or the home +directory of a runtime or tool), `${field..version}` and +`${field..major}` (runtime fields only; `21` for `21.0.4`, `8` for +`1.8.0_412`), and `${field..executable}` (tool fields only: the path of +the tool's executable in the chosen installation). Modifiers follow `|` and +apply in order: `path` turns dots into `/`, `package` makes a valid package name (`com.Example.my-app` → `com.example.myapp`), and `lower` lowercases. Template files use `{{ … }}` with the same variables, because POMs, Gradle @@ -2137,26 +2177,43 @@ file, and 8 MiB in total; archives to 20,000 entries and 512 MiB extracted. | Command | Payload | Result | | --- | --- | --- | -| `project.generatorCatalog` | `{plugins: [{manifest, enabled}], hostCapabilities?}` | `catalog: {generators: [{pluginID, pluginName, pluginEnabled, generator, requirements: [{languageID or buildSystemID, providers: [{pluginID, pluginName, enabled}]}], usesNetwork}], issues: [{pluginID, error}]}` | -| `project.generatorForm` | `{manifest, generatorID, form}` | `form: {projectName: {problem}, fields: [{id, visible, value, problem?}], activeRequirements, valid}` | -| `project.generationPlan` | `{manifest, generatorID, packagePath, form}` | `plan: {projectName, steps, openFiles, runtimes: [{field, runtime, home, version}], usesNetwork}` | +| `project.generatorCatalog` | `{plugins: [{manifest, enabled}], hostCapabilities?}` | `catalog: {generators: [{pluginID, pluginName, pluginEnabled, generator, requirements: [{languageID or buildSystemID, providers: [{pluginID, pluginName, enabled}]}], tools: [{fieldID, tool, providers: [{pluginID, pluginName, enabled}]}], usesNetwork}], issues: [{pluginID, error}]}` | +| `project.generatorForm` | `{manifest, generatorID, form}` | `form: {projectName: {problem}, fields: [{id, visible, value, problem?, installations?}], activeRequirements, valid}` | +| `project.generationPlan` | `{manifest, generatorID, packagePath, form}` | `plan: {projectName, steps, openFiles, runtimes: [{field, runtime, home, version}], toolPins: [{field, tool, pinAs, home, executable}], usesNetwork}` | | `project.checkName` | `{projectName}` | `name: {problem}` | | `project.extractArchive` | `{archivePath, destination}` | `extracted: {files}` | -`form` is `{projectName, values, edited, runtimes}`. `values` maps field IDs -to values; a text value counts only when its ID is in `edited`, so derived +`form` is `{projectName, values, edited, runtimes, tools}`. `values` maps field +IDs to values; a text value counts only when its ID is in `edited`, so derived defaults keep following the fields they read. `runtimes` maps runtime field -IDs to `{home, version}` of the user's selection. A field `problem` is one of -`required`, `tooLong`, `invalidFormat`, `unsupportedValue`, `runtimeTooOld`, -and `versionTooHigh`; hosts localize by code. Plan steps are `{kind: +IDs to `{home, version}` of the user's selection. `tools` maps tool field IDs +to the installations the host found for the field's tool, in the order the +host prefers them, as `[{home, executable, version}]`; the field's `values` +entry is the `home` of the user's choice. A tool field in the result carries +`installations: [{home, executable, version, meetsMinimum}]`, so the host +draws its picker from Core's answer, and its `value` is the installation Core +selected. A field `problem` is one of `required`, `tooLong`, `invalidFormat`, +`unsupportedValue`, `runtimeTooOld`, `toolTooOld`, and `versionTooHigh`; hosts +localize by code. Plan steps are `{kind: "writeFiles", files: [{path, contents, executable}]}`, `{kind: "command", command, arguments, environment, collect, mayUseNetwork, timeoutSeconds}`, and -`{kind: "archive", url, host, format: "zip"}`. +`{kind: "archive", url, host, format: "zip"}`. `toolPins` lists, for each +visible tool field with `pinAs` and an installation, the tool and the chosen +`home` and `executable` that the host saves as the project's pin. + +The catalog entry's `tools` lists, for each tool field, the plugins that +declare the field's tool and whether they are enabled, in the same way as +`requirements`: the host can offer to enable one, or open the marketplace, +when the field has no installation. Core only names declarations; whether a +definition is trusted, and whether the tool is installed, is decided by the +host's tool registry and discovery. After the steps, the host moves the staging directory to its final location -(refusing an existing one), may record a `java` runtime as the project's JDK, -creates the Git repository when asked, and opens the project. A failure or -cancellation removes the staging directory and leaves nothing behind +(refusing an existing one), may record a `java` runtime as the project's JDK +(version 1 runtime fields only), saves the `toolPins` as the project's tool +choices through the same store the tool settings page edits, creates the Git +repository when asked, and opens the project. A failure or cancellation +removes the staging directory and leaves nothing behind (`plugin.project.generationFailed`). ## Plugin functions diff --git a/contract/v1/fixtures/manifest/invalid-manifests.json b/contract/v1/fixtures/manifest/invalid-manifests.json index 5ca1ec5..65ce236 100644 --- a/contract/v1/fixtures/manifest/invalid-manifests.json +++ b/contract/v1/fixtures/manifest/invalid-manifests.json @@ -1054,6 +1054,15 @@ {"name": "languages 2 is used by a grammar or configuration", "base": "language-grammar.plugin.json", "edits": [{"op": "remove", "path": "/languages/0/grammar"}, {"op": "remove", "path": "/languages/0/configuration"}], "expectedCode": "plugin.manifest.invalid"}, {"name": "a grammar names a known engine", "base": "language-grammar.plugin.json", "edits": [{"op": "set", "path": "/languages/0/grammar/kind", "value": "textmate"}], "expectedCode": "plugin.manifest.invalid"}, {"name": "a grammar file stays in the package", "base": "language-grammar.plugin.json", "edits": [{"op": "set", "path": "/languages/0/grammar/path", "value": "../http.monarch.json"}], "expectedCode": "plugin.manifest.invalid"}, - {"name": "a comment delimiter is one short line", "base": "language-grammar.plugin.json", "edits": [{"op": "set", "path": "/languages/0/configuration/lineComment", "value": "#\n"}], "expectedCode": "plugin.manifest.invalid"} + {"name": "a comment delimiter is one short line", "base": "language-grammar.plugin.json", "edits": [{"op": "set", "path": "/languages/0/configuration/lineComment", "value": "#\n"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a tool field needs projectGenerators 2", "base": "project-generator.plugin.json", "edits": [{"op": "set", "path": "/projectGenerators/0/fields/1", "value": {"id": "sdk", "type": "tool", "title": "SDK", "tool": "python"}}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "projectGenerators 2 has no runtime field", "base": "project-generator.plugin.json", "edits": [{"op": "set", "path": "/requires/projectGenerators", "value": 2}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a tool field names a tool definition ID", "base": "project-generator-tool-field.plugin.json", "edits": [{"op": "set", "path": "/projectGenerators/0/fields/0/tool", "value": "Python Interpreter"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a tool field names a tool", "base": "project-generator-tool-field.plugin.json", "edits": [{"op": "remove", "path": "/projectGenerators/0/fields/0/tool"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a tool field minimum version is dotted numbers", "base": "project-generator-tool-field.plugin.json", "edits": [{"op": "set", "path": "/projectGenerators/0/fields/0/minimumVersion", "value": "latest"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a tool field is pinned to a known scope", "base": "project-generator-tool-field.plugin.json", "edits": [{"op": "set", "path": "/projectGenerators/0/fields/0/pinAs", "value": "workspace"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "the host chooses a tool field's initial installation", "base": "project-generator-tool-field.plugin.json", "edits": [{"op": "set", "path": "/projectGenerators/0/fields/0/default", "value": "/usr/bin"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "only a tool field has an executable", "base": "project-generator-tool-field.plugin.json", "edits": [{"op": "set", "path": "/projectGenerators/0/steps/1/arguments/2", "value": "${field.venv.executable}"}], "expectedCode": "plugin.manifest.invalid"}, + {"name": "a tool property needs a tool field", "base": "project-generator-tool-field.plugin.json", "edits": [{"op": "set", "path": "/projectGenerators/0/fields/1/tool", "value": "python"}], "expectedCode": "plugin.manifest.invalid"} ] } diff --git a/contract/v1/fixtures/project-generators/project-generator-tool-field.plugin.json b/contract/v1/fixtures/project-generators/project-generator-tool-field.plugin.json new file mode 100644 index 0000000..4994323 --- /dev/null +++ b/contract/v1/fixtures/project-generators/project-generator-tool-field.plugin.json @@ -0,0 +1,45 @@ +{ + "schemaVersion": 2, + "id": "dev.lithe.fixture.project-generator-tool-field", + "displayName": "Project Generator Tool Field Fixture", + "version": "1.0.0", + "pluginAPIVersion": 3, + "requires": {"projectGenerators": 2}, + "vendor": {"id": "org.example", "displayName": "Example", "signatureRequirement": "publisherPackage"}, + "permissions": ["project.generate"], + "activationEvents": [], + "projectGenerators": [ + { + "id": "python-app", + "displayName": "Python App", + "section": "project", + "fields": [ + {"id": "python", "type": "tool", "title": "Python", "tool": "python", + "minimumVersion": "3.11", "pinAs": "project"}, + {"id": "venv", "type": "boolean", "title": "Create a virtual environment", "default": true} + ], + "steps": [ + {"kind": "files", "source": "templates/tool-field"}, + {"kind": "command", "command": "uv", "mayUseNetwork": false, + "arguments": ["venv", "--python", "${field.python.executable}"], + "environment": {"PYTHON_HOME": "${field.python}"}, + "when": {"field": "venv", "equals": true}} + ] + }, + { + "id": "node-app", + "displayName": "Node App", + "section": "generator", + "fields": [ + {"id": "node", "type": "tool", "title": "Node.js", "tool": "node", + "minimumVersion": "18.0.0", "pinAs": "project", "optional": true} + ], + "steps": [ + {"kind": "command", "command": "npm", "mayUseNetwork": true, + "arguments": ["init", "--yes"], + "environment": {"NODE": "${field.node.executable}"}, + "when": {"field": "node", "notEquals": ""}} + ] + } + ] +} diff --git a/contract/v1/fixtures/project-generators/project-plans.json b/contract/v1/fixtures/project-generators/project-plans.json index f35b509..67324d9 100644 --- a/contract/v1/fixtures/project-generators/project-plans.json +++ b/contract/v1/fixtures/project-generators/project-plans.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "description": "Golden answers of the project.* commands for project-generator.plugin.json. generationPlan cases read templates with fixtures/project-generators as the package path.", + "description": "Golden answers of the project.* commands for project-generator.plugin.json, or for the fixture a case names in manifest (a file name in fixtures/project-generators). generationPlan cases read templates with fixtures/project-generators as the package path.", "cases": [ { "name": "form defaults", @@ -201,6 +201,7 @@ "version": "21.0.4" } ], + "toolPins": [], "usesNetwork": false } } @@ -277,6 +278,7 @@ "version": "21.0.4" } ], + "toolPins": [], "usesNetwork": true } } @@ -308,6 +310,7 @@ ], "openFiles": [], "runtimes": [], + "toolPins": [], "usesNetwork": true } } @@ -334,6 +337,369 @@ } } }, + { + "name": "tool form selects the newest installation", + "manifest": "project-generator-tool-field.plugin.json", + "operation": "generatorForm", + "generatorID": "python-app", + "form": { + "projectName": "demo", + "tools": { + "python": [ + {"home": "/opt/python-3.10", "executable": "/opt/python-3.10/bin/python3", "version": "3.10.14"}, + {"home": "/opt/python-3.12", "executable": "/opt/python-3.12/bin/python3", "version": "3.12.4"} + ] + } + }, + "expected": { + "status": "ok", + "form": { + "projectName": {"problem": null}, + "fields": [ + { + "id": "python", + "visible": true, + "value": "/opt/python-3.12", + "installations": [ + {"home": "/opt/python-3.10", "executable": "/opt/python-3.10/bin/python3", "version": "3.10.14", "meetsMinimum": false}, + {"home": "/opt/python-3.12", "executable": "/opt/python-3.12/bin/python3", "version": "3.12.4", "meetsMinimum": true} + ] + }, + {"id": "venv", "visible": true, "value": true} + ], + "activeRequirements": [], + "valid": true + } + } + }, + { + "name": "tool form keeps the chosen installation", + "manifest": "project-generator-tool-field.plugin.json", + "operation": "generatorForm", + "generatorID": "python-app", + "form": { + "projectName": "demo", + "values": {"python": "/opt/python-3.11"}, + "tools": { + "python": [ + {"home": "/opt/python-3.11", "executable": "/opt/python-3.11/bin/python3", "version": "3.11.9"}, + {"home": "/opt/python-3.13", "executable": "/opt/python-3.13/bin/python3", "version": "3.13.1"} + ] + } + }, + "expected": { + "status": "ok", + "form": { + "projectName": {"problem": null}, + "fields": [ + { + "id": "python", + "visible": true, + "value": "/opt/python-3.11", + "installations": [ + {"home": "/opt/python-3.11", "executable": "/opt/python-3.11/bin/python3", "version": "3.11.9", "meetsMinimum": true}, + {"home": "/opt/python-3.13", "executable": "/opt/python-3.13/bin/python3", "version": "3.13.1", "meetsMinimum": true} + ] + }, + {"id": "venv", "visible": true, "value": true} + ], + "activeRequirements": [], + "valid": true + } + } + }, + { + "name": "tool form reports an installation below the minimum", + "manifest": "project-generator-tool-field.plugin.json", + "operation": "generatorForm", + "generatorID": "python-app", + "form": { + "projectName": "demo", + "tools": { + "python": [ + {"home": "/opt/python-3.10", "executable": "/opt/python-3.10/bin/python3", "version": "3.10.14"} + ] + } + }, + "expected": { + "status": "ok", + "form": { + "projectName": {"problem": null}, + "fields": [ + { + "id": "python", + "visible": true, + "value": "/opt/python-3.10", + "problem": "toolTooOld", + "installations": [ + {"home": "/opt/python-3.10", "executable": "/opt/python-3.10/bin/python3", "version": "3.10.14", "meetsMinimum": false} + ] + }, + {"id": "venv", "visible": true, "value": true} + ], + "activeRequirements": [], + "valid": false + } + } + }, + { + "name": "tool form without an installation is required", + "manifest": "project-generator-tool-field.plugin.json", + "operation": "generatorForm", + "generatorID": "python-app", + "form": {"projectName": "demo"}, + "expected": { + "status": "ok", + "form": { + "projectName": {"problem": null}, + "fields": [ + {"id": "python", "visible": true, "value": "", "problem": "required"}, + {"id": "venv", "visible": true, "value": true} + ], + "activeRequirements": [], + "valid": false + } + } + }, + { + "name": "plan expands the tool and pins it", + "manifest": "project-generator-tool-field.plugin.json", + "operation": "generationPlan", + "generatorID": "python-app", + "form": { + "projectName": "demo", + "tools": { + "python": [ + {"home": "/opt/python-3.12", "executable": "/opt/python-3.12/bin/python3", "version": "3.12.4"} + ] + } + }, + "expected": { + "status": "ok", + "plan": { + "projectName": "demo", + "steps": [ + { + "kind": "writeFiles", + "files": [ + { + "path": "README.md", + "contents": "# demo\n\nInterpreter: /opt/python-3.12/bin/python3\nInstallation: /opt/python-3.12\n", + "executable": false + } + ] + }, + { + "kind": "command", + "command": "uv", + "arguments": ["venv", "--python", "/opt/python-3.12/bin/python3"], + "environment": {"PYTHON_HOME": "/opt/python-3.12"}, + "collect": null, + "mayUseNetwork": false, + "timeoutSeconds": 600 + } + ], + "openFiles": [], + "runtimes": [], + "toolPins": [ + { + "field": "python", + "tool": "python", + "pinAs": "project", + "home": "/opt/python-3.12", + "executable": "/opt/python-3.12/bin/python3" + } + ], + "usesNetwork": false + } + } + }, + { + "name": "plan refuses a tool below the minimum", + "manifest": "project-generator-tool-field.plugin.json", + "operation": "generationPlan", + "generatorID": "python-app", + "form": { + "projectName": "demo", + "tools": { + "python": [ + {"home": "/opt/python-3.10", "executable": "/opt/python-3.10/bin/python3", "version": "3.10.14"} + ] + } + }, + "expected": { + "status": "rejected", + "error": { + "code": "plugin.project.invalidInput", + "message": "The value of python is not accepted (toolTooOld).", + "field": "values.python" + } + } + }, + { + "name": "plan leaves an optional tool without an installation unpinned", + "manifest": "project-generator-tool-field.plugin.json", + "operation": "generationPlan", + "generatorID": "node-app", + "form": {"projectName": "demo"}, + "expected": { + "status": "ok", + "plan": { + "projectName": "demo", + "steps": [], + "openFiles": [], + "runtimes": [], + "toolPins": [], + "usesNetwork": false + } + } + }, + { + "name": "plan pins an optional tool that has an installation", + "manifest": "project-generator-tool-field.plugin.json", + "operation": "generationPlan", + "generatorID": "node-app", + "form": { + "projectName": "demo", + "tools": { + "node": [ + {"home": "/opt/node-22", "executable": "/opt/node-22/bin/node", "version": "22.11.0"} + ] + } + }, + "expected": { + "status": "ok", + "plan": { + "projectName": "demo", + "steps": [ + { + "kind": "command", + "command": "npm", + "arguments": ["init", "--yes"], + "environment": {"NODE": "/opt/node-22/bin/node"}, + "collect": null, + "mayUseNetwork": true, + "timeoutSeconds": 600 + } + ], + "openFiles": [], + "runtimes": [], + "toolPins": [ + { + "field": "node", + "tool": "node", + "pinAs": "project", + "home": "/opt/node-22", + "executable": "/opt/node-22/bin/node" + } + ], + "usesNetwork": true + } + } + }, + { + "name": "tool form treats a release with fewer trailing zeros as the same release", + "manifest": "project-generator-tool-field.plugin.json", + "operation": "generatorForm", + "generatorID": "node-app", + "form": { + "projectName": "demo", + "tools": { + "node": [ + {"home": "/opt/node-18", "executable": "/opt/node-18/bin/node", "version": "18"}, + {"home": "/opt/node-18.0", "executable": "/opt/node-18.0/bin/node", "version": "18.0.0"} + ] + } + }, + "expected": { + "status": "ok", + "form": { + "projectName": {"problem": null}, + "fields": [ + { + "id": "node", + "visible": true, + "value": "/opt/node-18", + "installations": [ + {"home": "/opt/node-18", "executable": "/opt/node-18/bin/node", "version": "18", "meetsMinimum": true}, + {"home": "/opt/node-18.0", "executable": "/opt/node-18.0/bin/node", "version": "18.0.0", "meetsMinimum": true} + ] + } + ], + "activeRequirements": [], + "valid": true + } + } + }, + { + "name": "tool form leaves an optional tool with only old installations empty", + "manifest": "project-generator-tool-field.plugin.json", + "operation": "generatorForm", + "generatorID": "node-app", + "form": { + "projectName": "demo", + "tools": { + "node": [ + {"home": "/opt/node-16", "executable": "/opt/node-16/bin/node", "version": "16.20.2"} + ] + } + }, + "expected": { + "status": "ok", + "form": { + "projectName": {"problem": null}, + "fields": [ + { + "id": "node", + "visible": true, + "value": "", + "installations": [ + {"home": "/opt/node-16", "executable": "/opt/node-16/bin/node", "version": "16.20.2", "meetsMinimum": false} + ] + } + ], + "activeRequirements": [], + "valid": true + } + } + }, + { + "name": "plan leaves an optional tool with only old installations unpinned", + "manifest": "project-generator-tool-field.plugin.json", + "operation": "generationPlan", + "generatorID": "node-app", + "form": { + "projectName": "demo", + "tools": { + "node": [ + {"home": "/opt/node-16", "executable": "/opt/node-16/bin/node", "version": "16.20.2"} + ] + } + }, + "expected": { + "status": "ok", + "plan": {"projectName": "demo", "steps": [], "openFiles": [], "runtimes": [], "toolPins": [], "usesNetwork": false} + } + }, + { + "name": "plan refuses an old optional tool the user chose", + "manifest": "project-generator-tool-field.plugin.json", + "operation": "generationPlan", + "generatorID": "node-app", + "form": { + "projectName": "demo", + "values": {"node": "/opt/node-16"}, + "tools": { + "node": [ + {"home": "/opt/node-16", "executable": "/opt/node-16/bin/node", "version": "16.20.2"} + ] + } + }, + "expected": { + "status": "rejected", + "error": {"code": "plugin.project.invalidInput", "message": "The value of node is not accepted (toolTooOld).", "field": "values.node"} + } + }, { "name": "name accepted", "operation": "checkName", diff --git a/contract/v1/fixtures/project-generators/templates/tool-field/README.md b/contract/v1/fixtures/project-generators/templates/tool-field/README.md new file mode 100644 index 0000000..9bbb191 --- /dev/null +++ b/contract/v1/fixtures/project-generators/templates/tool-field/README.md @@ -0,0 +1,4 @@ +# {{ project.name }} + +Interpreter: {{ field.python.executable }} +Installation: {{ field.python }} diff --git a/contract/v1/fixtures/toolchains/proposal/java-tool-definition.json b/contract/v1/fixtures/toolchains/proposal/java-tool-definition.json deleted file mode 100644 index 501ec9f..0000000 --- a/contract/v1/fixtures/toolchains/proposal/java-tool-definition.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "proposalVersion": 1, - "pluginID": "dev.lithe.fixture.java-support", - "requires": { "toolDefinitions": 1, "executableResolution": 2, "toolchainConsumers": 1 }, - "permissions": [], - "toolDefinitions": [ - { - "id": "java", - "displayName": "JDK", - "icon": { "host": "plugin" }, - "executableLayouts": [ - { "path": "bin/java", "platforms": ["macos", "linux"] }, - { "path": "bin/java.exe", "platforms": ["windows"] } - ], - "executables": [ - { "id": "pinned", "kind": "setting", "settingID": "executable", "layer": "project" }, - { "id": "project-request", "kind": "toolchain", "scope": "project" }, - { "id": "default", "kind": "setting", "settingID": "executable", "layer": "global" }, - { "id": "global-request", "kind": "toolchain", "scope": "global" }, - { "id": "home", "kind": "environment", "variable": "JAVA_HOME", "path": "bin/java", "platforms": ["macos", "linux"] }, - { "id": "home-windows", "kind": "environment", "variable": "JAVA_HOME", "path": "bin/java.exe", "platforms": ["windows"] }, - { "id": "shell", "kind": "command", "command": "java" }, - { "id": "detected", "kind": "toolchain", "scope": "installed" } - ], - "version": { "arguments": ["-version"], "pattern": "version \\\"(?:1\\.)?(\\d+(?:\\.\\d+)*)" }, - "homeLayouts": [ - { "match": "*.jdk", "home": "Contents/Home", "platforms": ["macos"] }, - { "match": "*", "home": "." } - ], - "discovery": [ - { "root": { "kind": "absolute", "path": "/Library/Java/JavaVirtualMachines" }, "pattern": "*.jdk", "platforms": ["macos"] }, - { "root": { "kind": "home" }, "pattern": "Library/Java/JavaVirtualMachines/*.jdk", "platforms": ["macos"] } - ], - "homeVariable": "JAVA_HOME", - "commands": ["java"], - "wrappers": [], - "exports": [ { "name": "JAVA_HOME", "value": "${home}" }, { "name": "LITHE_JAVA_PATH", "value": "${executable}" } ], - "pathPrepend": ["${home}/bin"], - "launch": ["${executable}", "${runtimeArguments}", "-jar", "${entry}", "${arguments}"] - } - ], - "consumers": [ - { - "id": "fixture-language-server", - "requiresTools": [ { "tool": "java", "minimumVersion": "21" } ], - "onToolchainChange": "restart", - "launch": { "kind": "runtime", "tool": "java", "entry": "server/fixture-language-server.jar" } - } - ] -} diff --git a/contract/v1/fixtures/toolchains/proposal/local.json b/contract/v1/fixtures/toolchains/proposal/local.json deleted file mode 100644 index 7bf8520..0000000 --- a/contract/v1/fixtures/toolchains/proposal/local.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "version": 1, - "toolchains": {}, - "pins": { - ".": { - "java": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "version": "21.0.4", - "provider": "dev.lithe.fixture.mise:mise" - } - }, - "apps/web": { - "java": { - "home": "/opt/lithe-fixtures/manual/java/17.0.12", - "version": "17.0.12", - "provider": "host:manual" - } - }, - ".config/tools": { - "java": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "version": "21.0.4", - "provider": "dev.lithe.fixture.mise:mise" - } - } - } -} diff --git a/contract/v1/fixtures/toolchains/proposal/manifest-rejections.json b/contract/v1/fixtures/toolchains/proposal/manifest-rejections.json deleted file mode 100644 index 89adc06..0000000 --- a/contract/v1/fixtures/toolchains/proposal/manifest-rejections.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "proposalVersion": 1, - "description": "Proposed rejection scenarios, not current Core tests. Apply RFC 6902 patch to base; schema checks shape, registry checks enabled definition references and manifest semantics, hostTrust checks independently verified installation provenance. Context is supplied by the test host, never trusted plugin JSON.", - "cases": [ - { - "name": "provider capability requires effective permission", - "base": "mise-provider.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/permissions", "value": ["backend.launch"] }], - "expected": { "status": "rejected", "code": "plugin.permission.undeclared" } - }, - { - "name": "export cannot replace PATH", - "base": "java-tool-definition.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/exports/0/name", "value": "PATH" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid" } - }, - { - "name": "export cannot set loader injection variable", - "base": "java-tool-definition.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/exports/0/name", "value": "DYLD_INSERT_LIBRARIES" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid" } - }, - { - "name": "PATH prepend cannot escape home", - "base": "java-tool-definition.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/pathPrepend/0", "value": "${home}/../bin" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid" } - }, - { - "name": "watch glob cannot recurse without a bound", - "base": "mise-provider.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/toolchainProviders/0/watchFiles/0/pattern", "value": "**/mise.toml" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid" } - }, - { - "name": "probe cannot pass an arbitrary script argument", - "base": "java-tool-definition.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/version/arguments", "value": ["-c", "project-code"] }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid" } - }, - { - "name": "launch interpolation is argv not shell text", - "base": "java-tool-definition.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/launch/1", "value": "--args=${arguments}" }], - "expected": { "status": "rejected", "code": "plugin.template.invalid" } - }, - { - "name": "provider backend cannot bootstrap from discovered runtime", - "base": "mise-provider.json", - "phase": "schema", - "patch": [{ "op": "replace", "path": "/backend/launch", "value": { "kind": "runtime", "tool": "node", "entry": "provider/main.mjs" } }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid" } - }, - { - "name": "consumer reference to undefined tool is not a syntax error", - "base": "java-tool-definition.json", - "phase": "registry", - "patch": [{ "op": "replace", "path": "/consumers/0/requiresTools", "value": [{ "tool": "java", "minimumVersion": "21" }, { "tool": "dev.lithe.fixture.java-support:absent" }] }], - "context": { "enabledToolIDs": ["java"] }, - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "undefinedTool" } - }, - { - "name": "community plugin cannot claim reserved short definition", - "base": "java-tool-definition.json", - "phase": "hostTrust", - "patch": [], - "context": { "verifiedOrigin": "communityPackage" }, - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "reservedToolID" } - }, - { - "name": "community definition namespace belongs to its plugin", - "base": "java-tool-definition.json", - "phase": "registry", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/id", "value": "dev.lithe.fixture.other:java" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "foreignToolNamespace" } - }, - { - "name": "later definition cannot replace existing owner", - "base": "java-tool-definition.json", - "phase": "registry", - "patch": [], - "context": { "registeredTools": [{ "id": "java", "pluginID": "dev.lithe.fixture.first-java" }] }, - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "duplicateToolID" } - }, - { - "name": "capability list cannot advertise unused provider", - "base": "java-tool-definition.json", - "phase": "registry", - "patch": [{ "op": "add", "path": "/requires/toolchainProviders", "value": 1 }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "unusedCapability" } - }, - { - "name": "pattern needs a numeric version capture", - "base": "java-tool-definition.json", - "phase": "registry", - "patch": [{ "op": "replace", "path": "/toolDefinitions/0/version/pattern", "value": "version.*" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "missingVersionCapture" } - }, - { - "name": "ordering cannot reference another contribution set", - "base": "java-tool-definition.json", - "phase": "registry", - "patch": [{ "op": "add", "path": "/toolDefinitions/0/executables/0/insertBefore", "value": "another-set" }], - "expected": { "status": "rejected", "code": "plugin.manifest.invalid", "reason": "unknownCandidateAnchor" } - } - ] -} diff --git a/contract/v1/fixtures/toolchains/proposal/mise-provider.json b/contract/v1/fixtures/toolchains/proposal/mise-provider.json deleted file mode 100644 index 4ec3e07..0000000 --- a/contract/v1/fixtures/toolchains/proposal/mise-provider.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "proposalVersion": 1, - "pluginID": "dev.lithe.fixture.mise", - "requires": { "toolchainProviders": 1, "backend": 2, "serverProcess": 1 }, - "permissions": ["backend.launch", "toolchains.provide"], - "toolchainProviders": [ - { - "id": "mise", - "displayName": "mise", - "tools": ["java", "node", "maven", "pnpm", "go"], - "watchFiles": [ - { "pattern": "mise.toml", "reader": "native" }, - { "pattern": "mise.local.toml", "reader": "native" }, - { "pattern": ".mise.toml", "reader": "native" }, - { "pattern": ".tool-versions", "reader": "compatible" }, - { "pattern": ".mise/config.toml", "reader": "native" }, - { "pattern": ".mise/config.local.toml", "reader": "native" } - ] - } - ], - "backend": { - "launch": { "kind": "packaged", "path": "bin/mise-provider" }, - "arguments": ["--stdio"] - } -} diff --git a/contract/v1/fixtures/toolchains/proposal/query-cases.json b/contract/v1/fixtures/toolchains/proposal/query-cases.json deleted file mode 100644 index e876e4d..0000000 --- a/contract/v1/fixtures/toolchains/proposal/query-cases.json +++ /dev/null @@ -1,852 +0,0 @@ -{ - "proposalVersion": 1, - "status": "review-only", - "cases": [ - { - "name": "installed project requirement", - "request": { - "jsonrpc": "2.0", - "id": 1, - "method": "toolchains/query", - "params": { - "operationID": "query-1", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 1, - "result": { - "operationID": "query-1", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "java", - "version": "21.0.4", - "home": "/opt/lithe-fixtures/installs/java/21.0.4" - } - ], - "statements": [ - { - "tool": "java", - "scope": "project", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "21", - "installation": "/opt/lithe-fixtures/installs/java/21.0.4", - "kind": "declaration", - "reader": "native" - } - ], - "watch": [ - "/opt/lithe-fixtures/workspace/mise.toml", - "/opt/lithe-fixtures/installs/java" - ] - } - }, - "filesystemEvidence": [ - { - "path": "/opt/lithe-fixtures/installs/java/21.0.4", - "canonical": "/opt/lithe-fixtures/installs/java/21.0.4", - "kind": "directory" - }, - { - "path": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "canonical": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "kind": "file", - "executable": true - } - ], - "expected": { - "validation": "accepted", - "selectedHome": "/opt/lithe-fixtures/installs/java/21.0.4" - } - }, - { - "name": "uninstalled request is retained as unmet", - "request": { - "jsonrpc": "2.0", - "id": 2, - "method": "toolchains/query", - "params": { - "operationID": "query-2", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "node" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 2, - "result": { - "operationID": "query-2", - "generation": 1, - "providerID": "mise", - "installations": [], - "statements": [ - { - "tool": "node", - "scope": "project", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "99", - "installation": null, - "kind": "declaration", - "reader": "native" - } - ], - "watch": [ - "/opt/lithe-fixtures/workspace/mise.toml" - ] - } - }, - "expected": { - "validation": "accepted", - "reason": { - "code": "requestedNotInstalled", - "tool": "node", - "provider": "dev.lithe.fixture.mise:mise", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "99" - }, - "continueToLowerLayer": true - } - }, - { - "name": "provider home inside workspace is unsafe", - "request": { - "jsonrpc": "2.0", - "id": 3, - "method": "toolchains/query", - "params": { - "operationID": "query-3", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 3, - "result": { - "operationID": "query-3", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "java", - "version": "21.0.4", - "home": "/opt/lithe-fixtures/workspace/vendor/jdk" - } - ], - "statements": [ - { - "tool": "java", - "scope": "project", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "21", - "installation": "/opt/lithe-fixtures/workspace/vendor/jdk", - "kind": "declaration", - "reader": "native" - } - ], - "watch": [] - } - }, - "expected": { - "validation": "rejected", - "code": "plugin.toolchain.unsafePath", - "rpcCode": -32076 - } - }, - { - "name": "cancelled query ignores late success", - "request": { - "jsonrpc": "2.0", - "id": 4, - "method": "toolchains/query", - "params": { - "operationID": "query-4", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 4, - "result": { - "operationID": "query-4", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "java", - "version": "21.0.4", - "home": "/opt/lithe-fixtures/installs/java/21.0.4" - } - ], - "statements": [], - "watch": [] - } - }, - "events": [ - "dispatched", - "cancelRequested", - "localCompletion", - "lateReply" - ], - "expected": { - "completionCount": 1, - "code": "plugin.toolchain.cancelled", - "rpcCode": -32800, - "publishReply": false, - "cacheReply": false - } - }, - { - "name": "global query generation rejects old result", - "request": { - "jsonrpc": "2.0", - "id": 5, - "method": "toolchains/query", - "params": { - "operationID": "query-5", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": null, - "workspace": null, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 5, - "result": { - "operationID": "query-5", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "java", - "version": "21.0.4", - "home": "/opt/lithe-fixtures/installs/java/21.0.4" - } - ], - "statements": [], - "watch": [] - } - }, - "events": [ - "dispatchedGeneration1", - "watchChanged", - "dispatchedGeneration2", - "replyGeneration1" - ], - "expected": { - "publishReply": false, - "cacheReply": false, - "requery": "allProviders" - } - }, - { - "name": "remote query never falls back to local", - "request": { - "jsonrpc": "2.0", - "id": 6, - "method": "toolchains/query", - "params": { - "operationID": "query-6", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "remote", - "id": "ssh-1" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/srv/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "expected": { - "validation": "rejected", - "code": "plugin.toolchain.authorityUnsupported", - "rpcCode": -32071, - "localProcessesStarted": 0 - } - }, - { - "name": "missing definition retains display-only requirement", - "request": { - "jsonrpc": "2.0", - "id": 7, - "method": "toolchains/query", - "params": { - "operationID": "query-7", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "maven" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 7, - "result": { - "operationID": "query-7", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "maven", - "version": "3.9.9", - "home": "/opt/lithe-fixtures/installs/maven/3.9.9" - } - ], - "statements": [ - { - "tool": "maven", - "scope": "project", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "3.9.9", - "installation": "/opt/lithe-fixtures/installs/maven/3.9.9", - "kind": "declaration", - "reader": "native" - } - ], - "watch": [] - } - }, - "enabledDefinitions": [ - "java" - ], - "expected": { - "validation": "acceptedWithFilteredAuthority", - "installations": [], - "reason": { - "code": "definitionUnavailable", - "tool": "maven", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "3.9.9" - }, - "enableActionSource": "verifiedInstalledRegistry" - } - }, - { - "name": "external spelling cannot hide workspace symlink", - "request": { - "jsonrpc": "2.0", - "id": 8, - "method": "toolchains/query", - "params": { - "operationID": "query-8", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 8, - "result": { - "operationID": "query-8", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "java", - "version": "21.0.4", - "home": "/opt/lithe-fixtures/linked-jdk" - } - ], - "statements": [], - "watch": [] - } - }, - "filesystemEvidence": [ - { - "path": "/opt/lithe-fixtures/linked-jdk", - "canonical": "/opt/lithe-fixtures/workspace/vendor/jdk", - "kind": "directory" - } - ], - "expected": { - "validation": "rejected", - "code": "plugin.toolchain.unsafePath", - "rpcCode": -32076 - } - }, - { - "name": "statement cannot reference an unlisted home", - "request": { - "jsonrpc": "2.0", - "id": 9, - "method": "toolchains/query", - "params": { - "operationID": "query-9", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 9, - "result": { - "operationID": "query-9", - "generation": 1, - "providerID": "mise", - "installations": [ - { - "tool": "java", - "version": "21.0.4", - "home": "/opt/lithe-fixtures/installs/java/21.0.4" - } - ], - "statements": [ - { - "tool": "java", - "scope": "project", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "21", - "installation": "/opt/lithe-fixtures/other-jdk", - "kind": "declaration", - "reader": "native" - } - ], - "watch": [] - } - }, - "expected": { - "validation": "rejected", - "code": "plugin.toolchain.invalidReply", - "rpcCode": -32074 - } - }, - { - "name": "query timeout remains different from cancellation", - "request": { - "jsonrpc": "2.0", - "id": 10, - "method": "toolchains/query", - "params": { - "operationID": "query-10", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "events": [ - "dispatched", - "advanceManualClockToDeadline", - "lateReply" - ], - "expected": { - "completionCount": 1, - "code": "plugin.toolchain.timeout", - "rpcCode": -32072, - "publishReply": false - } - }, - { - "name": "create missing watched file re-queries all providers", - "request": { - "jsonrpc": "2.0", - "id": 11, - "method": "toolchains/query", - "params": { - "operationID": "query-11", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "events": [ - "fingerprintMissingMiseToml", - "createMiseToml", - "watchEvent" - ], - "enabledProviders": [ - "host:system", - "dev.lithe.fixture.mise:mise", - "dev.lithe.fixture.sdkman:sdkman" - ], - "expected": { - "requery": "allProviders", - "incremental": false - } - }, - { - "name": "delete watched input re-queries all providers", - "request": { - "jsonrpc": "2.0", - "id": 12, - "method": "toolchains/query", - "params": { - "operationID": "query-12", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "events": [ - "fingerprintExistingToolVersions", - "deleteToolVersions", - "watchEvent" - ], - "expected": { - "requery": "allProviders", - "incremental": false - } - }, - { - "name": "provider disable invalidates pending reply", - "request": { - "jsonrpc": "2.0", - "id": 13, - "method": "toolchains/query", - "params": { - "operationID": "query-13", - "generation": 1, - "providerID": "mise", - "authority": { - "kind": "local" - }, - "context": { - "contextID": "context-1", - "generation": 1 - }, - "workspace": { - "rootID": "root-1", - "root": "/opt/lithe-fixtures/workspace", - "directory": "." - }, - "tools": [ - "java" - ] - } - }, - "response": { - "jsonrpc": "2.0", - "id": 13, - "result": { - "operationID": "query-13", - "generation": 1, - "providerID": "mise", - "installations": [], - "statements": [], - "watch": [] - } - }, - "events": [ - "dispatched", - "providerDisabled", - "lateReply" - ], - "expected": { - "publishReply": false, - "cacheReply": false, - "ownedListenersRemaining": 0 - } - } - ], - "errorResponses": [ - { - "jsonrpc": "2.0", - "id": 100, - "error": { - "code": -32601, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.methodUnavailable", - "operationID": "query-100", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 101, - "error": { - "code": -32602, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.invalidParams", - "operationID": "query-101", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 102, - "error": { - "code": -32800, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.cancelled", - "operationID": "query-102", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 103, - "error": { - "code": -32070, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.permissionDenied", - "operationID": "query-103", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 104, - "error": { - "code": -32071, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.authorityUnsupported", - "operationID": "query-104", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 105, - "error": { - "code": -32072, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.timeout", - "operationID": "query-105", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 106, - "error": { - "code": -32073, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.providerUnavailable", - "operationID": "query-106", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 107, - "error": { - "code": -32074, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.invalidReply", - "operationID": "query-107", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 108, - "error": { - "code": -32075, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.limitExceeded", - "operationID": "query-108", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 109, - "error": { - "code": -32076, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.unsafePath", - "operationID": "query-109", - "providerID": "mise" - } - } - }, - { - "jsonrpc": "2.0", - "id": 110, - "error": { - "code": -32077, - "message": "Fixture diagnostic.", - "data": { - "code": "plugin.toolchain.operationExpired", - "operationID": "query-110", - "providerID": "mise" - } - } - } - ] -} diff --git a/contract/v1/fixtures/toolchains/proposal/resolution-cases.json b/contract/v1/fixtures/toolchains/proposal/resolution-cases.json deleted file mode 100644 index 9dd4a35..0000000 --- a/contract/v1/fixtures/toolchains/proposal/resolution-cases.json +++ /dev/null @@ -1,571 +0,0 @@ -{ - "proposalVersion": 1, - "status": "review-only", - "cases": [ - { - "name": "nearest ancestor pin applies component-wise", - "input": { - "directory": "apps/web/src", - "localFixture": "local.json" - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": "apps/web/src", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/manual/java/17.0.12", - "executable": "/opt/lithe-fixtures/manual/java/17.0.12/bin/java", - "version": "17.0.12", - "provider": "host:manual", - "layer": "pinned", - "projectCode": false, - "environment": { - "JAVA_HOME": "/opt/lithe-fixtures/manual/java/17.0.12" - }, - "pathPrepend": [ - "/opt/lithe-fixtures/manual/java/17.0.12/bin" - ] - } - }, - "trace": [ - { - "candidateID": "pinned", - "layer": "pinned", - "state": "resolved", - "source": "apps/web" - } - ], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "similarly prefixed sibling does not inherit child pin", - "input": { - "directory": "apps/website", - "localFixture": "local.json" - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": "apps/website", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "executable": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "version": "21.0.4", - "provider": "dev.lithe.fixture.mise:mise", - "layer": "pinned", - "projectCode": false, - "environment": { - "JAVA_HOME": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "pathPrepend": [ - "/opt/lithe-fixtures/installs/java/21.0.4/bin" - ] - } - }, - "trace": [ - { - "candidateID": "pinned", - "layer": "pinned", - "state": "resolved", - "source": "." - } - ], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "nonempty unavailable pin stops fallback", - "input": { - "pinHome": "/opt/lithe-fixtures/missing", - "installedHomes": [ - "/opt/lithe-fixtures/installs/java/21.0.4" - ] - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "terminalFailure", - "reason": { - "code": "notFound" - } - }, - "trace": [ - { - "candidateID": "pinned", - "layer": "pinned", - "state": "terminalFailure", - "reason": { - "code": "notFound" - } - } - ], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "uninstalled project version remains visible after fallback", - "input": { - "statement": { - "tool": "java", - "scope": "project", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "99", - "installation": null, - "kind": "declaration", - "reader": "native" - }, - "loginShellHome": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "executable": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "version": "21.0.4", - "provider": "host:system", - "layer": "global", - "projectCode": false, - "environment": { - "JAVA_HOME": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "pathPrepend": [ - "/opt/lithe-fixtures/installs/java/21.0.4/bin" - ] - } - }, - "trace": [ - { - "candidateID": "project-request", - "layer": "project", - "state": "skip", - "reason": { - "code": "requestedNotInstalled", - "tool": "java", - "provider": "dev.lithe.fixture.mise:mise", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "99" - } - }, - { - "candidateID": "shell-command", - "layer": "global", - "state": "resolved" - } - ], - "unmet": [ - { - "code": "requestedNotInstalled", - "tool": "java", - "provider": "dev.lithe.fixture.mise:mise", - "source": "/opt/lithe-fixtures/workspace/mise.toml", - "request": "99" - } - ], - "conflicts": [] - } - }, - { - "name": "conflicting globals skip providers and use login shell", - "input": { - "providerGlobals": [ - { - "provider": "dev.lithe.fixture.mise:mise", - "home": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - { - "provider": "dev.lithe.fixture.sdkman:sdkman", - "home": "/opt/lithe-fixtures/sdkman/java/17.0.12" - } - ], - "loginShellHome": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "executable": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "version": "21.0.4", - "provider": "host:system", - "layer": "global", - "projectCode": false, - "environment": { - "JAVA_HOME": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "pathPrepend": [ - "/opt/lithe-fixtures/installs/java/21.0.4/bin" - ] - } - }, - "trace": [ - { - "candidateID": "global-request", - "layer": "global", - "state": "skip", - "reason": { - "code": "providerConflict" - } - }, - { - "candidateID": "shell-command", - "layer": "global", - "state": "resolved" - } - ], - "unmet": [], - "conflicts": [ - { - "scope": "global", - "providers": [ - "dev.lithe.fixture.mise:mise", - "dev.lithe.fixture.sdkman:sdkman" - ], - "action": "chooseProvider" - } - ] - } - }, - { - "name": "automatic compares numeric components not text", - "input": { - "probedVersions": [ - "9.0.9", - "21.0.4", - "17.0.12" - ], - "minimumVersion": "17" - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "executable": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "version": "21.0.4", - "provider": "dev.lithe.fixture.mise:mise", - "layer": "automatic", - "projectCode": false, - "environment": { - "JAVA_HOME": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "pathPrepend": [ - "/opt/lithe-fixtures/installs/java/21.0.4/bin" - ] - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "manual-only newer installation is not automatic", - "input": { - "installedHomes": [ - "/opt/lithe-fixtures/installs/java/21.0.4" - ], - "manualInstallations": [ - { - "home": "/opt/lithe-fixtures/manual/java/99", - "version": "99" - } - ] - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "executable": "/opt/lithe-fixtures/installs/java/21.0.4/bin/java", - "version": "21.0.4", - "provider": "dev.lithe.fixture.mise:mise", - "layer": "automatic", - "projectCode": false, - "environment": { - "JAVA_HOME": "/opt/lithe-fixtures/installs/java/21.0.4" - }, - "pathPrepend": [ - "/opt/lithe-fixtures/installs/java/21.0.4/bin" - ] - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "child declaration beats more distant wrapper", - "input": { - "directory": "apps/web", - "projectCandidates": [ - { - "directory": ".", - "kind": "wrapper", - "path": "mvnw" - }, - { - "directory": "apps/web", - "kind": "declaration", - "home": "/opt/lithe-fixtures/installs/maven/3.9.9" - } - ] - }, - "expected": { - "operationID": "resolve-1", - "tool": "maven", - "directory": "apps/web", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/installs/maven/3.9.9", - "executable": "/opt/lithe-fixtures/installs/maven/3.9.9/bin/mvn", - "version": "3.9.9", - "provider": "dev.lithe.fixture.mise:mise", - "layer": "project", - "projectCode": false, - "environment": {}, - "pathPrepend": [ - "/opt/lithe-fixtures/installs/maven/3.9.9/bin" - ] - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "same-directory wrapper precedes manager statement after user action", - "input": { - "directory": ".", - "userActionAuthorized": true, - "wrapper": "/opt/lithe-fixtures/workspace/mvnw", - "providerHome": "/opt/lithe-fixtures/installs/maven/3.9.9" - }, - "expected": { - "operationID": "resolve-1", - "tool": "maven", - "directory": ".", - "outcome": { - "kind": "resolved", - "selection": { - "home": null, - "executable": "/opt/lithe-fixtures/workspace/mvnw", - "version": null, - "provider": "dev.lithe.fixture.maven:wrapper", - "layer": "project", - "projectCode": true, - "environment": {}, - "pathPrepend": [] - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "native reader beats compatible reader for the same file", - "input": { - "source": "/opt/lithe-fixtures/workspace/.nvmrc", - "readers": [ - { - "provider": "dev.lithe.fixture.mise:mise", - "reader": "compatible", - "home": "/opt/lithe-fixtures/installs/node/22.0.0" - }, - { - "provider": "dev.lithe.fixture.nvm:nvm", - "reader": "native", - "home": "/opt/lithe-fixtures/nvm/node/20.0.0" - } - ] - }, - "expected": { - "operationID": "resolve-1", - "tool": "node", - "directory": ".", - "outcome": { - "kind": "resolved", - "selection": { - "home": "/opt/lithe-fixtures/nvm/node/20.0.0", - "executable": "/opt/lithe-fixtures/nvm/node/20.0.0/bin/node", - "version": "20.0.0", - "provider": "dev.lithe.fixture.nvm:nvm", - "layer": "project", - "projectCode": false, - "environment": {}, - "pathPrepend": [ - "/opt/lithe-fixtures/nvm/node/20.0.0/bin" - ] - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "disabled definition leaves pin but no executable authority", - "input": { - "localFixture": "local.json", - "enabledDefinitions": [] - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "skip", - "reason": { - "code": "definitionUnavailable", - "tool": "java" - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "unknown probed version cannot satisfy automatic minimum", - "input": { - "installedHomes": [ - "/opt/lithe-fixtures/installs/java/21.0.4" - ], - "probeOutput": "unknown", - "minimumVersion": "17" - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "skip", - "reason": { - "code": "versionUnsatisfied" - } - }, - "trace": [], - "unmet": [], - "conflicts": [] - } - }, - { - "name": "equal newest versions at distinct homes require explicit choice", - "input": { - "installations": [ - { - "provider": "dev.lithe.fixture.mise:mise", - "home": "/opt/lithe-fixtures/installs/java/21.0.4", - "normalizedVersion": "21.0.4" - }, - { - "provider": "dev.lithe.fixture.mise:mise", - "home": "/opt/lithe-fixtures/other-installs/java/21.0.4", - "normalizedVersion": "21.0.4" - } - ] - }, - "expected": { - "operationID": "resolve-1", - "tool": "java", - "directory": ".", - "outcome": { - "kind": "skip", - "reason": { - "code": "ambiguousInstallation", - "tool": "java" - } - }, - "trace": [ - { - "candidateID": "installed", - "layer": "automatic", - "state": "skip", - "reason": { - "code": "ambiguousInstallation", - "tool": "java" - } - } - ], - "unmet": [], - "conflicts": [ - { - "scope": "automatic", - "providers": [ - "dev.lithe.fixture.mise:mise" - ], - "installations": [ - "/opt/lithe-fixtures/installs/java/21.0.4", - "/opt/lithe-fixtures/other-installs/java/21.0.4" - ], - "action": "chooseInstallation" - } - ] - } - } - ], - "invalidLocalDocuments": [ - { - "name": "parent segment", - "value": { - "version": 1, - "toolchains": {}, - "pins": { - "apps/../other": {} - } - } - }, - { - "name": "absolute directory key", - "value": { - "version": 1, - "toolchains": {}, - "pins": { - "/apps": {} - } - } - }, - { - "name": "relative home", - "value": { - "version": 1, - "toolchains": {}, - "pins": { - ".": { - "java": { - "home": "vendor/jdk", - "version": "21", - "provider": "host:manual" - } - } - } - } - } - ] -} diff --git a/contract/v1/plugin-manifest.schema.json b/contract/v1/plugin-manifest.schema.json index a418847..3122ad9 100644 --- a/contract/v1/plugin-manifest.schema.json +++ b/contract/v1/plugin-manifest.schema.json @@ -46,7 +46,7 @@ "languageServers": { "description": "Required exactly when languageServers is non-empty. Version 2 adds languageServers[].workspaceConfiguration.", "enum": [1, 2] }, "languages": { "description": "Version 2 adds languages[].grammar and languages[].configuration and is required exactly when one of them is used.", "enum": [1, 2] }, "packageTrust": { "description": "Required exactly when vendor.bundledSignatureRequirement is present.", "enum": [1] }, - "projectGenerators": { "description": "Entries of the New Project window: fields, requirements, and steps.", "enum": [1] }, + "projectGenerators": { "description": "Entries of the New Project window: fields, requirements, and steps. Version 2 adds the tool field and retires the runtime field.", "enum": [1, 2] }, "serverProcess": { "description": "The launch fields of language servers, of build providers that start their own process, of VCS providers, of debug adapters, and of the plugin backend. Version 2 adds runSelectedRuntime and is required exactly when a debug adapter uses it (pluginAPIVersion 4).", "enum": [1, 2] }, "settings": { "description": "Required exactly when settings or settingActions is non-empty. Version 1 is the API level 2 section: integer, enum, and boolean options. Version 2 adds scope, effect, page, section, the multiSelect, text, file, and directory types, and settingActions.", "enum": [1, 2] }, "toolDefinitions": { "const": 1 }, @@ -847,12 +847,12 @@ "required": ["id", "type", "title"], "properties": { "id": { "type": "string", "pattern": "^[a-z][A-Za-z0-9]*$" }, - "type": { "enum": ["text", "enum", "boolean", "multiSelect", "runtime"] }, + "type": { "enum": ["text", "enum", "boolean", "multiSelect", "runtime", "tool"], "description": "runtime needs requires.projectGenerators 1; tool needs requires.projectGenerators 2." }, "title": { "type": "string", "minLength": 1, "maxLength": 120 }, "description": { "type": "string", "minLength": 1, "maxLength": 480 }, "advanced": { "type": "boolean" }, "visibleWhen": { "$ref": "#/$defs/generatorConditions" }, - "default": { "description": "text: a template over ${project.name} and earlier fields; enum: an option; boolean; multiSelect: options. Not allowed for runtime." }, + "default": { "description": "text: a template over ${project.name} and earlier fields; enum: an option; boolean; multiSelect: options. Not allowed for runtime and tool." }, "options": { "type": "array", "items": { @@ -872,7 +872,14 @@ "placeholder": { "type": "string", "minLength": 1, "maxLength": 120 }, "optional": { "type": "boolean" }, "runtime": { "enum": ["java"] }, - "minimumVersion": { "type": "string" }, + "tool": { + "description": "tool: a tool definition ID, the short ID of a verified official definition or :.", + "type": "string", + "maxLength": 192, + "pattern": "^([a-z0-9]+([.-][a-z0-9]+)*:)?[a-z][a-z0-9-]{0,63}$" + }, + "pinAs": { "description": "tool: project saves the selection as the created project's own tool choice.", "enum": ["project"] }, + "minimumVersion": { "type": "string", "description": "runtime and tool: one to three dotted numbers." }, "maximumVersion": { "type": "string", "description": "text and enum: the highest version the value may name, a template over earlier fields such as ${field.jdk.major}." } } }, diff --git a/contract/v1/toolchains/README.md b/contract/v1/toolchains/README.md index 23b03ef..a11e308 100644 --- a/contract/v1/toolchains/README.md +++ b/contract/v1/toolchains/README.md @@ -16,10 +16,9 @@ toolchain adapter and does not advertise `toolDefinitions` or `toolchainProviders`, so Core refuses packages that require them there. Publisher signatures provide independent origin evidence when present; legacy unsigned records never do. -The [broader proposal](proposal/README.md) still describes unsupported -consumer migration, native lifecycle, persistence and manager-plugin work. -The active shape is [plugin-manifest.schema.json](../plugin-manifest.schema.json), -not the proposal envelopes. +Consumer migration, the native query lifecycle and cache, pin saving and +version-manager plugins are not specified yet. The manifest shape is +[plugin-manifest.schema.json](../plugin-manifest.schema.json). ## Manifest admission and registry @@ -121,6 +120,46 @@ providers:[{pluginID,provider}]}` in stable order. Duplicate plugin/tool ownersh rejects the batch without replacing earlier owners. Limits are 128 plugins, 512 aggregate definitions and 256 aggregate providers. +## Provider backend negotiation + +A provider answers over its plugin backend's JSON-RPC connection, the same +`serverProcess` transport other backend requests use. The backend need not +declare a language, language server, Build provider or callable function. + +The host starts the backend with `lsp.startServer` and passes the manifest's +provider IDs as `toolchainProviders`. Only a plugin-owned session that declares +at least one provider is offered the protocol, in the client capabilities of +`initialize`: + +```json +{"experimental": {"litheToolchains": {"versions": [1], "providers": ["first", "second"]}}} +``` + +`providers` lists the declared IDs in sorted order. The backend selects in the +server capabilities of its `initialize` result: + +```json +{"experimental": {"litheToolchains": {"version": 1, "providers": ["first"]}}} +``` + +`version` must be an offered version and `providers` a non-empty list of +offered IDs without repeats; other fields are ignored. Any other answer, including +an unoffered version, an undeclared, repeated or non-string ID, or an empty list, +enables no provider at all rather than a partial set, and no query is sent. On +success Core emits a `toolchainProviders` runtime event with `result: {providers}`. + +The host sends each query with `lsp.toolchainRequest` +`{sessionId, operationId?, params, timeoutMilliseconds?}`, where `params` is the +`toolchains/query` params described below; Core writes the request with that +method and its own JSON-RPC ID. A `params.providerID` the backend did not select +is refused with `not_supported` and details `plugin.toolchain.providerUnavailable`, +and nothing is written. `timeoutMilliseconds` is clamped to 1-60,000; without it +the session's request timeout applies. Completion carries `result.value: {id, +response}`: the wire ID and the complete, unvalidated response, error replies +included. The host passes it with the same `params` to `decodeReply`. +Cancellation and late results follow +[`lsp.toolchainRequest`](https://github.com/1lck/Lithe-IDEA/blob/preview/shared/contracts/rust-core-api.md). + ## Provider reply validation `decodeReply` takes `{registry,pluginID,request,response,currentGeneration, @@ -164,10 +203,46 @@ remote authority, unsafe paths and malformed error mappings reject it. The snapshot contains `{installations,statements,watch,diagnostics}`; normalized entries carry qualified `:` provenance and project source -directory. JSON-RPC errors are checked against the distinct semantic/numeric map -in the [protocol proposal](proposal/protocol.md#stable-failure-mapping). -Cancellation, deadlines, cache invalidation and whole-generation requery remain -native-client implementation work, not behavior performed by this decoder. +directory. Cancellation, deadlines, cache invalidation and whole-generation +requery remain native-client implementation work, not behavior performed by this +decoder. + +### Error replies + +A provider that cannot answer returns a JSON-RPC `error` instead of `result`. +`error` has exactly `{code,message,data}`, and `data` has `{code,operationID, +providerID}` plus an optional `details`: + +- `code` is a number from the table below and `data.code` is the stable code + on its row. Each number has exactly one stable code. +- `operationID` and `providerID` equal those of the request `params`. +- `message` is non-empty diagnostic text of at most 1,024 UTF-8 bytes without + control characters. It is shown as provider text and never selects behavior. +- `details`, when present, is an object of at most 4,096 bytes serialized. It + must not carry credentials. + +`decodeReply` then refuses with the stable code and the provider's `message`. +An error reply that breaks any of these rules, including an unknown number or a +number that does not match `data.code`, is refused as +`plugin.toolchain.invalidReply`. + +| JSON-RPC | Stable code | Meaning | +| --- | --- | --- | +| `-32601` | `plugin.toolchain.methodUnavailable` | The backend does not serve the query method | +| `-32602` | `plugin.toolchain.invalidParams` | Malformed request or invalid declared input | +| `-32800` | `plugin.toolchain.cancelled` | The caller cancelled the request | +| `-32070` | `plugin.toolchain.permissionDenied` | A permission, review or enabled-state prerequisite is missing | +| `-32071` | `plugin.toolchain.authorityUnsupported` | The provider cannot serve the requested authority | +| `-32072` | `plugin.toolchain.timeout` | The local deadline passed | +| `-32073` | `plugin.toolchain.providerUnavailable` | The backend failed to start, disconnected, or its upstream query failed | +| `-32074` | `plugin.toolchain.invalidReply` | Malformed reply, or an installation reference to another tool | +| `-32075` | `plugin.toolchain.limitExceeded` | A reply, scan or watch budget was exceeded | +| `-32076` | `plugin.toolchain.unsafePath` | A path is not absolute, escapes its containment or fails executable checks | +| `-32077` | `plugin.toolchain.operationExpired` | The context or generation was already stale when the query was sent | + +A missing installation, an incompatible version or conflicting requirements are +not errors: report them in `result` (an unmet requirement has +`installation:null`) and let the resolver give the reason. When a query gets no reply at all, the host asks `failureReply` for one. `params` is the query's own `params` and `code` is `plugin.toolchain.timeout` (the local @@ -297,7 +372,9 @@ Production-shape examples are [tool-definition.plugin.json](../fixtures/toolchai and [toolchain-provider.plugin.json](../fixtures/toolchains/toolchain-provider.plugin.json). Core acceptance vectors are in [toolchain_definitions.rs](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_definitions.rs), -with sibling pins/query/resolution suites. Passing them is not native product -verification. Contract changes must be mirrored after merge with +with sibling pins/query/resolution suites. The pin document, query exchange and +one error reply per stable code that those suites share are inline in +[toolchain_vectors.rs](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_vectors.rs). +Passing them is not native product verification. Contract changes must be mirrored after merge with `node scripts/contract.mjs sync --revision ` in [lithe-plugin-docs](https://github.com/Lithe-IDEA/lithe-plugin-docs). diff --git a/contract/v1/toolchains/proposal/README.md b/contract/v1/toolchains/proposal/README.md deleted file mode 100644 index e250eed..0000000 --- a/contract/v1/toolchains/proposal/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# Plugin-injected toolchains (draft) - -> **Status: broader proposal; a Core-only foundation is now implemented.** -> The implemented subset is specified in [the active contract](../README.md). -> This directory remains a review artifact for -> [PR #1581's replacement plan](https://github.com/1lck/Lithe-IDEA/pull/1581#issuecomment-6541366112). -> It is not an extension of the currently accepted `plugin.json` format. -> The active manifest admits the implemented definition/provider fields, not -> these projection envelopes or the whole proposal. Native runtime selection, -> consumer migration, query transport, persistence and UI remain unimplemented. - -## Ownership - -A **tool definition** describes a tool's identity, executable layouts, version -probe, discovery locations, launch template, and environment exports. Only an -enabled plugin contributes a definition. The host has no built-in definitions, -reserved-name alias table, or tool-name-dependent launch logic in the proposed -path. Officially signed plugins may define short IDs; community plugins define -IDs in their own plugin namespace and may provide installations for any enabled -definition, including official short IDs. Verified package identity, not a -manifest assertion or ID prefix, establishes official status. - -A **toolchain provider** reports installed tools and, when it declares watched -files, the project's or user's requirements. It covers installation shapes the -frozen declarative `discovery` primitives cannot express; a plugin may provide -installations of a tool it defines. Both routes feed one automatic candidate set. -Version-manager configuration, version aliases, and file precedence -remain upstream/provider-owned. The host never interprets opaque version -requests such as `lts`. A **consumer** references a defined tool and declares -whether a changed selection needs a restart, resynchronization, or the next run. -These are independent contributions, not additional language/build requirements. - -The host owns a single generic resolver and query client, directory-scoped pins, -and the environment/sources presentation skeleton. Provider-specific settings -are injected through that provider plugin's existing settings page. Definitions -and provider replies are untrusted input; permission to provide toolchains does -not authorize replacing unrelated host commands. - -## Review Artifacts - -| File | Purpose | -| --- | --- | -| [manifest.md](manifest.md) | Definition, provider, consumer, trust, and capability contribution rules | -| [manifest-contributions.schema.json](manifest-contributions.schema.json) | Standalone draft contribution projection, not a complete plugin manifest | -| [protocol.md](protocol.md) | Query lifecycle, validation, four-layer selection, storage, and stable failures | -| [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/toolchains/proposal/` | Illustrative contributions, queries, resolution outcomes, pins, and rejections | - -Schemas describe JSON shape. Signature verification, duplicate-definition -registration, enabled-plugin ownership, path containment, executable evidence, -and lifecycle state require separate semantic validation during implementation. -A shape-valid reply is never sufficient permission to launch a program. -Example filesystem paths are fictional and must not be probed on the reader's -machine; filesystem observations in cases are inputs for future test doubles. - -## Required Behavior - -Every tool uses the existing shared executable-candidate mechanism, extended -with `toolchain` candidates and structured `skip` reasons. There is no parallel -Java, Maven, or generic-command selection chain in the proposed path. - -| Layer | Selection rule | -| --- | --- | -| Pin | Nearest ancestor directory's explicit pin; an unusable pin is a terminal failure, never automatic fallback | -| Project | Nearest declaring directory; at equal depth wrapper before environment before version statement; for the same file native before compatible reader | -| Global | Lithe's explicit global default, then provider defaults; conflicting provider defaults require a user choice, using login-shell discovery until chosen | -| Automatic | Newest eligible provider installation satisfying the minimum version; manually added installations are excluded | - -An uninstalled project requirement skips with an explicit reason and stays -visible as unmet even if a lower layer succeeds. Provider installations cannot -come from the workspace; project wrappers remain project code and require the -consumer's existing explicit-action/consent policy. Queries, discovery, settings -inspection, and passive project import must not execute project code. - -The built-in **system provider** is a mechanism, not a contributor of definitions: -it scans only enabled definitions' declared locations. Adding a manager or tool -must not require a new parser, directory constant, or tool-name branch in Core. -The first implementation must prove this with two different providers. - -Each query has an operation identity, cancellation, a bounded deadline, and a -snapshot generation. New, changed, or deleted declared watch inputs invalidate -the entire generation and requery all enabled providers. Late replies cannot -repopulate revoked state. Disabling/uninstalling the defining plugin removes its -tool rows, consumers, settings, and exports, but retains stored pins for a later -reenable. Unknown/disabled definitions grant no executable authority; only bounded -display diagnostics may remain to explain the missing plugin. - -Reading `.lithe/toolchains/local.json` never creates it. Explicitly confirmed pin -edits use conflict-protected atomic replacement, then reread the result; the first -write preserves `.lithe/.gitignore` and adds `/toolchains/local.json`. Manually -added installations live in platform-owned application storage and are pin-only. -Installed app bundles and plugin packages remain read-only throughout. - -## Staged Landing And Compatibility - -1. **Proposal:** review schemas, examples, trust boundaries, and - the decision record. No host capability is enabled and no production format - is relaxed merely by this draft. Only the active contract allocates supported - Core capability versions; native hosts have not advertised them. -2. **Core and query client:** implement definition/answer validation, candidate - extensions, pin inheritance, full-generation invalidation, and macOS query - lifecycle. Preserve strict rejection by unsupported hosts. System discovery - consumes plugin declarations, never a built-in definition data file. -3. **Selection and UI:** integrate consumers and the environment/sources pages, - move Java definitions into Java Support and coordinate Maven definitions with - [PR #1558](https://github.com/1lck/Lithe-IDEA/pull/1558). Remove migrated legacy - special cases in the same slice. Other tools keep existing behavior until - their official plugins declare definitions; do not add new compatibility - aliases or tool-specific branches. -4. **Provider acceptance:** mise delegates to its upstream machine-readable CLI; - SDKMAN supplies the second, different provider without Core changes. Each - owns its own settings and bootstrap runtime. Backend-bearing providers still - require restart in the initial implementation; this proposal does not relax - live activation. - -The existing executable-resolution v1 contract does not permit project tool -overrides and supports only the documented consumer positions. These new -semantics need explicit capability admission and implemented adapters; do not -retroactively change v1 or advertise support based on schema changes alone. -Runtime minimum-version checking must use the definition's version probe rather -than a new per-tool branch. No current API is removed by this proposal. - -The Chinese architecture decision and acceptance criteria are recorded in the -[proposed Agent Note](https://github.com/1lck/Lithe-IDEA/blob/preview/.agents/notes/proposed/architecture/2026-10-31-plugin-injected-toolchains.md). -The UI reference is the -[interactive prototype](https://gist.github.com/xiaoyumuxi/2ae889a6592599b13998bc811c647942). -This directory specifies host-readable contracts, not a plugin-author tutorial -or SDK implementation; those belong in `lithe-plugin-docs`. - -After the proposal merges, sync the byte-identical contract mirror in -`Lithe-IDEA/lithe-plugin-docs` using -`node scripts/contract.mjs sync --revision ` and commit -there. Draft status must survive the sync. SDK protocol compatibility and both -SDK end-to-end suites become implementation-stage gates; this proposal does not -claim that either SDK already implements these messages. diff --git a/contract/v1/toolchains/proposal/local.schema.json b/contract/v1/toolchains/proposal/local.schema.json deleted file mode 100644 index 481d584..0000000 --- a/contract/v1/toolchains/proposal/local.schema.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/Lithe-IDEA/lithe-plugin-docs/main/contract/v1/toolchains/proposal/local.schema.json", - "title": "Proposed local toolchain pins extension (not implemented)", - "description": "Review-only complete document shape for .lithe/toolchains/local.json. The active toolchain-local-v1.schema.json remains unchanged and does not accept pins.", - "type": "object", - "additionalProperties": false, - "required": [ - "version", - "toolchains" - ], - "properties": { - "version": { - "const": 1 - }, - "toolchains": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": false, - "properties": { - "home": { - "type": "string", - "minLength": 1 - }, - "executable": { - "type": "string", - "minLength": 1 - }, - "javaHome": { - "type": "string", - "minLength": 1 - } - } - }, - "description": "Existing v1 entries are preserved byte-semantically; new resolution does not manufacture aliases to legacy project-jdk/project-maven IDs." - }, - "pins": { - "type": "object", - "maxProperties": 1024, - "propertyNames": { - "$ref": "#/$defs/directory" - }, - "additionalProperties": { - "type": "object", - "maxProperties": 64, - "propertyNames": { - "$ref": "#/$defs/toolID" - }, - "additionalProperties": { - "$ref": "#/$defs/pin" - } - } - } - }, - "$defs": { - "directory": { - "type": "string", - "maxLength": 2048, - "pattern": "^(?:\\.|(?!\\.\\.?(?:/|$))[^/\\\\:]+(?:/(?!\\.\\.?(?:/|$))[^/\\\\:]+)*)$", - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - } - }, - "toolID": { - "$ref": "manifest-contributions.schema.json#/$defs/toolID" - }, - "nativePath": { - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^(?:/|[A-Za-z]:[\\\\/]|\\\\\\\\)", - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - } - }, - "pin": { - "type": "object", - "additionalProperties": false, - "required": [ - "home", - "version", - "provider" - ], - "properties": { - "home": { - "$ref": "#/$defs/nativePath" - }, - "version": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - } - }, - "provider": { - "type": "string", - "minLength": 1, - "maxLength": 256, - "pattern": "^[a-z][a-z0-9.-]*:[a-z][a-z0-9-]*$", - "description": "Qualified pluginID:providerID, or reserved host:manual / host:system; not an activation instruction." - } - } - } - } -} diff --git a/contract/v1/toolchains/proposal/manifest-contributions.schema.json b/contract/v1/toolchains/proposal/manifest-contributions.schema.json deleted file mode 100644 index a86dde1..0000000 --- a/contract/v1/toolchains/proposal/manifest-contributions.schema.json +++ /dev/null @@ -1,260 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/Lithe-IDEA/lithe-plugin-docs/main/contract/v1/toolchains/proposal/manifest-contributions.schema.json", - "title": "PROPOSAL: toolchain contribution projections, not a plugin manifest", - "description": "A standalone review envelope for proposed manifest fields. Passing this schema does not make a plugin installable and does not prove signature, registry, filesystem or executable safety. See manifest.md.", - "type": "object", - "additionalProperties": false, - "required": ["proposalVersion", "pluginID", "requires", "permissions"], - "properties": { - "proposalVersion": { "const": 1 }, - "pluginID": { "type": "string", "pattern": "^[a-z0-9]+(?:[.-][a-z0-9]+)*$", "maxLength": 128 }, - "requires": { - "type": "object", "additionalProperties": false, "minProperties": 1, - "properties": { - "toolDefinitions": { "const": 1 }, - "toolchainProviders": { "const": 1 }, - "toolchainConsumers": { "const": 1 }, - "executableResolution": { "const": 2 }, - "backend": { "const": 2 }, - "serverProcess": { "const": 1 } - } - }, - "permissions": { - "type": "array", "maxItems": 16, "uniqueItems": true, - "items": { "enum": ["toolchains.provide", "backend.launch"] } - }, - "toolDefinitions": { "type": "array", "minItems": 1, "maxItems": 32, "items": { "$ref": "#/$defs/toolDefinition" } }, - "toolchainProviders": { "type": "array", "minItems": 1, "maxItems": 8, "items": { "$ref": "#/$defs/provider" } }, - "backend": { "$ref": "#/$defs/backend" }, - "consumers": { - "description": "Review projection of fields on existing language/build/run/debug contributions; NOT a proposed top-level manifest field.", - "type": "array", "minItems": 1, "maxItems": 64, "items": { "$ref": "#/$defs/consumer" } - } - }, - "anyOf": [ { "required": ["toolDefinitions"] }, { "required": ["toolchainProviders"] }, { "required": ["consumers"] } ], - "allOf": [ - { - "if": { "required": ["toolDefinitions"] }, - "then": { "properties": { "requires": { "required": ["toolDefinitions", "executableResolution"] } } } - }, - { - "if": { "required": ["toolchainProviders"] }, - "then": { - "required": ["backend"], - "properties": { - "requires": { "required": ["toolchainProviders", "backend", "serverProcess"] }, - "permissions": { "allOf": [ { "contains": { "const": "toolchains.provide" } }, { "contains": { "const": "backend.launch" } } ] } - } - } - }, - { - "if": { "required": ["consumers"] }, - "then": { "properties": { "requires": { "required": ["toolchainConsumers"] } } } - } - ], - "$defs": { - "identifier": { "type": "string", "pattern": "^[a-z][a-z0-9-]{0,63}$" }, - "toolID": { "type": "string", "pattern": "^(?:[a-z0-9]+(?:[.-][a-z0-9]+)*:)?[a-z][a-z0-9-]{0,63}$", "maxLength": 192 }, - "label": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[^\\u0000-\\u001f\\u007f]+$" }, - "relativePath": { - "type": "string", "minLength": 1, "maxLength": 256, - "pattern": "^(?!/)(?!.*(?:^|/)(?:\\.|\\.\\.)(?:/|$))[^/\\\\:*?\\u0000-\\u001f\\u007f]+(?:/[^/\\\\:*?\\u0000-\\u001f\\u007f]+)*$" - }, - "boundedGlob": { - "description": "At most eight segments; only single-segment * is supported, never **, ?, character classes, or traversal. Semantic validation also bounds expansion.", - "type": "string", "minLength": 1, "maxLength": 256, - "pattern": "^(?!/)(?!.*\\*\\*)(?!.*(?:^|/)(?:\\.|\\.\\.)(?:/|$))[^/\\\\:?\\[\\]{}\\u0000-\\u001f\\u007f]+(?:/[^/\\\\:?\\[\\]{}\\u0000-\\u001f\\u007f]+){0,7}$" - }, - "command": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$" }, - "safeEnvironmentName": { "type": "string", "maxLength": 64, "pattern": "^(?!PATH$)(?!DYLD_)(?!LD_)[A-Z_][A-Z0-9_]*$" }, - "platforms": { "type": "array", "minItems": 1, "maxItems": 3, "uniqueItems": true, "items": { "enum": ["macos", "windows", "linux"] } }, - "version": { - "type": "object", "additionalProperties": false, "required": ["arguments", "pattern"], - "properties": { - "arguments": { "type": "array", "minItems": 1, "maxItems": 8, "items": { "type": "string", "pattern": "^--?[A-Za-z][A-Za-z0-9-]*$", "maxLength": 64 } }, - "pattern": { "description": "Rust-regex-compatible, first capture is a dotted numeric version. Validity, capture count and runtime bounds require semantic checks.", "type": "string", "minLength": 1, "maxLength": 512 } - } - }, - "executableLayout": { - "type": "object", "additionalProperties": false, "required": ["path"], - "properties": { "path": { "$ref": "#/$defs/relativePath" }, "platforms": { "$ref": "#/$defs/platforms" } } - }, - "homeLayout": { - "type": "object", "additionalProperties": false, "required": ["match", "home"], - "properties": { - "match": { "allOf": [ { "$ref": "#/$defs/boundedGlob" }, { "pattern": "^[^/]+$" } ] }, - "home": { "oneOf": [ { "const": "." }, { "$ref": "#/$defs/relativePath" } ] }, - "platforms": { "$ref": "#/$defs/platforms" } - } - }, - "discovery": { - "type": "object", "additionalProperties": false, "required": ["root", "pattern"], - "properties": { - "root": { - "oneOf": [ - { "type": "object", "additionalProperties": false, "required": ["kind"], "properties": { "kind": { "const": "home" } } }, - { "type": "object", "additionalProperties": false, "required": ["kind", "variable"], "properties": { "kind": { "const": "environment" }, "variable": { "$ref": "#/$defs/safeEnvironmentName" } } }, - { "type": "object", "additionalProperties": false, "required": ["kind", "path"], "properties": { "kind": { "const": "absolute" }, "path": { "type": "string", "minLength": 2, "maxLength": 256, "pattern": "^(?:/|[A-Za-z]:[\\\\/])[^\\u0000-\\u001f\\u007f]*$" } } } - ] - }, - "pattern": { "$ref": "#/$defs/boundedGlob" }, - "platforms": { "$ref": "#/$defs/platforms" } - } - }, - "wrapper": { - "type": "object", "additionalProperties": false, "required": ["path"], - "properties": { - "path": { "$ref": "#/$defs/relativePath" }, - "requires": { "type": "array", "maxItems": 8, "uniqueItems": true, "items": { "$ref": "#/$defs/relativePath" } }, - "platforms": { "$ref": "#/$defs/platforms" } - } - }, - "candidateMetadata": { - "type": "object", - "properties": { - "id": { "$ref": "#/$defs/identifier" }, - "priority": { "type": "integer", "minimum": -2147483648, "maximum": 2147483647 }, - "insertBefore": { "$ref": "#/$defs/identifier" }, - "insertAfter": { "$ref": "#/$defs/identifier" }, - "platforms": { "$ref": "#/$defs/platforms" } - }, - "not": { "anyOf": [ { "required": ["priority", "insertBefore"] }, { "required": ["priority", "insertAfter"] }, { "required": ["insertBefore", "insertAfter"] } ] } - }, - "candidate": { - "$ref": "#/$defs/candidateMetadata", "unevaluatedProperties": false, - "oneOf": [ - { - "required": ["kind", "settingID", "layer"], - "properties": { "kind": { "const": "setting" }, "settingID": { "const": "executable" }, "layer": { "enum": ["project", "global"] } } - }, - { - "required": ["kind", "scope"], - "properties": { "kind": { "const": "toolchain" }, "scope": { "enum": ["project", "global", "installed"] } } - }, - { - "required": ["kind", "path"], - "properties": { - "kind": { "const": "projectFile" }, - "path": { "$ref": "#/$defs/relativePath" }, - "requires": { "type": "array", "maxItems": 8, "items": { "$ref": "#/$defs/relativePath" } }, - "projectKind": { "enum": ["wrapper", "environment"], "default": "wrapper" }, - "home": { "oneOf": [ { "const": "." }, { "$ref": "#/$defs/relativePath" } ] } - }, - "allOf": [ - { - "if": { "required": ["projectKind"], "properties": { "projectKind": { "const": "environment" } } }, - "then": { "required": ["home"] }, - "else": { "not": { "required": ["home"] } } - } - ] - }, - { - "required": ["kind", "variable", "path"], - "properties": { "kind": { "const": "environment" }, "variable": { "$ref": "#/$defs/safeEnvironmentName" }, "path": { "$ref": "#/$defs/relativePath" } } - }, - { - "required": ["kind", "command"], - "properties": { "kind": { "const": "command" }, "command": { "$ref": "#/$defs/command" } } - }, - { - "required": ["kind", "runtime"], - "properties": { "kind": { "const": "runtime" }, "runtime": { "$ref": "#/$defs/toolID" } } - } - ] - }, - "launchTokens": { - "type": "array", "minItems": 1, "maxItems": 32, - "prefixItems": [ { "const": "${executable}" } ], - "items": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^(?:\\$\\{(?:home|executable|entry|runtimeArguments|arguments)\\}|[^$\\u0000-\\u001f\\u007f]+)$" } - }, - "toolDefinition": { - "type": "object", "additionalProperties": false, - "required": ["id", "displayName", "executableLayouts", "executables", "version", "commands", "launch"], - "properties": { - "id": { "$ref": "#/$defs/toolID" }, - "displayName": { "$ref": "#/$defs/label" }, - "icon": { "$ref": "../../plugin-manifest.schema.json#/$defs/uiIcon" }, - "executableLayouts": { "type": "array", "minItems": 1, "maxItems": 8, "items": { "$ref": "#/$defs/executableLayout" } }, - "executables": { "type": "array", "minItems": 1, "maxItems": 32, "items": { "$ref": "#/$defs/candidate" } }, - "version": { "$ref": "#/$defs/version" }, - "homeLayouts": { "type": "array", "maxItems": 8, "items": { "$ref": "#/$defs/homeLayout" } }, - "discovery": { "type": "array", "maxItems": 16, "items": { "$ref": "#/$defs/discovery" } }, - "homeVariable": { "$ref": "#/$defs/safeEnvironmentName" }, - "commands": { "type": "array", "minItems": 1, "maxItems": 8, "uniqueItems": true, "items": { "$ref": "#/$defs/command" } }, - "wrappers": { "type": "array", "maxItems": 8, "items": { "$ref": "#/$defs/wrapper" } }, - "exports": { - "type": "array", "maxItems": 16, - "items": { - "type": "object", "additionalProperties": false, "required": ["name", "value"], - "properties": { "name": { "$ref": "#/$defs/safeEnvironmentName" }, "value": { "enum": ["${home}", "${executable}"] } } - } - }, - "pathPrepend": { - "type": "array", "maxItems": 8, "uniqueItems": true, - "items": { "type": "string", "maxLength": 256, "pattern": "^\\$\\{home\\}(?:/(?!\\.{1,2}(?:/|$))[^/\\\\:$*?\\u0000-\\u001f\\u007f]+)*$" } - }, - "launch": { "$ref": "#/$defs/launchTokens" } - } - }, - "provider": { - "type": "object", "additionalProperties": false, "required": ["id", "displayName", "tools"], - "properties": { - "id": { "$ref": "#/$defs/identifier" }, - "displayName": { "$ref": "#/$defs/label" }, - "tools": { "type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": { "$ref": "#/$defs/toolID" } }, - "watchFiles": { - "type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, - "items": { - "type": "object", "additionalProperties": false, "required": ["pattern", "reader"], - "properties": { "pattern": { "$ref": "#/$defs/boundedGlob" }, "reader": { "enum": ["native", "compatible"] } } - } - } - } - }, - "backend": { - "type": "object", "additionalProperties": false, "required": ["launch"], - "properties": { - "launch": { - "oneOf": [ - { - "type": "object", "additionalProperties": false, "required": ["kind", "path"], - "properties": { "kind": { "const": "packaged" }, "path": { "$ref": "#/$defs/relativePath" } } - }, - { - "type": "object", "additionalProperties": false, "required": ["kind", "tool", "packagedRuntime", "entry"], - "properties": { - "kind": { "const": "runtime" }, "tool": { "$ref": "#/$defs/toolID" }, - "packagedRuntime": { "$ref": "#/$defs/relativePath" }, "entry": { "$ref": "#/$defs/relativePath" }, - "runtimeArguments": { "type": "array", "maxItems": 16, "items": { "type": "string", "maxLength": 256, "pattern": "^[^$\\u0000-\\u001f\\u007f]*$" } } - } - } - ] - }, - "arguments": { "type": "array", "maxItems": 16, "items": { "type": "string", "maxLength": 256, "pattern": "^[^$\\u0000-\\u001f\\u007f]*$" } } - } - }, - "consumer": { - "type": "object", "additionalProperties": false, "required": ["id", "requiresTools", "onToolchainChange"], - "properties": { - "id": { "$ref": "#/$defs/identifier" }, - "requiresTools": { - "type": "array", "minItems": 1, "maxItems": 16, - "items": { "type": "object", "additionalProperties": false, "required": ["tool"], "properties": { "tool": { "$ref": "#/$defs/toolID" }, "minimumVersion": { "type": "string", "pattern": "^[0-9]+(?:\\.[0-9]+){0,7}$", "maxLength": 64 } } } - }, - "onToolchainChange": { "enum": ["restart", "resync", "nextRun"] }, - "launch": { - "type": "object", "additionalProperties": false, "required": ["kind", "tool", "entry"], - "properties": { - "kind": { "const": "runtime" }, "tool": { "$ref": "#/$defs/toolID" }, "entry": { "$ref": "#/$defs/relativePath" }, - "runtimeArguments": { "type": "array", "maxItems": 16, "items": { "type": "string", "maxLength": 256 } } - } - }, - "runtimeDependencies": { - "type": "array", "maxItems": 16, - "items": { "type": "object", "additionalProperties": false, "required": ["tool"], "properties": { "tool": { "$ref": "#/$defs/toolID" } } } - } - } - } - } -} diff --git a/contract/v1/toolchains/proposal/manifest.md b/contract/v1/toolchains/proposal/manifest.md deleted file mode 100644 index 2f45a1c..0000000 --- a/contract/v1/toolchains/proposal/manifest.md +++ /dev/null @@ -1,318 +0,0 @@ -# Toolchain manifest contributions (proposal) - -**Status: full proposal, not a claim of native host support.** The implemented -Core subset is specified by [the active contract](../README.md). This -document follows the -[final scope of PR #1581](https://github.com/1lck/Lithe-IDEA/pull/1581#issuecomment-6541366112). -It is a host contract draft, not a plugin author tutorial. The current manifest -schema now admits only that subset; generic consumer/runtime migration remains -unsupported. Do not install these projection examples as `plugin.json`. - -[`manifest-contributions.schema.json`](manifest-contributions.schema.json) is a -standalone **review projection**, not a replacement manifest schema and not an -`allOf` extension of the current closed schema. Its `proposalVersion`, `pluginID` -and `consumers` envelope fields organize fixtures; they are not new production -manifest fields. `toolDefinitions`, `toolchainProviders`, and the consumer fields -below would be integrated into the existing manifest only when Core and the -corresponding host adapters implement and advertise the capabilities. The -production `schemaVersion: 2` and existing API levels are not changed by this -proposal. A future implementation must not silently accept these fields under an -older capability. - -## Three contributors, one tool registry - -| Contributor | Owns | Does not own | -| --- | --- | --- | -| Tool definition | Identity, executable layout, version probe, bounded discovery, candidate order, exported environment, argv launch template | Version-manager aliases, private configuration formats, provider priority rules | -| Toolchain provider | Installed tools and project/global requirements, computed by its own backend or upstream manager | Tool definitions, arbitrary command replacement, host UI or resolver code | -| Consumer | Explicit tool references and minimum versions, directory, response to a changed selection | Discovery paths, alternate secret selection chains | - -There are **no host-owned tool definitions, built-in definition data files, -short-name aliases, or tool-name switches**. Java support defines `java`; a Maven -plugin defines `maven`. Node, Python and other official plugins migrate only when -they supply their own definitions. This draft does not remove existing behavior -or authorize new hardcoded fallbacks during that staged migration. - -The enabled registry is populated from installed, enabled plugins. A namespaced -definition ID is `:`; a plugin cannot define another -plugin's namespace. Short IDs such as `java` are reserved for packages whose -official identity the **host independently verified during installation**. A -manifest's vendor or signature-policy text is not evidence of that identity. -Community providers may answer short IDs; defining and answering are different -authorities. The first registered owner of an ID remains the owner; a later -duplicate is rejected, never used as an override. Stable enumeration order makes -diagnostics reproducible, not a priority policy for conflicting runtime facts. - -## Proposed capability admission - -The names and versions here are review assignments, not host advertisements: - -| Capability | Required exactly when | -| --- | --- | -| `toolDefinitions: 1` | The plugin contributes definitions | -| `toolchainProviders: 1` | The plugin contributes providers | -| `executableResolution: 2` | A definition uses the new shared candidate projection, including `toolchain` or `setting.layer` | -| `toolchainConsumers: 1` | A consumer uses generic tool references and change policy | -| Existing `backend: 2`, `serverProcess: 1` | A provider uses the plugin's backend | - -Each actual contribution still declares its existing language/build/run/debug -capabilities and permissions. The review envelope lists only capabilities relevant -to this proposal. Missing **or unused** capabilities are rejected, just as the -current manifest contract requires. There is no broad new API-level increment to -pretend that an unimplemented launch adapter supports the draft. - -## Tool definitions - -The [Java-support-style projection](../../fixtures/toolchains/proposal/java-tool-definition.json) supplies -the tool name and every executable policy. Its short ID is valid only in an -independently official installation context; the fixture itself is not signed. - -| Field | Draft meaning and limits | -| --- | --- | -| `id`, `displayName`, `icon` | Tool ID, one-line label of at most 64 characters, and existing shared `uiIcon` format; normal icon package checks still apply | -| `executableLayouts` | 1-8 exact tool-home-relative executable paths, optionally filtered by platform, in fallback order; native executable checks apply | -| `executables` | 1-32 **resolver candidates**, not executable filenames; see below | -| `version` | 1-8 flag-shaped literal probe arguments and a Rust-regex-compatible expression of at most 512 bytes; first capture yields the normalized version | -| `homeLayouts` | At most 8 ordered, single-component name matches mapping a selected installation directory to `.` or a relative home suffix; for example `*.jdk` maps to `Contents/Home` | -| `discovery` | At most 16 bounded scans with root `home`, named `environment`, or an absolute system directory; the host executes data, not a tool-specific scanner. This primitive set is frozen; shapes it cannot express use a provider | -| `homeVariable`, `commands` | Metadata identifying the login-shell input and 1-8 bare commands belonging to this tool | -| `wrappers` | At most 8 exact project-relative wrapper paths, required marker files, and platform filters | -| `exports` | At most 16 unique environment names, each assigned exactly `${home}` or `${executable}` | -| `pathPrepend` | At most 8 paths under `${home}`; no traversal, shell interpolation, or replacement of PATH | -| `launch` | At most 32 argv tokens; the first is exactly `${executable}` | - -`homeLayouts` applies only when normalizing a manual selection or a system -discovery match. A provider's `installations[].home` is already the final tool -home; applying a layout again could append the same suffix twice. - -`executableLayouts` and `executables` deliberately have different roles. The first -locates the executable **within an already chosen installation**. The second -selects which source may supply that installation or executable. A candidate does -not gain permission to launch a same-named command for another tool. - -`homeVariable`, `commands`, and `wrappers` are checked metadata for the same -candidate plan, **not alternate discovery or launch paths**: every `environment` -candidate must use the declared home variable and one declared executable layout; -every `command` must belong to `commands`; every wrapper `projectFile` must match -one wrapper, including its `requires` markers and platform applicability. A -`projectFile` with `projectKind: "environment"` instead declares its own exact -`path`, `requires` markers and mandatory project-relative `home`; its path must -equal that home plus an applicable executable layout. It is not a wrapper or -an external provider installation. Conversely, -declared wrappers and shell inputs must have their corresponding candidate(s). -Any mismatch rejects the definition, so the UI and resolver cannot drift. Only -the candidate plan is executed. An implementation may normalize these references -to one internal representation; it must not maintain two independent pipelines. - -### Version probing and argv safety - -The first version capture must match `[0-9]+(?:\.[0-9]+){0,7}`, at most 64 bytes. -Core compares unsigned numeric components, padding absent trailing components -with zeros. The Java fixture's optional `1.` prefix is declared by that plugin: -`1.8.0_422` produces `8.0`, while `21.0.4` produces `21.0.4`. The host has no Java -8 branch. Provider display text is provenance; auto-selection uses the validated -probe capture, not a parsed alias such as `temurin-21`. - -The host caps probe stdout and stderr at 64 KiB each, applies a 5-second monotonic -deadline, and cancels the child tree on timeout or cancellation. A failed probe -does not provide a sortable version. It cannot satisfy a consumer's minimum -version by assumption. Equal normalized versions with distinct homes are shown -as an explicit ambiguity requiring a choice; no plugin ID or directory name wins. - -Arguments match `-x`/`--long-flag` shape only; no positional commands, `=value`, -scripts, or environment templates. This is a syntax restriction, **not proof that -an executable is side-effect-free**. Installation review must list each executable, -probe arguments, scan roots, export names, and provider backend the package can -run. A future tool whose upstream version command cannot satisfy this restriction -needs a reviewed generic extension; do not add a tool-name exception. - -Launch is an argv vector, never a shell command. `${home}`, `${executable}` and -`${entry}` are whole scalar tokens; `${runtimeArguments}` and `${arguments}` are -whole-token list expansions, at most once each. No inline interpolation, unknown -variable, shell evaluation, shell launcher, or empty executable is allowed. -`${entry}` is independently validated inside the consumer package. Empty argument -lists expand to zero tokens. Arguments and paths with spaces remain single argv -elements. On Windows the existing platform argv-quoting adapter owns native -serialization; the plugin cannot supply shell quoting. - -Exports reject `PATH`, `DYLD_*`, and `LD_*` names. Existing host process-environment -filtering still applies; the permission does not authorize injecting credentials, -loader variables or arbitrary environment expressions. `pathPrepend` is checked -after canonicalization: symlinks may not escape the selected home. Exports from a -rejected candidate never leak into the next candidate's environment. - -## One candidate resolver, four product layers - -The draft extends the -[existing candidate model](../../README.md#shared-executable-candidates-executableresolution-12) -rather than building a second runtime resolver: - -- `setting` gains mandatory `layer: "project" | "global"` in this projection. - `settingID: "executable"` names a generic framework slot, not a plugin private - path-setting ID. Project values come from nearest-ancestor `pins`; global values - are Lithe-owned application preferences. An invalid nonempty fixed/default - value is `terminalFailure`, never an automatic fallback. -- `toolchain` gains `scope: "project" | "global" | "installed"`. The tool is the - containing definition; a candidate cannot inject facts for a different ID. -- Existing `projectFile`, `environment`, `command`, and `runtime` source kinds - retain their source meanings; `runtime` references an explicit tool ID and - dependency cycles are rejected. A `projectFile` only runs through the existing - consumer trust/explicit-user-action boundary, never passive discovery/probing. -- Under the new capability only, `projectFile.projectKind` distinguishes - `wrapper` (default, executable-only) from `environment` (explicit `home`). A - wrapper produces `home: null`; it never invents a tool home from the workspace - root. The environment's home and program are relative to the candidate's - source-directory anchor, which also determines project specificity. The - existing `environment` kind still means an external login-shell variable, - not a project environment. Old executable-resolution v1 is unchanged. -- IDs, signed priority, insertion anchors, deterministic topological ordering, - platform filtering, and validation of the whole set remain the current model. - Anchors cannot cross contributions or plugins. Consumer adjustments apply only - to their own candidate set, not to another tool's definition or global registry. - -The normalized candidate list must preserve these four barriers: - -1. Fixed: nearest ancestor pin in the chosen directory, invalid means terminal. -2. Project: nearest applicable directory, then wrapper > environment > declaration; - for the same source file native reader > compatible reader. Equal claims are - an explicit unresolved conflict, not an ID-based tie-break. Uninstalled requests - return `skip` **with structured reason**, and remain visible as unsatisfied. -3. Global: Lithe default > unambiguous provider global > login shell. Conflicting - provider globals require a user choice; until then, skip them with a conflict - reason and use login-shell candidates. No hidden provider-ID precedence. -4. Automatic: eligible installations from both routes, provider answers and the - generic system scanner driven only by `discovery`; one canonical home counts - once. Select the highest valid normalized version satisfying the consumer - minimum, excluding manually added pin-only homes. - -Array order is the default source order inside these barriers. Ordering metadata -must not move candidates across a barrier or reverse project specificity/reader -precedence; such a definition is rejected. Tool definitions may omit unavailable -sources, but cannot silently redefine the four-layer policy. Absence is `skip`, -not a fabricated value. See the adjacent protocol/selection proposal for exact -query results and reason shapes; no current `skip` consumer changes in this stage. - -## Providers, file watching, and bootstrap - -The [mise projection](../../fixtures/toolchains/proposal/mise-provider.json) declares `id`, `displayName`, -`tools`, and `watchFiles`. `watchFiles` is optional: without it a provider reports -installations only and any returned statement is rejected. That shape lets the -plugin defining a tool answer for its own installations when declarative -`discovery` cannot express them. Provider IDs are local to the plugin/session; registry -identity is the pair `(verified pluginID, provider id)`. There are at most eight -providers, each naming 1-32 tool IDs. The query set is the intersection of requested -and declared IDs; this may include dormant IDs solely for requirement diagnostics. -Only its subset with enabled definitions can supply executable candidates. -Dormant unknown IDs do not inject commands; the host may preserve the requirements -as diagnostics so it can offer enabling the installed definition plugin. A -consumer with a missing required definition cannot launch. - -`toolchains.provide` means the backend may supply candidates which influence the -program Lithe launches, **not merely read directories**. Its installation review -must say so explicitly. It does not authorize installation/download, network, -trusting workspace code, reading secrets, writing native configuration, or -replacing arbitrary commands. Normal backend permission and sandbox restrictions -continue to apply. - -`watchFiles` entries are `{pattern, reader: "native" | "compatible"}`. The host -validates each project statement's source and reader against the declaration; -returned facts cannot promote a compatible reader to native. Native reader -ownership conflicts are explicit and not resolved by plugin registration order. -Patterns are relative, at most eight path components and 256 bytes. Only `*` -inside a single component is supported; no `**`, `?`, classes, braces, traversal, -absolute paths or following a symlink outside the watched root. The host applies -each pattern directly at the selected directory and at most 64 ancestor -directories, including enclosing configuration above the opened workspace; no -recursive `**` scan is implied. Providers also return potential external/global -inputs, including missing paths, in `watch`. Watching an enclosing source does -not expand the workspace identity or editable pin scope. Expansion across the -whole query round is bounded to 256 watch roots and 4,096 enumerated entries, -not just successful matches, as well as the existing workspace scan budget. -Limit hits are reported, never treated as a complete snapshot. The host observes -the parent of missing matches so creation and deletion also invalidate the snapshot. - -Any declared watched file or returned absolute input change cancels the generation -and re-queries **all enabled providers**; no mixed-generation incremental result -is published. Remote workspaces query their own authority or are explicitly -unsupported, never the local machine's managers. Providers own private version-file -formats, `lts`, `latest`, `system`, configuration hierarchy and manager semantics. -The host neither parses those files nor independently guesses their precedence. - -The provider uses the plugin's existing `backend`; no per-tool process framework -and no LSP language feature is required. It must negotiate the typed toolchain -capability over the existing base JSON-RPC transport before receiving queries. -`toolchainProviders: 1` proposes an additional **host-query startup trigger** for -that backend. Existing `backend: 2` alone does not start the fixture today: its -current view/command/`onWorkspaceOpen` admission rules remain unchanged until this -new capability is implemented. The future host starts at most one backend for -the plugin when it needs a query, not one process per tool or directory; after -startup it negotiates support before dispatch. A query is not an implicit -`onWorkspaceOpen` activation declaration. -The backend is either `launch.kind: "packaged"` (a signed/package-checked native -executable), or `launch.kind: "runtime"` with mandatory `packagedRuntime`. The -latter starts exactly that packaged executable and uses the referenced active -tool definition's argv launch template; it never calls the resolver for its own -runtime. This also holds when a plugin provides installations of a tool it -defines: the backend never runs on the tool it is discovering. The launch definition must be registered independently of provider -queries and must be available before backend start. Missing definitions or -dependency cycles fail admission/start explicitly. A packaged runtime is not a -host fallback if a definition is disabled. Native packaged executables are the -recommended bootstrap, and are what the mise fixture demonstrates. - -Provider backends stay within the current restart-required activation policy. -This proposal does not relax live activation. Disabling/uninstalling cancels -queries, invalidates generations, stops the owned backend, and removes active -definitions, dependent consumer rows, exports, and plugin settings together. -Local pins remain stored for re-enable; dormant pins are not allowed to re-create -an executable definition or export. - -## Consumer field projections - -`consumers[]` exists only in this review envelope. Its fields project additions -onto existing language-server, build-provider, execution-provider, debug-adapter -or run-configuration contributions: - -- `launch.kind: "runtime"` accepts any registered `tool` ID; no `java | node` - enum or inferred filename mapping remains on the migrated path. -- `runtimeDependencies[].tool` likewise accepts any registered ID, with existing - uniqueness/no-duplicate-launch-runtime checks retained. -- `requiresTools: [{tool, minimumVersion?}]` explicitly lists every tool a - consumer uses. `launch.tool` and dependencies must be included, each once. -- `onToolchainChange` is `restart`, `resync`, or `nextRun`; an absent corresponding - lifecycle adapter makes that contribution unsupported, not silently ignored. - -Definitions must be known for a required consumer to launch. A syntactically valid -ID alone grants no authority. If its installed defining plugin is disabled, show -the requirement, known owner, and plugin-page enable action; do not launch a stale -pin. Optional provider facts for missing definitions remain diagnostic data only. - -The environment/source page skeleton renders only enabled registry data, scoped -to each directory/module. Tool/plugin-specific settings are contributed on that -plugin's settings page. Manually added homes live in platform application storage, -can only be pinned, never enter auto-selection, and are validated through the -same declared layouts/probe before a confirmed pin is written. - -## Validation boundaries and fixtures - -The standalone schema proves shape and bounded syntax only. Registry semantics -also check capability exactness, ownership, references, candidate graph/layers, -regular-expression validity, version captures, metadata consistency and template -usage. The native host separately proves verified signature provenance, package -containment, canonical paths, executable status, workspace exclusion, cancellation -and limits. None of those checks can be replaced by JSON Schema. - -[`manifest-rejections.json`](../../fixtures/toolchains/proposal/manifest-rejections.json) separates these -phases. Each case applies RFC 6902 `patch` to its `base`; `context` is **host test -input**, never a plugin field. `phase: "schema"` must fail schema validation; -`registry` and `hostTrust` cases intentionally remain valid JSON shape and describe -the later semantic rejection. Codes reuse existing `plugin.manifest.invalid`, -`plugin.permission.undeclared`, and `plugin.template.invalid` categories; `reason` -is structured diagnostic detail, not a second numeric JSON-RPC error mapping. -These are proposed acceptance/rejection vectors, not tests of current Core. - -The implementation stage must add executable Core and host tests before declaring -support, then run both SDK end-to-end suites for actual backend protocol changes. -After merging contract changes, synchronize the byte-for-byte mirror using -`node scripts/contract.mjs sync --revision ` in -[lithe-plugin-docs](https://github.com/Lithe-IDEA/lithe-plugin-docs); do not hand-edit -the mirror or place SDK parsing helpers in the Lithe host. diff --git a/contract/v1/toolchains/proposal/protocol.md b/contract/v1/toolchains/proposal/protocol.md deleted file mode 100644 index 532cc44..0000000 --- a/contract/v1/toolchains/proposal/protocol.md +++ /dev/null @@ -1,371 +0,0 @@ -# Proposed toolchain query, resolution and local pins - -**Status: broader proposal; the macOS settings pages query providers read-only, and SDK support is not implemented.** -The Core-only validation/resolution subset is specified by -[the active contract](../README.md). These files remain review vectors, -not an advertised native capability or an SDK guide. The existing local-toolchain -writer/schema is unchanged. The design follows the -[latest #1581 review](https://github.com/1lck/Lithe-IDEA/pull/1581#issuecomment-6541366112). - -## Ownership and transport - -A tool definition says how to recognize, probe and use a tool. A provider -answers which installations and requirements exist. A consumer explicitly -references a tool ID. The host supplies query orchestration and one resolver; -it never parses a manager's version files, aliases or requested-version syntax. -There are no built-in tool definitions, short-name aliases or tool-name -branches. The generic system scanner is a host provider whose entire scan plan -comes from enabled definitions' `discovery` contributions. Plugin providers -cover installation shapes those frozen scan primitives cannot express; both -routes feed one automatic candidate set, and one canonical home counts once. - -The logical **and wire** method is `toolchains/query`. This independent typed -extension reuses the existing LSP base JSON-RPC transport and ServerProcess -lifecycle, not the language-intelligence domain. A toolchain backend need not -declare a language, language server, Build provider or private callable -function. Initialization offers `experimental.litheToolchains: {versions: [1], -providers: ["mise"]}`; the backend selects `{version: 1, providers: ["mise"]}`. -The host intersects the returned local provider IDs with the verified manifest. -An unoffered version, duplicate/undeclared provider ID, or malformed answer fails -negotiation rather than enabling a partial registry; no query is sent before -negotiation succeeds. -This draft does not upgrade or relax any existing `litheFunctions` version. - -An installed, enabled provider requires `toolchains.provide` and explicit -installation review of the programs it may invoke. This permission can affect -which external program the host launches; it is not described as a harmless -filesystem read. Plugin identity, signature/trust, package digest and enabled -state come from the host registry, never from a reply. The provider's stable -identity is `pluginID:providerID`; the unqualified `providerID` on the wire is -bound to that already authenticated session. - -## Query and reply - -[`query.schema.json`](query.schema.json) defines JSON-RPC envelopes. A request -contains `operationID`, `generation`, `providerID`, `authority`, `context`, -`workspace`, and `tools`. The host round generation also applies to global-only -queries, which have no workspace context. Tools are unique and sorted by ID. `workspace: null` asks only about -global state. Otherwise `workspace.root` is the absolute native root in the -query's authority, `rootID` is the host's opaque identity, and `directory` is -the directory being resolved relative to that root (`.` for the root). A -non-null workspace requires the existing `{contextID, generation}` host context -reference. These native paths are transient machine-local bindings, not portable -project identities. The opened workspace, not its containing repository, sets -the default scope. - -The host queries the intersection of requested tool IDs and the provider's -declared `tools`. IDs with no enabled definition may still be queried for -display-only requirement diagnostics. They cannot create installations, exports, -launches or tool rows. IDs outside that intersection are discarded entirely. -A failed or empty provider reply is not proof that there is no requirement. - -A successful result echoes `operationID`, `generation` and `providerID`, then returns: - -| Field | Meaning | -| --- | --- | -| `installations` | `{tool, version, home}` for external installations; `home` is absolute, `version` is provider display data, not a manager selector interpreted by Core. | -| `statements` | `{tool, scope, source, request, installation, kind, reader}` requirements; `request` is opaque, `installation` is a listed home or `null` for unmet. | -| `watch` | Absolute native input paths to fingerprint, including absent inputs and installation roots. This does not authorize reading contents, executing or writing these paths. | - -`scope` is `project` or `global`; `source` is the absolute originating file. -`kind` is `environment` or `declaration`. Wrappers are separate `projectFile` -candidates supplied by the tool definition, never provider installations. -`reader` is `native` or `compatible`, matching that file's declared -`watchFiles[].reader`; a backend cannot claim native ownership arbitrarily. -If more than one enabled provider declares native ownership of the same source, -the host reports an unresolved conflict rather than sorting plugin IDs. - -For a project statement, match the declared watch pattern relative to each -candidate source-directory anchor, with at most 64 ancestors of the requested -native directory considered. The matching anchor determines project specificity, -not the source file's physical parent: `.mise/config.toml` at `/project` belongs -to `/project`, not `/project/.mise`. If more than one pattern/anchor matches the -same canonical source, use the nearest matching anchor and then native-reader -precedence; indistinguishable conflicting claims remain explicit conflicts. -An upstream may identify an enclosing project file above the opened workspace; -its absolute source is evidence only and never expands the workspace identity -or the editable pins scope. Global sources need not be ancestors. A global-only -query cannot return project statements. `installation: null` means the manager -knows the request but cannot supply a usable installed choice. The host does not -translate `lts`, `system`, `ref:`, `prefix:` or any other request syntax. - -## Validation and limits - -The host passes untrusted replies through Core's deterministic shape and policy -validation before publishing them. Native adapters supply canonical paths, -file identities, existence, type and executable evidence; Core must not assume a -string prefix or JSON Schema proves any of these facts. Every candidate is -revalidated before launch to close stale-cache and symlink replacement windows. -The eventual launch adapter retains its existing atomic/native identity checks. - -- A reply is at most 1 MiB of UTF-8 JSON: at most 512 installations, 256 - statements and 256 watch paths. Each native path is at most 4096 characters; - operation/tool/provider identifiers and request strings have schema bounds. - Oversized replies fail as a whole. Deduplication cannot hide a limit breach. -- Installation homes are absolute directories. Executables come only from the - enabled tool definition's `executableLayouts`; the provider cannot replace - them with arbitrary argv, executables or environment variables. The selected - executable must exist, be a regular executable in the current authority, and - remain inside the canonical home. Providers return the final tool home; - `homeLayouts` applies to manual/system discovery inputs, not a second rewrite - of a provider home. -- Neither a provider installation's canonical home nor its executable may be - inside any root of the current workspace, including a symlink into a root. - An unsafe installation rejects the provider reply with `unsafePath`; it is - not silently promoted to a project wrapper. A provider can report an unmet - requirement instead of returning a project-contained home. -- A non-null statement installation must reference a validated installation of - the same tool in this reply. Unknown or disabled definition IDs lose all - installation authority. Their statements retain only bounded diagnostic facts - (`tool`, `scope`, `source`, `request`), never a usable path. -- Apply byte/count/string bounds and structural validation before filtering. - Then discard installation entries without requested, declared and active - definition IDs **before native filesystem validation**. Only remaining - installations undergo existence/executable/containment checks. Consequently, - an unknown tool's unusable home cannot invalidate an otherwise valid reply; - malformed JSON or a bounds violation still can. -- The host's installed-plugin registry supplies any missing-definition plugin - name and Enable action. Provider data cannot invent plugin names, install - links, enable targets or arbitrary commands. Enabling requires user action. -- Paths cannot contain controls, unnormalized parent traversal or mismatched - authority. Native case and volume rules determine equality and containment; - do not lowercase all paths. Canonical duplicates are collapsed after bounds - checking. Missing watch paths are valid; missing installations are not. -- Lists are normalized deterministically: installations by tool, canonical - home, then version; statements by tool, scope, canonical source, kind, reader, - request, then installation; watch paths by canonical identity. Sorting is not - a tie-breaking rule for conflicting provider requirements. - -Project wrappers and environments are **project code**, not exceptions to the -provider boundary. A tool definition's `projectFile`/wrapper candidate may -resolve a workspace-contained program only through the consumer's existing -explicit user-action/consent flow (there is no persistent workspace trust state -in the current product). Passive import or opening settings does not -execute wrappers or probe project environments. All resulting selections carry -`projectCode: true`, and automatic background consumers must reject them unless -their existing trust boundary explicitly permits execution. Returning -`kind: environment` from a provider cannot waive this rule. - -The new capability's `projectFile.projectKind` is `wrapper` by default or -`environment` with an explicit source-directory-relative `home`. The latter's -`path` must equal that home plus an applicable declared executable layout; no -tool-name or `bin`-parent heuristic supplies a home. Specificity uses the -candidate's source-directory anchor, not the executable's parent directory. -A project-environment candidate can be declared as -`{"kind":"projectFile","projectKind":"environment","home":".venv","path":".venv/bin/python"}`; -the tool's executable layout supplies `bin/python`, not the host. -A wrapper is executable-only and returns `selection.home: null`. It emits no -`${home}` exports or PATH entries; a launch template requiring `${home}` skips -with `homeUnavailable` rather than pretending the repository is an installation. - -Project environments may contain executable symlinks (for example a venv's -interpreter link to an external interpreter). This is a distinct project-code -policy, not a provider exception: the lexical home must remain within the -workspace, the lexical program must match its declared layout, and the adapter -must validate the resolved executable and the complete link chain's identities -at the authorized launch boundary. Preserve the declared lexical launch path; -substituting the canonical target can change environment semantics. A changed -link or missing native identity check rejects the launch. No link grants -background execution authority. Without the consumer's explicit-action/consent -admission, skip with `projectCodeRequiresAction`, never `requestedNotInstalled`. - -## Query lifecycle, cache and watching - -Each query round captures authority, workspace/root/context generation, enabled -definition/provider digests, tool set, environment snapshot and a monotonic -round generation. It queries **every enabled provider** relevant to that round; -each request owns a unique operation ID and JSON-RPC ID. Replies must match -both IDs, round generation and the bound provider. A round publishes one atomic snapshot after -all requests finish, fail, or reach their deadlines. It can preserve a previous -valid snapshot while visibly loading, but cannot label old state as current. -Partial failures stay visible; absence of a reply never means an empty provider. - -The default per-query deadline is 5 seconds including backend startup; -host configuration can choose 100-30000 milliseconds. Cancellation sends -`$/cancelRequest` with the JSON-RPC ID and completes local ownership immediately. -An owned backend unresponsive to cancellation gets a bounded 2-second cleanup -grace before the existing process-tree termination path. A shared backend's -unrelated operations are not stopped to cancel one query. Session shutdown, -disable, uninstall or workspace replacement cancels all owned operations and -listeners; callbacks recheck generation before changing any state. Every -operation completes once. A reply after cancellation, timeout, generation -change or provider removal is discarded, never cached or published. - -The cache key includes the captured identities above. Its input fingerprint -combines returned `watch` paths with the manifest's `watchFiles` declarations. -The host watches parent directories and the bounded set of matching inputs so -creation, deletion, atomic replacement and rename are observable even when a -file did not exist in the previous reply. Missing/existing, type, identity, -size, modification time and canonical target are fingerprinted; native watcher -events invalidate even if a replacement happens to preserve metadata. A lost -watcher event/overflow invalidates the round rather than claiming the cache is -fresh. No file contents or manager syntax are parsed by this cache. - -Manifest patterns have at most eight components and no recursive `**`; matching -is bounded to workspace/context ancestor directories and explicitly returned -absolute watch roots. The host caps each round at 256 watch roots and 4096 -enumerated directory entries, not only successful matches; exceeding this -produces a visible limit failure, not a recursive scan -of the user's home. Multiple file events may coalesce in one scheduling turn. -**Any** matching file change invalidates the entire snapshot, cancels a pending -round, and re-queries **all** providers. There are no per-provider or per-tool -incremental updates. Providers remain responsible for returning every external -global/configuration input they read or would consult if created, including -absent inputs during a global-only query. Returning only existing files is not -sufficient to detect the first creation of a higher-priority configuration. - -Local and remote authorities never share caches, shell environment, native -paths or providers. The first implementation is local only. A remote request -fails with `authorityUnsupported` until that authority has an actual query and -filesystem adapter. It never probes the local placeholder, shell, local pins -or local installations as a fallback. Static definitions may be reused only as -inert data, not as permission to run a local scanner for a remote workspace. - -## One four-layer resolver - -[`resolution.schema.json`](resolution.schema.json) describes normalized output, -not a second resolver implementation. Tool definitions supply their ordered -`executables` candidates; the reusable executable resolver extends its existing -`skip`, `resolved`, `terminalFailure` result. `setting.layer` distinguishes -project pins from application defaults; `toolchain.scope` is `project`, -`global`, or `installed`. Existing per-owner candidate ordering and insertion -rules remain; a consumer cannot reorder another plugin's candidate set or move -a lower layer above a pin. The following layer order is invariant: - -| Layer | Selection and failure behavior | -| --- | --- | -| 1 Fixed | Nearest ancestor pin for this directory and tool; invalid/missing/unexecutable/probe-failing/incompatible nonempty pin is `terminalFailure`, never fallback. | -| 2 Project | Nearest originating directory first; within it wrapper > environment > version declaration. For the same source, native reader > compatible reader. Uninstalled requirements are `skip` with a reason, retained as unmet while lower layers continue. | -| 3 Global | Lithe's explicitly saved default > selected provider global statement > login-shell home variable/commands. Conflicting provider globals require user choice; before choosing, skip provider globals and follow login Shell, not plugin-ID order. | -| 4 Automatic | Highest compatible probed installed release from all providers plus the definition-driven system scanner; excludes manual-only registrations. | - -Project distance is compared **before** project candidate category: a child -version file beats a wrapper in a more distant parent. After native/compatible -precedence, differing equally specific project statements are an explicit -conflict requiring user choice; while unresolved they skip with a conflict -reason. Equal normalized installations are deduplicated, not conflicts. Chosen -provider identities are application-owned preferences scoped by authority, -workspace/directory and tool, never implicit ID ordering. - -Consequently, project sources must expand into one bounded project-layer plan -before its first candidate is attempted. A sequential implementation that -searches every ancestor for the wrapper candidate before considering the next -`toolchain` candidate violates this contract. The existing resolver consumes -the normalized plan; source enumeration is read-only and must not execute or -probe shadowed wrappers/environments to establish their precedence. - -The `skip.reason` object is mandatory. `requestedNotInstalled` includes `tool`, -`provider`, `source`, and opaque `request`; `definitionUnavailable` always includes -`tool` and also includes `source` and `request` when it describes an observed -statement rather than an inactive definition or dormant pin. `providerConflict` is accompanied by a -`conflicts` entry listing qualified identities. Reasons are structured facts -rendered through shared i18n, not English sentences assembled in Core. A -successful fallback retains all applicable unmet requirements and conflicts. -Empty layers remain `empty` trace rows; layers masked by success are `shadowed` -and must not be probed just to populate the settings display. Expanders show -only the effective layer by default; Show All exposes the trace without -changing execution behavior. No selection means `skip`, not invented success. - -The version probe's declared regular expression supplies a normalized numeric -dotted release in its first capture (at most eight unsigned 32-bit components, -64 characters). Compare components numerically with absent trailing components -as zero; there is no Java-8, `v` prefix, vendor, alias or pre-release special -case. No valid comparable capture means not eligible for automatic selection; -an explicit choice still cannot claim to satisfy a consumer's declared minimum -without a successful probe. The provider's `version` is preserved as display -provenance, never trusted instead of the definition's probe. A wrapper may have -`selection.version: null` until the existing user-authorized execution flow can -probe it; consumers requiring a version cannot treat `null` as compatible. -If the newest normalized release has different canonical homes, automatic -resolution reports `ambiguousInstallation`, an `automatic` conflict listing -the distinct canonical homes, and a `chooseInstallation` action; it requires an -explicit choice; sorting homes or plugin IDs must not silently decide which binary runs. -Multiple reports of the same canonical home are one installation. - -Environment exports and PATH prepend entries are generated solely by enabled -definitions using the selected validated home/executable. A failed candidate's -environment is discarded before trying the next. Consumer `requiresTools`, -`launch.tool` and `onToolchainChange` bind directory-scoped usage and explain -whether a switch needs restart, resync or applies next run. Disabling a -definition withdraws its rows, consumers, exports and injected settings page; -it does not delete saved pins. Existing processes do not silently restart. -An executable-only wrapper can export `${executable}`, but cannot export or -prepend paths based on an absent home. Its consumer still owns wrapper-specific -launch arguments; it cannot silently reuse a runtime entry template requiring -installation facts that the wrapper does not provide. - -## Local pins and manual installations - -[`local.schema.json`](local.schema.json) is an isolated proposed extension of -the machine-local `.lithe/toolchains/local.json`, retaining `version: 1` and -legacy `toolchains` while adding `pins`. It does not modify the active v1 -schema or promise that an old host accepts new fields. The implementation phase -must gate writers on the new capability and migrate the corresponding readers -together; it must not write this draft shape for an old reader. - -`pins` maps normalized workspace-relative directory keys to tool-ID maps. -`.` is the opened root; absolute keys, backslashes, empty segments, `.`, `..` -segments and trailing separators are rejected. Dotted directory names are valid. -For `apps/web/src`, check that directory, `apps/web`, `apps`, then `.`, never -`apps/website` by string prefix. Each pin stores `{home, version, provider}`; -provider is a qualified identity or the reserved generic `host:manual` / -`host:system`, not a backend activation instruction. An inherited pin applies -until a nearer explicit value replaces it. Removing an override reveals the -ancestor; it does not delete that ancestor or the whole local file. - -Opening a project or settings only reads. An edit is a draft until explicit -Save/Apply confirms the target directory, tool and machine-local path. The -platform adapter checks the read revision and rejects stale writes, preserves -unrelated legacy entries/pins and permissions, atomically replaces the file, -then reads it again to display the effective choice. No write on focus loss, -navigation, startup, cancellation or unchanged inherited values. Read failures -are errors, not empty defaults. Reject symlinked local targets and any write -escaping the selected workspace's machine-local store. - -The first explicit save adds `/toolchains/local.json` exactly once to -`.lithe/.gitignore`, preserving other content and using conflict protection. -There is no transaction spanning two files: secure the ignore entry before -publishing the new local file; an ignore-write failure aborts the save. If local -file publication fails afterward, the harmless ignore entry may remain and the -draft remains unsaved. Neither target is created merely by opening a project. - -Manual Add validates a user-selected directory via `homeLayouts`, -`executableLayouts`, and the declared bounded version probe. It registers the -installation only in platform application storage, outside app bundles, -installed plugin packages and the project. Such registrations can be explicitly -pinned but do not participate in automatic discovery. A workspace-contained -directory is not converted into a trusted external installation by manual Add; -project wrappers/environments retain their separate project-code boundary. -Pins survive plugin disable/uninstall and reactivate only when that definition -is enabled again. No tool is downloaded, installed, trusted or repaired by query. - -## Stable failure mapping - -These are proposed additions, not assignments in the current runtime. Existing -host-service codes `-32010` through `-32021` keep their meanings. Toolchain -custom failures reserve `-32070` through `-32077` in the server-error range; -each numeric code has exactly one semantic code. JSON-RPC error `data` includes -`code`, `operationID`, and `providerID`; optional details are bounded to 4 KiB -serialized and contain no credentials. Native errors are translated to shared -i18n at the host boundary; plugin diagnostic text remains unmodified but is -not a policy selector. Timeout is not cancellation, and stale replies do not -generate a second completion. - -| JSON-RPC | Semantic code | Meaning | -| --- | --- | --- | -| `-32601` | `plugin.toolchain.methodUnavailable` | Query method/capability not negotiated. | -| `-32602` | `plugin.toolchain.invalidParams` | Malformed request or invalid declared input. | -| `-32800` | `plugin.toolchain.cancelled` | Caller cancelled the owned request. | -| `-32070` | `plugin.toolchain.permissionDenied` | Permission/review/enabled-state prerequisite absent. | -| `-32071` | `plugin.toolchain.authorityUnsupported` | No adapter for this authority. | -| `-32072` | `plugin.toolchain.timeout` | Monotonic local deadline exceeded. | -| `-32073` | `plugin.toolchain.providerUnavailable` | Backend failed to start, disconnected, or failed its upstream query. | -| `-32074` | `plugin.toolchain.invalidReply` | Malformed reply or inconsistent same-tool installation reference. | -| `-32075` | `plugin.toolchain.limitExceeded` | Reply/scan/watch budget exceeded. | -| `-32076` | `plugin.toolchain.unsafePath` | Invalid absolute path, containment or executable safety evidence. | -| `-32077` | `plugin.toolchain.operationExpired` | Context/generation was already invalid when a query was dispatched. | - -Request-local failures (missing installation, incompatible version, conflicting -requirements) are resolver reasons, not JSON-RPC transport errors. The query -fixture includes the numeric mapping; runtime tests, SDK end-to-end tests and -both native host lifecycle tests are required in later implementation phases. diff --git a/contract/v1/toolchains/proposal/query.schema.json b/contract/v1/toolchains/proposal/query.schema.json deleted file mode 100644 index a2c7f60..0000000 --- a/contract/v1/toolchains/proposal/query.schema.json +++ /dev/null @@ -1,621 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/Lithe-IDEA/lithe-plugin-docs/main/contract/v1/toolchains/proposal/query.schema.json", - "title": "Proposed toolchain query protocol v1 (not implemented)", - "description": "Review-only JSON-RPC request, successful response or error response. Semantic validation and authority checks are specified in protocol.md.", - "oneOf": [ - { - "$ref": "#/$defs/request" - }, - { - "$ref": "#/$defs/response" - }, - { - "$ref": "#/$defs/errorResponse" - } - ], - "$defs": { - "identifier": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" - }, - "toolID": { - "$ref": "manifest-contributions.schema.json#/$defs/toolID" - }, - "providerID": { - "$ref": "manifest-contributions.schema.json#/$defs/identifier" - }, - "nativePath": { - "type": "string", - "minLength": 1, - "maxLength": 4096, - "pattern": "^(?:/|[A-Za-z]:[\\\\/]|\\\\\\\\)", - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - }, - "description": "Absolute native path in this query's authority; canonical containment requires native filesystem evidence, not this lexical pattern." - }, - "directory": { - "type": "string", - "maxLength": 2048, - "pattern": "^(?:\\.|(?!\\.\\.?(?:/|$))[^/\\\\:]+(?:/(?!\\.\\.?(?:/|$))[^/\\\\:]+)*)$", - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - }, - "description": "Workspace-relative directory using /; . means the opened root. Each other component must not be . or ..; control characters are forbidden semantically." - }, - "authority": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": [ - "kind" - ], - "properties": { - "kind": { - "const": "local" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "kind", - "id" - ], - "properties": { - "kind": { - "const": "remote" - }, - "id": { - "$ref": "#/$defs/identifier" - } - } - } - ] - }, - "context": { - "type": "object", - "additionalProperties": false, - "required": [ - "contextID", - "generation" - ], - "properties": { - "contextID": { - "$ref": "#/$defs/identifier" - }, - "generation": { - "type": "integer", - "minimum": 1 - } - } - }, - "workspace": { - "type": "object", - "additionalProperties": false, - "required": [ - "rootID", - "root", - "directory" - ], - "properties": { - "rootID": { - "$ref": "#/$defs/identifier" - }, - "root": { - "$ref": "#/$defs/nativePath" - }, - "directory": { - "$ref": "#/$defs/directory" - } - } - }, - "params": { - "type": "object", - "additionalProperties": false, - "required": [ - "operationID", - "generation", - "providerID", - "authority", - "context", - "workspace", - "tools" - ], - "properties": { - "operationID": { - "$ref": "#/$defs/identifier" - }, - "providerID": { - "$ref": "#/$defs/providerID" - }, - "authority": { - "$ref": "#/$defs/authority" - }, - "context": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/context" - } - ] - }, - "workspace": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/workspace" - } - ] - }, - "tools": { - "type": "array", - "minItems": 1, - "maxItems": 64, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/toolID" - } - }, - "generation": { - "type": "integer", - "minimum": 1, - "description": "Host query-round generation, including workspace:null queries; echoed unchanged." - } - }, - "allOf": [ - { - "if": { - "properties": { - "workspace": { - "type": "object" - } - } - }, - "then": { - "properties": { - "context": { - "$ref": "#/$defs/context" - } - } - } - } - ] - }, - "installation": { - "type": "object", - "additionalProperties": false, - "required": [ - "tool", - "version", - "home" - ], - "properties": { - "tool": { - "$ref": "#/$defs/toolID" - }, - "version": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - } - }, - "home": { - "$ref": "#/$defs/nativePath" - } - } - }, - "statement": { - "type": "object", - "additionalProperties": false, - "required": [ - "tool", - "scope", - "source", - "request", - "installation", - "kind", - "reader" - ], - "properties": { - "tool": { - "$ref": "#/$defs/toolID" - }, - "scope": { - "enum": [ - "project", - "global" - ] - }, - "source": { - "$ref": "#/$defs/nativePath" - }, - "request": { - "type": "string", - "minLength": 1, - "maxLength": 512, - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - } - }, - "installation": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/$defs/nativePath" - } - ] - }, - "kind": { - "enum": [ - "environment", - "declaration" - ] - }, - "reader": { - "enum": [ - "native", - "compatible" - ] - } - } - }, - "result": { - "type": "object", - "additionalProperties": false, - "required": [ - "operationID", - "generation", - "providerID", - "installations", - "statements", - "watch" - ], - "properties": { - "operationID": { - "$ref": "#/$defs/identifier" - }, - "providerID": { - "$ref": "#/$defs/providerID" - }, - "installations": { - "type": "array", - "maxItems": 512, - "items": { - "$ref": "#/$defs/installation" - } - }, - "statements": { - "type": "array", - "maxItems": 256, - "items": { - "$ref": "#/$defs/statement" - } - }, - "watch": { - "type": "array", - "maxItems": 256, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/nativePath" - } - }, - "generation": { - "type": "integer", - "minimum": 1, - "description": "Host query-round generation, including workspace:null queries; echoed unchanged." - } - } - }, - "request": { - "type": "object", - "additionalProperties": false, - "required": [ - "jsonrpc", - "id", - "method", - "params" - ], - "properties": { - "jsonrpc": { - "const": "2.0" - }, - "id": { - "oneOf": [ - { "type": "integer", "minimum": 1 }, - { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^\\u0000-\\u001f\\u007f]*$" } - ] - }, - "method": { - "const": "toolchains/query" - }, - "params": { - "$ref": "#/$defs/params" - } - } - }, - "response": { - "type": "object", - "additionalProperties": false, - "required": [ - "jsonrpc", - "id", - "result" - ], - "properties": { - "jsonrpc": { - "const": "2.0" - }, - "id": { - "oneOf": [ - { "type": "integer", "minimum": 1 }, - { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^\\u0000-\\u001f\\u007f]*$" } - ] - }, - "result": { - "$ref": "#/$defs/result" - } - } - }, - "errorResponse": { - "type": "object", - "additionalProperties": false, - "required": [ - "jsonrpc", - "id", - "error" - ], - "properties": { - "jsonrpc": { - "const": "2.0" - }, - "id": { - "oneOf": [ - { "type": "integer", "minimum": 1 }, - { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[^\\u0000-\\u001f\\u007f]*$" } - ] - }, - "error": { - "type": "object", - "additionalProperties": false, - "required": [ - "code", - "message", - "data" - ], - "properties": { - "code": { - "enum": [ - -32601, - -32602, - -32800, - -32070, - -32071, - -32072, - -32073, - -32074, - -32075, - -32076, - -32077 - ] - }, - "message": { - "type": "string", - "minLength": 1, - "maxLength": 1024 - }, - "data": { - "type": "object", - "additionalProperties": false, - "required": [ - "code", - "operationID", - "providerID" - ], - "properties": { - "code": { - "enum": [ - "plugin.toolchain.methodUnavailable", - "plugin.toolchain.invalidParams", - "plugin.toolchain.cancelled", - "plugin.toolchain.permissionDenied", - "plugin.toolchain.authorityUnsupported", - "plugin.toolchain.timeout", - "plugin.toolchain.providerUnavailable", - "plugin.toolchain.invalidReply", - "plugin.toolchain.limitExceeded", - "plugin.toolchain.unsafePath", - "plugin.toolchain.operationExpired" - ] - }, - "operationID": { - "$ref": "#/$defs/identifier" - }, - "providerID": { - "$ref": "#/$defs/providerID" - }, - "details": { - "type": "object", - "maxProperties": 8 - } - } - } - }, - "allOf": [ - { - "oneOf": [ - { - "properties": { - "code": { - "const": -32601 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.methodUnavailable" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32602 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.invalidParams" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32800 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.cancelled" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32070 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.permissionDenied" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32071 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.authorityUnsupported" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32072 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.timeout" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32073 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.providerUnavailable" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32074 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.invalidReply" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32075 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.limitExceeded" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32076 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.unsafePath" - } - } - } - } - }, - { - "properties": { - "code": { - "const": -32077 - }, - "data": { - "properties": { - "code": { - "const": "plugin.toolchain.operationExpired" - } - } - } - } - } - ] - } - ] - } - } - } - } -} diff --git a/contract/v1/toolchains/proposal/resolution.schema.json b/contract/v1/toolchains/proposal/resolution.schema.json deleted file mode 100644 index 2a101c2..0000000 --- a/contract/v1/toolchains/proposal/resolution.schema.json +++ /dev/null @@ -1,362 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/Lithe-IDEA/lithe-plugin-docs/main/contract/v1/toolchains/proposal/resolution.schema.json", - "title": "Proposed shared toolchain resolution result v1 (not implemented)", - "description": "Review-only normalized resolver output. Host-owned snapshots and native filesystem evidence are inputs; providers never produce this object directly.", - "type": "object", - "additionalProperties": false, - "required": [ - "operationID", - "tool", - "directory", - "outcome", - "trace", - "unmet", - "conflicts" - ], - "properties": { - "operationID": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "tool": { - "$ref": "#/$defs/toolID" - }, - "directory": { - "$ref": "#/$defs/directory" - }, - "outcome": { - "$ref": "#/$defs/outcome" - }, - "trace": { - "type": "array", - "maxItems": 256, - "items": { - "$ref": "#/$defs/trace" - } - }, - "unmet": { - "type": "array", - "maxItems": 256, - "items": { - "$ref": "#/$defs/reason" - } - }, - "conflicts": { - "type": "array", - "maxItems": 64, - "items": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": [ - "scope", - "providers", - "action" - ], - "properties": { - "scope": { - "enum": [ - "project", - "global" - ] - }, - "providers": { - "type": "array", - "minItems": 2, - "maxItems": 32, - "uniqueItems": true, - "items": { - "type": "string", - "minLength": 1, - "maxLength": 256 - } - }, - "action": { - "const": "chooseProvider" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "scope", - "providers", - "installations", - "action" - ], - "properties": { - "scope": { - "const": "automatic" - }, - "providers": { - "type": "array", - "minItems": 1, - "maxItems": 32, - "uniqueItems": true, - "items": { - "type": "string", - "minLength": 1, - "maxLength": 256 - } - }, - "installations": { - "type": "array", - "minItems": 2, - "maxItems": 512, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/nativePath" - } - }, - "action": { - "const": "chooseInstallation" - } - } - } - ] - } - } - }, - "$defs": { - "directory": { - "type": "string", - "maxLength": 2048, - "pattern": "^(?:\\.|(?!\\.\\.?(?:/|$))[^/\\\\:]+(?:/(?!\\.\\.?(?:/|$))[^/\\\\:]+)*)$", - "not": { - "pattern": "[\\u0000-\\u001f\\u007f]" - } - }, - "toolID": { - "$ref": "manifest-contributions.schema.json#/$defs/toolID" - }, - "layer": { - "enum": [ - "pinned", - "project", - "global", - "automatic" - ] - }, - "reason": { - "type": "object", - "additionalProperties": false, - "required": [ - "code" - ], - "properties": { - "code": { - "enum": [ - "notConfigured", - "notFound", - "notExecutable", - "probeFailed", - "versionUnsatisfied", - "requestedNotInstalled", - "definitionUnavailable", - "providerConflict", - "unsafePath", - "providerUnavailable", - "ambiguousInstallation", - "homeUnavailable", - "projectCodeRequiresAction" - ] - }, - "tool": { - "$ref": "#/$defs/toolID" - }, - "provider": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "source": { - "type": "string", - "minLength": 1, - "maxLength": 4096 - }, - "request": { - "type": "string", - "minLength": 1, - "maxLength": 512 - } - }, - "description": "Structured facts, not assembled English. The host renders its shared i18n message with these arguments. Required facts per code are specified in protocol.md." - }, - "selection": { - "type": "object", - "additionalProperties": false, - "required": [ - "home", - "executable", - "version", - "provider", - "layer", - "projectCode", - "environment", - "pathPrepend" - ], - "properties": { - "home": { - "oneOf": [ - { "type": "null" }, - { "$ref": "#/$defs/nativePath" } - ] - }, - "executable": { - "$ref": "#/$defs/nativePath" - }, - "version": { - "type": [ - "string", - "null" - ], - "minLength": 1, - "maxLength": 128 - }, - "provider": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "layer": { - "$ref": "#/$defs/layer" - }, - "projectCode": { - "type": "boolean" - }, - "environment": { - "type": "object", - "maxProperties": 32, - "additionalProperties": { - "type": "string", - "maxLength": 4096 - }, - "propertyNames": { - "$ref": "manifest-contributions.schema.json#/$defs/safeEnvironmentName" - } - }, - "pathPrepend": { - "type": "array", - "maxItems": 16, - "items": { - "$ref": "#/$defs/nativePath" - } - } - } - }, - "outcome": { - "oneOf": [ - { - "type": "object", - "additionalProperties": false, - "required": [ - "kind", - "selection" - ], - "properties": { - "kind": { - "const": "resolved" - }, - "selection": { - "$ref": "#/$defs/selection" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "kind", - "reason" - ], - "properties": { - "kind": { - "const": "skip" - }, - "reason": { - "$ref": "#/$defs/reason" - } - } - }, - { - "type": "object", - "additionalProperties": false, - "required": [ - "kind", - "reason" - ], - "properties": { - "kind": { - "const": "terminalFailure" - }, - "reason": { - "$ref": "#/$defs/reason" - } - } - } - ] - }, - "trace": { - "type": "object", - "additionalProperties": false, - "required": [ - "candidateID", - "layer", - "state" - ], - "properties": { - "candidateID": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "layer": { - "$ref": "#/$defs/layer" - }, - "state": { - "enum": [ - "skip", - "resolved", - "terminalFailure", - "shadowed", - "empty" - ] - }, - "reason": { - "$ref": "#/$defs/reason" - }, - "source": { - "type": "string", - "minLength": 1, - "maxLength": 4096 - } - }, - "allOf": [ - { - "if": { - "properties": { - "state": { - "enum": [ - "skip", - "terminalFailure" - ] - } - } - }, - "then": { - "required": [ - "reason" - ] - } - } - ] - }, - "nativePath": { - "$ref": "query.schema.json#/$defs/nativePath" - } - } -} diff --git a/reference/api-catalog.md b/reference/api-catalog.md index 32af3ef..bd73bee 100644 --- a/reference/api-catalog.md +++ b/reference/api-catalog.md @@ -52,7 +52,7 @@ Lithe 的插件公共 API 按 [API 实施计划](https://github.com/1lck/Lithe-I | API-29 | 构建、任务、测试、运行准备 | 部分完成 | [`buildSystems` 1–4](../contract/v1/README.md#api-level-3-build-systems)、[`buildProviders` 1–11](../contract/v1/README.md#build-providers)、`buildModelInputs` 1 | 开发指南还不完整 | [声明式构建](domains/build-systems.md)、[构建提供者](domains/build-providers.md) | | API-30 | 调试适配器 | 既有能力 | [`debugAdapters` 1、2](../contract/v1/README.md#api-level-4-debug-adapters) 与宿主的 DAP 会话 | — | [调试适配器](domains/debug-adapters.md) | | API-31 | VCS 提供者 | 既有能力 | [`vcsProviders` 1](../contract/v1/README.md#api-level-3-vcs-providers) | 不含内置 Git 的公共读取(见 API-42) | [VCS](domains/vcs.md) | -| API-33 | 项目生成器 | 既有能力 | [`projectGenerators` 1](../contract/v1/README.md#project-generators) | — | [项目生成器](domains/project-generators.md) | +| API-33 | 项目生成器 | 既有能力 | [`projectGenerators` 1、2](../contract/v1/README.md#project-generators) | — | [项目生成器](domains/project-generators.md) | | API-37 | 环境信息与语言包 | 部分完成 | `getCapabilities` 返回协议版本、可用方法、限制和后端运行位置(`backendAuthority`) | 语言包、环境和主题变化事件 | [插件后端](domains/backend.md) | | API-40 | 工作区 authority | 部分完成 | 工作区上下文带 `local` 或 `remote`;文件和编辑器方法对远程上下文明确返回 `plugin.host.authorityUnsupported` | 远程执行 | [插件后端](domains/backend.md) | | API-41 | 激活事件与启动预算 | 部分完成 | [`onWorkspaceOpen`](../contract/v1/README.md#permissions-and-activation)、`onCommand`、后台两路并发启动、`initialize` 超时、有界通知队列 | 按工作区内容匹配激活、全局冷启动预算 | [插件后端](domains/backend.md) | @@ -175,7 +175,7 @@ Status meanings: | API-29 | Build, tasks, tests, run preparation | Partial | [`buildSystems` 1–4](../contract/v1/README.md#api-level-3-build-systems), [`buildProviders` 1–11](../contract/v1/README.md#build-providers), `buildModelInputs` 1 | The developer guides are incomplete | [Build systems](domains/build-systems.md), [Build providers](domains/build-providers.md) | | API-30 | Debug adapters | Existing | [`debugAdapters` 1 and 2](../contract/v1/README.md#api-level-4-debug-adapters) and the host's DAP sessions | — | [Debug adapters](domains/debug-adapters.md) | | API-31 | VCS providers | Existing | [`vcsProviders` 1](../contract/v1/README.md#api-level-3-vcs-providers) | Public reads of built-in Git are not included (see API-42) | [VCS](domains/vcs.md) | -| API-33 | Project generators | Existing | [`projectGenerators` 1](../contract/v1/README.md#project-generators) | — | [Project generators](domains/project-generators.md) | +| API-33 | Project generators | Existing | [`projectGenerators` 1, 2](../contract/v1/README.md#project-generators) | — | [Project generators](domains/project-generators.md) | | API-37 | Environment info and l10n | Partial | `getCapabilities` returns the protocol version, usable methods, limits, and where the backend runs (`backendAuthority`) | Language packs, environment and theme change events | [Plugin backend](domains/backend.md) | | API-40 | Workspace authority | Partial | Workspace contexts carry `local` or `remote`; file and editor methods refuse a remote context with `plugin.host.authorityUnsupported` | Remote execution | [Plugin backend](domains/backend.md) | | API-41 | Activation events and startup budget | Partial | [`onWorkspaceOpen`](../contract/v1/README.md#permissions-and-activation), `onCommand`, two concurrent background starts, `initialize` timeout, bounded notification queues | Activation by workspace content, a global cold-start budget | [Plugin backend](domains/backend.md) | diff --git a/reference/capabilities-and-versions.md b/reference/capabilities-and-versions.md index e018dd0..63fc562 100644 --- a/reference/capabilities-and-versions.md +++ b/reference/capabilities-and-versions.md @@ -76,6 +76,7 @@ | `debugAdapters` | 1 | [`debugAdapters[]`](../contract/v1/README.md#api-level-4-debug-adapters);需要级别 4 | ✅ | ✅ | — | [调试适配器](domains/debug-adapters.md) | | `debugAdapters` | 2 | 调试目标的参数和环境变量 | ✅ | ✅ | — | [调试适配器](domains/debug-adapters.md) | | `projectGenerators` | 1 | [`projectGenerators[]`](../contract/v1/README.md#project-generators):新建项目窗口里的条目 | ✅ | ✅ | — | [项目生成器](domains/project-generators.md) | +| `projectGenerators` | 2 | [`tool` 字段](../contract/v1/README.md#tool-fields):从插件声明的工具的已有安装里选一个,可存为新项目的工具固定;不能再用 `runtime` 字段 | ✅ | ✅ | — | [项目生成器](domains/project-generators.md) | | `backend` | 1 | [插件后端](../contract/v1/README.md#plugin-backend):插件自己的长驻进程,从插件页面启动 | ✅ | ✅ | — | [插件后端](domains/backend.md) | | `backend` | 2 | [无页面启动和工作区上下文](../contract/v1/README.md#backend-2-workspace-contexts) | ✅ | ✅ | — | [插件后端](domains/backend.md) | | `packageTrust` | 1 | `vendor.bundledSignatureRequirement`:随宿主发布的发布者签名插件副本按来源信任 | ✅ | ✅ | — | [包信任](domains/package-trust.md) | @@ -109,7 +110,7 @@ | `litheBuild` | 11 | 注册表连接调试 | 同上 | [Connection debugging](../contract/v1/README.md#registry-connection-debugging-buildproviders-11) | | `litheVcs` | 1 | VCS 提供者的能力和请求 | VCS 提供者 | [VCS providers](../contract/v1/README.md#api-level-3-vcs-providers) | | `litheKeymapImport` | 1 | 后端从声明的提供者中选出要启用的子集;`lithe/keymap/import` | 声明了 `keymapImportProviders` 的插件会话 | [Shortcut import](../contract/v1/README.md#shortcut-import-providers-core-foundation) | -| `litheToolchains` | 1 | `toolchains/query`;后端从声明的提供者中选出子集 | 工具链提供者(macOS;Windows 未接入) | [Toolchains protocol](../contract/v1/toolchains/proposal/protocol.md) | +| `litheToolchains` | 1 | `toolchains/query`;后端从声明的提供者中选出子集 | 工具链提供者(macOS;Windows 未接入) | [Provider backend negotiation](../contract/v1/toolchains/README.md#provider-backend-negotiation) | | `litheLogging` | 1 | 结构化诊断通知 `lithe/log` | 每个插件拥有的进程 | [Structured diagnostics](../contract/v1/README.md#structured-plugin-diagnostics-logging-protocol-1) | - `litheBuild` 各版本新增的内容与同号的 `buildProviders` 能力版本一致。 @@ -235,6 +236,7 @@ Each row is one version of one capability. The Core column is what Core can vali | `debugAdapters` | 1 | [`debugAdapters[]`](../contract/v1/README.md#api-level-4-debug-adapters); needs level 4 | ✅ | ✅ | — | [Debug adapters](domains/debug-adapters.md) | | `debugAdapters` | 2 | Debug target arguments and environment | ✅ | ✅ | — | [Debug adapters](domains/debug-adapters.md) | | `projectGenerators` | 1 | [`projectGenerators[]`](../contract/v1/README.md#project-generators): entries of the New Project window | ✅ | ✅ | — | [Project generators](domains/project-generators.md) | +| `projectGenerators` | 2 | [`tool` fields](../contract/v1/README.md#tool-fields): pick an existing installation of a plugin-declared tool, optionally saved as the new project's tool pin; `runtime` fields are no longer allowed | ✅ | ✅ | — | [Project generators](domains/project-generators.md) | | `backend` | 1 | [Plugin backend](../contract/v1/README.md#plugin-backend): the plugin's own long-lived process, started from its pages | ✅ | ✅ | — | [Plugin backend](domains/backend.md) | | `backend` | 2 | [Start without a page, and workspace contexts](../contract/v1/README.md#backend-2-workspace-contexts) | ✅ | ✅ | — | [Plugin backend](domains/backend.md) | | `packageTrust` | 1 | `vendor.bundledSignatureRequirement`: origin-scoped trust for a host-sealed bundled copy of a publisher-signed plugin | ✅ | ✅ | — | [Package trust](domains/package-trust.md) | @@ -268,7 +270,7 @@ These protocols are negotiated when a process starts and never appear in the man | `litheBuild` | 11 | Registry connection debugging | Same | [Connection debugging](../contract/v1/README.md#registry-connection-debugging-buildproviders-11) | | `litheVcs` | 1 | VCS provider capabilities and requests | VCS providers | [VCS providers](../contract/v1/README.md#api-level-3-vcs-providers) | | `litheKeymapImport` | 1 | The backend selects a subset of its declared providers; `lithe/keymap/import` | Plugin sessions with declared `keymapImportProviders` | [Shortcut import](../contract/v1/README.md#shortcut-import-providers-core-foundation) | -| `litheToolchains` | 1 | `toolchains/query`; the backend selects a subset of its declared providers | Toolchain providers (macOS; Windows not connected) | [Toolchains protocol](../contract/v1/toolchains/proposal/protocol.md) | +| `litheToolchains` | 1 | `toolchains/query`; the backend selects a subset of its declared providers | Toolchain providers (macOS; Windows not connected) | [Provider backend negotiation](../contract/v1/toolchains/README.md#provider-backend-negotiation) | | `litheLogging` | 1 | Structured diagnostic notification `lithe/log` | Every plugin-owned process | [Structured diagnostics](../contract/v1/README.md#structured-plugin-diagnostics-logging-protocol-1) | - Each `litheBuild` version adds what the `buildProviders` capability version of the same number adds. diff --git a/reference/domains/project-generators.md b/reference/domains/project-generators.md index 2087724..7b5f8f1 100644 --- a/reference/domains/project-generators.md +++ b/reference/domains/project-generators.md @@ -9,7 +9,7 @@ | 项目 | 内容 | | --- | --- | | 清单字段 | `projectGenerators[]`:`id`、`displayName`、`section`、`order`、`icon`、`fields`、`requirements`、`steps`、`openFiles` | -| 能力版本 | `projectGenerators` 1,级别 3。见[版本对照](../capabilities-and-versions.md#能力版本requires) | +| 能力版本 | `projectGenerators` 1、2,级别 3。见[版本对照](../capabilities-and-versions.md#能力版本requires) | | 协议 | 没有进程协议;生成器只是数据,由 Core 命令计算 | | API 编号 | [API-33](../api-catalog.md#现在能用的-api):项目生成器 | | 权限 | `project.generate` | @@ -21,22 +21,23 @@ | 版本 | 最低级别 | 新增 | Core | macOS | Windows | | --- | --- | --- | --- | --- | --- | | `projectGenerators` 1 | 3 | 两个分区(`project`、`generator`);五种字段(`text`、`enum`、`boolean`、`multiSelect`、`runtime`);条件、模板;三种步骤(`files`、`command`、`archive`);五个 Core 命令 | 支持 | 支持 | 不支持 | +| `projectGenerators` 2 | 3 | `tool` 字段(插件声明的工具定义的已有安装);`${field..executable}`;计划返回 `toolPins`,宿主存为项目的工具固定;表单输入 `tools`、结果 `installations`、问题码 `toolTooOld`;目录条目 `tools`。不能再用 `runtime` 字段;版本 1 不能用 `tool` 字段 | 支持 | 支持 | 不支持 | ### 字段、步骤与命令 | 部分 | 内容 | | --- | --- | | `section` | `project` 放在"新建项目"下(某种语言的普通项目),`generator` 放在"生成器"下(框架或脚手架);按分区、`order`(0–1000,默认 100)、名称排序 | -| 字段类型 | `text`(`default` 模板、`pattern`、`maxLength`、`maximumVersion`)、`enum`(1–16 个选项)、`boolean`、`multiSelect`(1–128 个选项,可分组)、`runtime`(宿主发现的安装,目前是 `java`) | +| 字段类型 | `text`(`default` 模板、`pattern`、`maxLength`、`maximumVersion`)、`enum`(1–16 个选项)、`boolean`、`multiSelect`(1–128 个选项,可分组)、`runtime`(宿主发现的安装,目前是 `java`,只用于版本 1)、`tool`(版本 2:插件声明的工具定义的已有安装,按主目录记录;属性 `tool`、`minimumVersion`、`pinAs`、`optional`;没有 `default`,优先保留用户的选择,否则选最新版本,版本比较忽略末尾的 0) | | 条件 | `when`、`visibleWhen`:`equals`、`notEquals`、`oneOf`;隐藏字段不校验,条件里视为不存在 | -| 清单模板 | `${project.name}`、`${field.}`、`${field..version}`、`${field..major}`,修饰符 `path`、`package`、`lower` | +| 清单模板 | `${project.name}`、`${field.}`、`${field..version}`、`${field..major}`(只用于 `runtime`)、`${field..executable}`(只用于 `tool`:所选安装里的可执行文件路径),修饰符 `path`、`package`、`lower` | | 模板文件 | 用 `{{ … }}`,因为 POM、Gradle 脚本里本来就有 `${…}`;`dot-` 开头的路径段变成点文件(`dot-gitignore` → `.gitignore`) | | `files` 步骤 | 把包内模板目录写进项目,保留可执行位;最多 1024 个文件、单个 1 MiB、合计 8 MiB | | `command` 步骤 | 像构建工具一样解析可执行文件,在暂存目录里运行;`timeoutSeconds` 默认 600、最多 1800;`collect` 把子目录内容上移到项目根 | | `archive` 步骤 | 下载 zip 并用 `project.extractArchive` 解压;必须是 HTTPS(`localhost`、`127.0.0.1`、`[::1]` 可用 HTTP),不带凭据;最多 20,000 个条目、解压后 512 MiB | | Core 命令 | `project.generatorCatalog`、`project.generatorForm`、`project.generationPlan`、`project.checkName`、`project.extractArchive` | -步骤在暂存目录里按顺序执行,结束后宿主把它移到最终位置(目标已存在时拒绝)、可选记录 `java` 运行时为项目 JDK、按需创建 Git 仓库并打开项目。失败或取消会删掉暂存目录,不留下任何东西(`plugin.project.generationFailed`)。`requirements` 只用来提示启用或去市场安装相应插件,不会阻止创建。 +步骤在暂存目录里按顺序执行,结束后宿主把它移到最终位置(目标已存在时拒绝)、可选记录 `java` 运行时为项目 JDK(只用于版本 1)、把 `toolPins` 存为项目的工具固定(和工具设置页用的是同一个存储)、按需创建 Git 仓库并打开项目。失败或取消会删掉暂存目录,不留下任何东西(`plugin.project.generationFailed`)。`requirements` 只用来提示启用或去市场安装相应插件,不会阻止创建。 ### 契约章节 @@ -44,6 +45,7 @@ | --- | --- | | [Project generators](../../contract/v1/README.md#project-generators) | 宿主和生成器各自负责的部分、清单字段 | | [Fields](../../contract/v1/README.md#fields) | 字段类型、属性、项目名称规则 | +| [Tool fields](../../contract/v1/README.md#tool-fields) | `tool` 字段的属性、初始选择、最低版本,以及和 `runtime` 字段按版本互斥 | | [Conditions](../../contract/v1/README.md#conditions) | `when`、`visibleWhen` | | [Templates](../../contract/v1/README.md#templates-1) | 清单模板、模板文件、点文件 | | [Steps](../../contract/v1/README.md#steps) | 三种步骤和限制 | @@ -54,10 +56,11 @@ | 文件 | 内容 | | --- | --- | | [`project-generators/project-generator.plugin.json`](../../contract/v1/fixtures/project-generators/project-generator.plugin.json) | 三个生成器:`app`(`project` 分区,按构建系统条件写模板文件)、`archetype`(运行脚手架命令)、`initializr`(从可配置服务器下载压缩包) | -| [`project-generators/project-plans.json`](../../contract/v1/fixtures/project-generators/project-plans.json) | 10 个 `project.*` 命令的标准答案:表单默认值与派生、计划写模板、运行时过旧、运行命令、下载压缩包、拒绝非 HTTPS 服务器、项目名称检查 | -| [`project-generators/templates/`](../../contract/v1/fixtures/project-generators/templates/) | `app` 的模板目录:`app/README.md`、`app/dot-gitignore`、`main/Main.java`(Java 示例);`.gitattributes` 关闭换行转换,保证模板逐字节输出 | +| [`project-generators/project-plans.json`](../../contract/v1/fixtures/project-generators/project-plans.json) | 22 个 `project.*` 命令的标准答案:表单默认值与派生、计划写模板、运行时过旧、运行命令、下载压缩包、拒绝非 HTTPS 服务器、项目名称检查;其中 12 个工具字段用例用 `manifest` 指定 `project-generator-tool-field.plugin.json`(选最新安装、保留用户选择、低于最低版本、没有安装、展开并固定、可选字段不固定、末尾 0 视为同一版本等) | +| [`project-generators/project-generator-tool-field.plugin.json`](../../contract/v1/fixtures/project-generators/project-generator-tool-field.plugin.json) | 两个用 `tool` 字段的生成器(`projectGenerators` 2):`python-app`(`project` 分区,必填 `python` 工具字段,最低 3.11、固定到项目,用 `${field.python.executable}` 运行 `uv venv`)、`node-app`(`generator` 分区,可选 `node` 工具字段) | +| [`project-generators/templates/`](../../contract/v1/fixtures/project-generators/templates/) | `app` 的模板目录:`app/README.md`、`app/dot-gitignore`、`main/Main.java`(Java 示例),以及 `python-app` 用的 `tool-field/README.md`;`.gitattributes` 关闭换行转换,保证模板逐字节输出 | -共享示例里没有生成器的清单拒绝用例;计划层面的拒绝(运行时过旧、非 HTTPS 服务器、名称带斜杠)在 `project-plans.json` 里。 +生成器的清单拒绝用例在 [`manifest/invalid-manifests.json`](../../contract/v1/fixtures/manifest/invalid-manifests.json) 里,共 9 个,都和工具字段有关:版本 1 用 `tool` 字段、版本 2 用 `runtime` 字段、`tool` 缺失或不是工具定义 ID、`minimumVersion` 不是点分数字、`pinAs` 取值未知、工具字段带 `default`、非工具字段用 `.executable`、非工具字段带 `tool` 属性。计划层面的拒绝(运行时或工具过旧、非 HTTPS 服务器、名称带斜杠)在 `project-plans.json` 里。 --- @@ -68,7 +71,7 @@ A plugin can add entries to the host's New Project window: a plain project of a | Item | Value | | --- | --- | | Manifest field | `projectGenerators[]`: `id`, `displayName`, `section`, `order`, `icon`, `fields`, `requirements`, `steps`, `openFiles` | -| Capability versions | `projectGenerators` 1, at level 3. See [versions](../capabilities-and-versions.md#capability-versions-requires) | +| Capability versions | `projectGenerators` 1 and 2, at level 3. See [versions](../capabilities-and-versions.md#capability-versions-requires) | | Protocols | No process protocol; a generator is data that Core commands evaluate | | API IDs | [API-33](../api-catalog.md#apis-usable-now): project generators | | Permissions | `project.generate` | @@ -80,22 +83,23 @@ A plugin can add entries to the host's New Project window: a plain project of a | Version | Lowest level | Adds | Core | macOS | Windows | | --- | --- | --- | --- | --- | --- | | `projectGenerators` 1 | 3 | Two sections (`project`, `generator`); five field types (`text`, `enum`, `boolean`, `multiSelect`, `runtime`); conditions and templates; three step kinds (`files`, `command`, `archive`); five Core commands | Yes | Yes | No | +| `projectGenerators` 2 | 3 | `tool` fields (an existing installation of a plugin-declared tool definition); `${field..executable}`; the plan returns `toolPins`, which the host saves as the project's tool pins; form input `tools`, result `installations`, the `toolTooOld` problem; catalog entry `tools`. `runtime` fields are no longer allowed; version 1 does not allow `tool` fields | Yes | Yes | No | ### Fields, steps, and commands | Part | Content | | --- | --- | | `section` | `project` lists the entry under "New Project" (a plain project of a language); `generator` lists it under "Generators" (a framework or scaffolding tool); sorted by section, `order` (0–1000, default 100), then name | -| Field types | `text` (`default` template, `pattern`, `maxLength`, `maximumVersion`), `enum` (1–16 options), `boolean`, `multiSelect` (1–128 options, optionally grouped), `runtime` (a host-discovered installation, currently `java`) | +| Field types | `text` (`default` template, `pattern`, `maxLength`, `maximumVersion`), `enum` (1–16 options), `boolean`, `multiSelect` (1–128 options, optionally grouped), `runtime` (a host-discovered installation, currently `java`; version 1 only), `tool` (version 2: an existing installation of a plugin-declared tool definition, recorded by home directory; properties `tool`, `minimumVersion`, `pinAs`, `optional`; no `default`: the user's choice is kept, otherwise the newest version, and trailing zeros do not count) | | Conditions | `when` and `visibleWhen`: `equals`, `notEquals`, `oneOf`; a hidden field is not validated and reads as absent in conditions | -| Manifest templates | `${project.name}`, `${field.}`, `${field..version}`, `${field..major}`, with the modifiers `path`, `package`, and `lower` | +| Manifest templates | `${project.name}`, `${field.}`, `${field..version}`, `${field..major}` (runtime fields only), `${field..executable}` (tool fields only: the executable in the chosen installation), with the modifiers `path`, `package`, and `lower` | | Template files | Use `{{ … }}`, because POMs and Gradle scripts already contain `${…}`; a path segment starting with `dot-` becomes a dotfile (`dot-gitignore` → `.gitignore`) | | `files` step | Writes a packaged template directory into the project, keeping the executable bit; at most 1024 files, 1 MiB each, 8 MiB in total | | `command` step | Resolves the executable like a build tool and runs it in the staging directory; `timeoutSeconds` defaults to 600, at most 1800; `collect` moves a subdirectory's contents up into the project root | | `archive` step | Downloads a zip and extracts it with `project.extractArchive`; HTTPS only (HTTP allowed for `localhost`, `127.0.0.1`, `[::1]`), no credentials; at most 20,000 entries and 512 MiB extracted | | Core commands | `project.generatorCatalog`, `project.generatorForm`, `project.generationPlan`, `project.checkName`, `project.extractArchive` | -Steps run in order in a staging directory. Afterwards the host moves it to its final location (refusing an existing one), may record a `java` runtime as the project's JDK, creates the Git repository when asked, and opens the project. A failure or cancellation removes the staging directory and leaves nothing behind (`plugin.project.generationFailed`). `requirements` only prompt the user to enable or install the providing plugins; they never block creation. +Steps run in order in a staging directory. Afterwards the host moves it to its final location (refusing an existing one), may record a `java` runtime as the project's JDK (version 1 only), saves the `toolPins` as the project's tool pins (the same store the tool settings page edits), creates the Git repository when asked, and opens the project. A failure or cancellation removes the staging directory and leaves nothing behind (`plugin.project.generationFailed`). `requirements` only prompt the user to enable or install the providing plugins; they never block creation. ### Contract sections @@ -103,6 +107,7 @@ Steps run in order in a staging directory. Afterwards the host moves it to its f | --- | --- | | [Project generators](../../contract/v1/README.md#project-generators) | What the host and the generator each own; manifest fields | | [Fields](../../contract/v1/README.md#fields) | Field types, properties, project name rules | +| [Tool fields](../../contract/v1/README.md#tool-fields) | `tool` field properties, the initial selection, minimum versions, and how `runtime` and `tool` fields are split by version | | [Conditions](../../contract/v1/README.md#conditions) | `when` and `visibleWhen` | | [Templates](../../contract/v1/README.md#templates-1) | Manifest templates, template files, dotfiles | | [Steps](../../contract/v1/README.md#steps) | The three step kinds and their limits | @@ -113,7 +118,8 @@ Steps run in order in a staging directory. Afterwards the host moves it to its f | File | Content | | --- | --- | | [`project-generators/project-generator.plugin.json`](../../contract/v1/fixtures/project-generators/project-generator.plugin.json) | Three generators: `app` (the `project` section, writing template files conditioned on the build system), `archetype` (running a scaffolding command), `initializr` (downloading an archive from a configurable server) | -| [`project-generators/project-plans.json`](../../contract/v1/fixtures/project-generators/project-plans.json) | Golden answers for 10 `project.*` command cases: form defaults and derived values, a plan writing templates, a runtime that is too old, running a command, downloading an archive, refusing a non-HTTPS server, project name checks | -| [`project-generators/templates/`](../../contract/v1/fixtures/project-generators/templates/) | The `app` template directory: `app/README.md`, `app/dot-gitignore`, `main/Main.java` (a Java example); `.gitattributes` disables line-ending conversion so templates are emitted byte for byte | +| [`project-generators/project-plans.json`](../../contract/v1/fixtures/project-generators/project-plans.json) | Golden answers for 22 `project.*` command cases: form defaults and derived values, a plan writing templates, a runtime that is too old, running a command, downloading an archive, refusing a non-HTTPS server, project name checks; 12 tool field cases name `project-generator-tool-field.plugin.json` in `manifest` (the newest installation, keeping the user's choice, below the minimum, no installation, expanding and pinning, an optional field left unpinned, trailing zeros as the same release, and more) | +| [`project-generators/project-generator-tool-field.plugin.json`](../../contract/v1/fixtures/project-generators/project-generator-tool-field.plugin.json) | Two generators with `tool` fields (`projectGenerators` 2): `python-app` (the `project` section; a required `python` tool field, at least 3.11, pinned to the project, running `uv venv` with `${field.python.executable}`) and `node-app` (the `generator` section; an optional `node` tool field) | +| [`project-generators/templates/`](../../contract/v1/fixtures/project-generators/templates/) | The `app` template directory: `app/README.md`, `app/dot-gitignore`, `main/Main.java` (a Java example), plus `tool-field/README.md` for `python-app`; `.gitattributes` disables line-ending conversion so templates are emitted byte for byte | -The shared fixtures have no manifest refusal cases for generators; plan-level refusals (a runtime that is too old, a non-HTTPS server, a name with a slash) are in `project-plans.json`. +Generator manifest refusals are in [`manifest/invalid-manifests.json`](../../contract/v1/fixtures/manifest/invalid-manifests.json): 9 cases, all about tool fields (a `tool` field under version 1, a `runtime` field under version 2, a missing `tool` or one that is not a tool definition ID, a `minimumVersion` that is not dotted numbers, an unknown `pinAs`, a `default` on a tool field, `.executable` on a non-tool field, a `tool` property on a non-tool field). Plan-level refusals (a runtime or tool that is too old, a non-HTTPS server, a name with a slash) are in `project-plans.json`. diff --git a/reference/domains/toolchains.md b/reference/domains/toolchains.md index cca6dbe..1477b3b 100644 --- a/reference/domains/toolchains.md +++ b/reference/domains/toolchains.md @@ -118,7 +118,8 @@ Core 另有 `toolchains.resolveConsumerPreferences` 和 `toolchains.editConsumer | [Pins and snapshot resolution](../../contract/v1/toolchains/README.md#pins-and-snapshot-resolution) | `local.json`、`resolve`、四层选择、`checkVersion`、`discoveryPlan` | | [Settings candidates](../../contract/v1/toolchains/README.md#settings-candidates) | `candidates` 和两次调用的探测流程 | | [Shared executable candidates](../../contract/v1/README.md#shared-executable-candidates-executableresolution-12) | `executableResolution` 1、2 的候选集 | -| [提案](../../contract/v1/toolchains/proposal/README.md):[manifest.md](../../contract/v1/toolchains/proposal/manifest.md)、[protocol.md](../../contract/v1/toolchains/proposal/protocol.md) | 更大范围的设计(消费者迁移、原生生命周期、保存、版本管理器插件),尚未实现;[错误码表](../../contract/v1/toolchains/proposal/protocol.md#stable-failure-mapping)已被 Core 使用 | +| [Provider backend negotiation](../../contract/v1/toolchains/README.md#provider-backend-negotiation) | `litheToolchains` 协商(`initialize` 里的 `experimental.litheToolchains`)、`lsp.toolchainRequest`、超时 | +| [Error replies](../../contract/v1/toolchains/README.md#error-replies) | 错误回复的格式和 11 个稳定失败码的数字码对照 | ### 示例与用例 @@ -126,13 +127,8 @@ Core 另有 `toolchains.resolveConsumerPreferences` 和 `toolchains.editConsumer | --- | --- | | [`toolchains/tool-definition.plugin.json`](../../contract/v1/fixtures/toolchains/tool-definition.plugin.json) | 生产形态的工具定义:`java`,8 个候选(项目/全局设置、项目/全局工具链请求、macOS/Linux 与 Windows 的 `JAVA_HOME`、`java` 命令、已安装的工具链),在 macOS 的 `JavaVirtualMachines` 目录里发现 `*.jdk` | | [`toolchains/toolchain-provider.plugin.json`](../../contract/v1/fixtures/toolchains/toolchain-provider.plugin.json) | 生产形态的提供者:`mise`,覆盖 `java`、`node`、`maven`、`pnpm`、`go`,监视 6 个文件(`.tool-versions` 用兼容读取器),后端是包内可执行文件 | -| [`toolchains/proposal/java-tool-definition.json`](../../contract/v1/fixtures/toolchains/proposal/java-tool-definition.json)、[`mise-provider.json`](../../contract/v1/fixtures/toolchains/proposal/mise-provider.json) | 提案形态的定义(含一个消费者)和提供者 | -| [`toolchains/proposal/local.json`](../../contract/v1/fixtures/toolchains/proposal/local.json) | 带 `pins` 的 `local.json` | -| [`toolchains/proposal/manifest-rejections.json`](../../contract/v1/fixtures/toolchains/proposal/manifest-rejections.json) | 15 个清单拒绝用例:缺权限、导出替换 `PATH`、`PATH` 前缀离开主目录、无界通配、探针带脚本参数、社区插件占用短 ID、后定义覆盖已有所有者等 | -| [`toolchains/proposal/query-cases.json`](../../contract/v1/fixtures/toolchains/proposal/query-cases.json) | 13 个查询用例(未满足的要求、工作区里的主目录、取消后迟到的应答、超时和取消的区别、监视文件变化触发全部重查等)和 11 个错误应答 | -| [`toolchains/proposal/resolution-cases.json`](../../contract/v1/fixtures/toolchains/proposal/resolution-cases.json) | 13 个选择用例(按路径段继承固定、不可用的固定不回退、全局冲突回到登录 shell、按数字比较版本、相同最高版本要求选择等)和 3 个无效 `local.json` | -Core 的验收测试在主仓库的 [`toolchain_definitions.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_definitions.rs) 和同目录的 `toolchain_pins.rs`、`toolchain_queries.rs`、`toolchain_resolution.rs`。 +Core 的验收测试在主仓库的 [`toolchain_definitions.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_definitions.rs) 和同目录的 `toolchain_pins.rs`、`toolchain_queries.rs`、`toolchain_resolution.rs`。这三个测试共用的固定值文档、查询往返和每个稳定失败码的错误回复内联在 [`toolchain_vectors.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_vectors.rs) 里。 ### 相关指南 @@ -257,7 +253,8 @@ A provider may use these codes in JSON-RPC errors, and `decodeReply` checks them | [Pins and snapshot resolution](../../contract/v1/toolchains/README.md#pins-and-snapshot-resolution) | `local.json`, `resolve`, the four layers, `checkVersion`, `discoveryPlan` | | [Settings candidates](../../contract/v1/toolchains/README.md#settings-candidates) | `candidates` and the two-call probe flow | | [Shared executable candidates](../../contract/v1/README.md#shared-executable-candidates-executableresolution-12) | The `executableResolution` 1 and 2 candidate sets | -| [Proposal](../../contract/v1/toolchains/proposal/README.md): [manifest.md](../../contract/v1/toolchains/proposal/manifest.md), [protocol.md](../../contract/v1/toolchains/proposal/protocol.md) | The broader design (consumer migration, native lifecycle, persistence, manager plugins), not implemented; Core already uses its [failure code table](../../contract/v1/toolchains/proposal/protocol.md#stable-failure-mapping) | +| [Provider backend negotiation](../../contract/v1/toolchains/README.md#provider-backend-negotiation) | `litheToolchains` negotiation (`experimental.litheToolchains` in `initialize`), `lsp.toolchainRequest`, timeouts | +| [Error replies](../../contract/v1/toolchains/README.md#error-replies) | The error reply format and the numeric codes of the 11 stable failure codes | ### Examples and cases @@ -265,13 +262,8 @@ A provider may use these codes in JSON-RPC errors, and `decodeReply` checks them | --- | --- | | [`toolchains/tool-definition.plugin.json`](../../contract/v1/fixtures/toolchains/tool-definition.plugin.json) | A production-shape tool definition: `java` with 8 candidates (project and global settings, project and global toolchain requests, `JAVA_HOME` on macOS/Linux and on Windows, the `java` command, installed toolchains), discovering `*.jdk` in the macOS `JavaVirtualMachines` directories | | [`toolchains/toolchain-provider.plugin.json`](../../contract/v1/fixtures/toolchains/toolchain-provider.plugin.json) | A production-shape provider: `mise` for `java`, `node`, `maven`, `pnpm` and `go`, watching 6 files (`.tool-versions` with the compatible reader), with a packaged executable backend | -| [`toolchains/proposal/java-tool-definition.json`](../../contract/v1/fixtures/toolchains/proposal/java-tool-definition.json), [`mise-provider.json`](../../contract/v1/fixtures/toolchains/proposal/mise-provider.json) | Proposal-shape definition (with one consumer) and provider | -| [`toolchains/proposal/local.json`](../../contract/v1/fixtures/toolchains/proposal/local.json) | A `local.json` with `pins` | -| [`toolchains/proposal/manifest-rejections.json`](../../contract/v1/fixtures/toolchains/proposal/manifest-rejections.json) | 15 manifest refusals: a missing permission, an export replacing `PATH`, a `PATH` prefix leaving the home, an unbounded glob, a probe with a script argument, a community plugin claiming a short ID, a later definition replacing an owner, and more | -| [`toolchains/proposal/query-cases.json`](../../contract/v1/fixtures/toolchains/proposal/query-cases.json) | 13 query cases (an unmet requirement, a home inside the workspace, a late reply after cancellation, timeout versus cancellation, a watched file change requerying every provider, and more) and 11 error replies | -| [`toolchains/proposal/resolution-cases.json`](../../contract/v1/fixtures/toolchains/proposal/resolution-cases.json) | 13 selection cases (component-wise pin inheritance, an unusable pin without fallback, conflicting globals falling back to the login shell, numeric version comparison, equal highest versions needing a choice, and more) and 3 invalid `local.json` documents | -Core's acceptance tests are [`toolchain_definitions.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_definitions.rs) and its siblings `toolchain_pins.rs`, `toolchain_queries.rs` and `toolchain_resolution.rs` in the main repository. +Core's acceptance tests are [`toolchain_definitions.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_definitions.rs) and its siblings `toolchain_pins.rs`, `toolchain_queries.rs` and `toolchain_resolution.rs` in the main repository. The pin document, query exchange and one error reply per stable failure code that those three share are inline in [`toolchain_vectors.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/tests/toolchain_vectors.rs). ### Related guides diff --git a/reference/protocol-changelog.md b/reference/protocol-changelog.md index a6ff57e..e7f95eb 100644 --- a/reference/protocol-changelog.md +++ b/reference/protocol-changelog.md @@ -5,7 +5,7 @@ - **收录范围**:Lithe 主仓库 `preview` 分支上的 PR,只要它修改了下面三处中的任意一处:插件契约 `shared/contracts/plugins/v1/`、Core 能力表 [`capabilities.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/plugins/capabilities.rs)、插件函数协议 [`plugin_function.rs`](https://github.com/1lck/Lithe-IDEA/blob/preview/rust/lithe-core/src/lsp/interface/plugin_function.rs)。三处都没改、只改宿主内部实现的 PR 不收录。 - **日期**:PR 合入 `preview` 的日期。 - **修改人**:原 PR 的作者(GitHub 账号)。登记这些 PR 的同步 PR 会在描述里 @ 他们,请他们核对自己那一行。 -- **记录到**:`preview` 的 [`644afdaac`](https://github.com/1lck/Lithe-IDEA/commit/644afdaac9087b276cb8f7902658b85ae1792d6d)([#1747](https://github.com/1lck/Lithe-IDEA/pull/1747)),即本仓库 `contract/v1/` 当前副本的来源。 +- **记录到**:`preview` 的 [`5d38352a1`](https://github.com/1lck/Lithe-IDEA/commit/5d38352a1757a70fd5fa297d7aa3e43bb2e33607)([#1752](https://github.com/1lck/Lithe-IDEA/pull/1752)),即本仓库 `contract/v1/` 当前副本的来源。 - **每一版的内容和宿主现状**:见[能力与协议版本对照](capabilities-and-versions.md)。字段和行为以 [`contract/v1/README.md`](../contract/v1/README.md) 为准,官方 SDK 的发版见 [SDK 发版历史](capabilities-and-versions.md#sdk-发版历史)。 时间线的“协议 / API”一列里,**加粗**的是这个 PR 新增的版本号;没有加粗的,是同一版本内的补充、宿主接入或说明修正。 @@ -58,6 +58,7 @@ | `executableResolution` | 2 | 2026-11-01 | [#1581](https://github.com/1lck/Lithe-IDEA/pull/1581) | | `vcsProviders` | 1 | 2026-10-23 | [#1430](https://github.com/1lck/Lithe-IDEA/pull/1430) | | `projectGenerators` | 1 | 2026-10-24 | [#1452](https://github.com/1lck/Lithe-IDEA/pull/1452) | +| `projectGenerators` | 2 | 2026-11-05 | [#1740](https://github.com/1lck/Lithe-IDEA/pull/1740) | | `backend` | 1 | 2026-10-24 | [#1470](https://github.com/1lck/Lithe-IDEA/pull/1470) | | `backend` | 2 | 2026-10-30 | [#1595](https://github.com/1lck/Lithe-IDEA/pull/1595) | | `debugAdapters` | 1 | 2026-10-26 | [#1510](https://github.com/1lck/Lithe-IDEA/pull/1510) | @@ -78,7 +79,7 @@ | `litheBuild` | 2–6 | 2026-10-28 | [#1517](https://github.com/1lck/Lithe-IDEA/pull/1517) | | `litheBuild` | 7–11 | 2026-11-04 | [#1558](https://github.com/1lck/Lithe-IDEA/pull/1558) | | `litheVcs` | 1 | 2026-10-23 | [#1430](https://github.com/1lck/Lithe-IDEA/pull/1430) | -| `litheToolchains` | 1(提案) | 2026-11-01 | [#1581](https://github.com/1lck/Lithe-IDEA/pull/1581) | +| `litheToolchains` | 1 | 2026-11-01 | [#1581](https://github.com/1lck/Lithe-IDEA/pull/1581) 实现;正式契约由 [#1752](https://github.com/1lck/Lithe-IDEA/pull/1752) 写入 | | `litheKeymapImport` | 1 | 2026-11-03 | [#1695](https://github.com/1lck/Lithe-IDEA/pull/1695) | | `litheLogging` | 1 | 2026-11-04 | [#1699](https://github.com/1lck/Lithe-IDEA/pull/1699) | @@ -88,6 +89,8 @@ | PR | 修改人 | 协议 / API | 改了什么 | | --- | --- | --- | --- | +| [#1752](https://github.com/1lck/Lithe-IDEA/pull/1752) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | `litheToolchains` 1:写入正式契约 | 删除工具链设计草案 `toolchains/proposal/` 和 `fixtures/toolchains/proposal/`。草案里已实现的部分写进正式契约 `toolchains/README.md`:新增“Provider backend negotiation”一节(`initialize` 里的 `experimental.litheToolchains` 协商、`lsp.toolchainRequest`、超时)和“Error replies”小节(错误回复格式与 11 个稳定失败码的数字码对照)。Core 测试改用内联数据 `toolchain_vectors.rs`。协议行为没有变化。 | +| [#1740](https://github.com/1lck/Lithe-IDEA/pull/1740) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | **`projectGenerators` 2**(API-33) | 生成器新增 `tool` 字段:从插件声明的工具定义的已有安装里选一个(`tool`、`minimumVersion`、`pinAs`、`optional`),模板可用 `${field..executable}`;`pinAs: "project"` 时计划返回 `toolPins`,宿主把它存为新项目的工具固定。表单输入加 `tools`,结果字段带 `installations`,新增问题码 `toolTooOld`;目录条目带 `tools`(声明该工具的插件)。版本 2 不能用 `runtime` 字段,版本 1 不能用 `tool` 字段。macOS 声明 1、2,Windows 不声明。新增示例 `project-generator-tool-field.plugin.json`、12 个计划用例和 9 个清单拒绝用例。 | | [#1747](https://github.com/1lck/Lithe-IDEA/pull/1747) | [@xiaoyumuxi](https://github.com/xiaoyumuxi) | 清单 schema:`requires.buildSystems` | schema 的枚举补齐到 1–4(Core 从 #1558 起已经支持 3、4,schema 当时只写到 3)。fixtures 按功能分目录,工具链提案移到 `toolchains/proposal/`,契约 README 的能力版本表随之修正。 | ### 2026-11-04