From 51c8caf55f7e62eb8aa2478717c0549f3d80cf7b Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sat, 12 Sep 2026 18:15:10 +0800 Subject: [PATCH 1/4] feat(protocol): support directory project targets --- conformance/README.md | 2 +- .../41-project-loading-directory-target.json | 28 ++++++ conformance/mock-provider/src/main.rs | 97 +++++++++++++++---- spec/lpp-v1.md | 64 ++++++++---- 4 files changed, 153 insertions(+), 38 deletions(-) create mode 100644 conformance/fixtures/v1/41-project-loading-directory-target.json diff --git a/conformance/README.md b/conformance/README.md index f28f1e4..3742ab7 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -1,6 +1,6 @@ # LPP v1 Conformance Suite -This directory contains the test suite and fixtures for the Language Provider Protocol v1 wire contract, including the LPP 1.1 additive project-loading revision (see [`../spec/lpp-v1.md`](../spec/lpp-v1.md)). +This directory contains the test suite and fixtures for the Language Provider Protocol v1 wire contract, including the LPP 1.1 file-entry and LPP 1.2 directory-target project-loading revisions (see [`../spec/lpp-v1.md`](../spec/lpp-v1.md)). ## Layout diff --git a/conformance/fixtures/v1/41-project-loading-directory-target.json b/conformance/fixtures/v1/41-project-loading-directory-target.json new file mode 100644 index 0000000..2c5570f --- /dev/null +++ b/conformance/fixtures/v1/41-project-loading-directory-target.json @@ -0,0 +1,28 @@ +{ + "name": "project-loading-directory-target", + "description": "LPP 1.2 lets a client select a directory while the provider owns default-entry selection and source closure loading.", + "scope": "protocol", + "providerArgs": ["--protocol-version", "1.2"], + "projectFiles": { + "entry.xdl": "puzzle entry {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n", + "support.xdl": "puzzle support {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n" + }, + "steps": [ + { + "request": {"jsonrpc": "2.0", "id": 1, "method": "lpp/initialize", "params": {"protocolVersion": "1.2"}}, + "expectResponse": {"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "1.2", "serverInfo": {"name": "lpp-mock-provider", "version": "0.1.0"}, "languages": [{"id": "x-demo-lang", "extensions": ["xdl"]}], "capabilities": {"check": true, "compile": true, "reconstruct": true, "symbols": true, "definition": true, "references": true, "rename": true, "editValidation": true, "projectLoading": true}}} + }, + { + "request": {"jsonrpc": "2.0", "id": 2, "method": "lpp/check", "params": {"entry": {"uri": "${PROJECT_URI}", "languageId": "x-demo-lang", "version": 7, "kind": "directory"}}}, + "expectResponse": {"jsonrpc": "2.0", "id": 2, "result": {"documents": [{"uri": "${PROJECT_URI}/entry.xdl", "version": 7, "diagnostics": []}, {"uri": "${PROJECT_URI}/support.xdl", "version": 7, "diagnostics": []}]}} + }, + { + "request": {"jsonrpc": "2.0", "id": 3, "method": "lpp/compile", "params": {"entry": {"uri": "${PROJECT_URI}", "languageId": "x-demo-lang", "version": 7, "kind": "directory"}}}, + "expectResponse": {"jsonrpc": "2.0", "id": 3, "result": {"diagnostics": [{"uri": "${PROJECT_URI}/entry.xdl", "version": 7, "diagnostics": []}, {"uri": "${PROJECT_URI}/support.xdl", "version": 7, "diagnostics": []}], "artifact": {"format": "x-demo/puzzle-eval-v1", "content": "{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}} + }, + { + "request": {"jsonrpc": "2.0", "id": 4, "method": "lpp/shutdown", "params": {}}, + "expectResponse": {"jsonrpc": "2.0", "id": 4, "result": null} + } + ] +} diff --git a/conformance/mock-provider/src/main.rs b/conformance/mock-provider/src/main.rs index 415617e..00347ca 100644 --- a/conformance/mock-provider/src/main.rs +++ b/conformance/mock-provider/src/main.rs @@ -14,6 +14,7 @@ use puzzle::{ const DEFAULT_PROTOCOL_VERSION: &str = "1.0"; const PROTOCOL_VERSION_1_1: &str = "1.1"; +const PROTOCOL_VERSION_1_2: &str = "1.2"; const SERVER_NAME: &str = "lpp-mock-provider"; const LANGUAGE_ID: &str = "x-demo-lang"; const LANGUAGE_EXTENSIONS: [&str; 1] = ["xdl"]; @@ -89,7 +90,10 @@ impl Capabilities { "rename": self.rename, "editValidation": self.edit_validation, }); - if protocol_version == PROTOCOL_VERSION_1_1 { + if matches!( + protocol_version, + PROTOCOL_VERSION_1_1 | PROTOCOL_VERSION_1_2 + ) { capabilities["projectLoading"] = json!(self.project_loading); } capabilities @@ -137,6 +141,16 @@ struct ProjectEntry { uri: String, language_id: String, version: i64, + #[serde(default)] + kind: ProjectTargetKind, +} + +#[derive(Debug, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +enum ProjectTargetKind { + #[default] + File, + Directory, } #[allow(dead_code)] @@ -276,7 +290,10 @@ fn parse_args() -> (String, Capabilities) { eprintln!("lpp-mock-provider: --protocol-version requires a version"); std::process::exit(2); }; - if version != DEFAULT_PROTOCOL_VERSION && version != PROTOCOL_VERSION_1_1 { + if version != DEFAULT_PROTOCOL_VERSION + && version != PROTOCOL_VERSION_1_1 + && version != PROTOCOL_VERSION_1_2 + { eprintln!("lpp-mock-provider: unsupported protocol version '{version}'"); std::process::exit(2); } @@ -838,7 +855,10 @@ fn documents_for_request( match (documents, entry) { (Some(documents), None) => Ok((documents, None)), (None, Some(entry)) => { - if server.protocol_version.as_deref() != Some(PROTOCOL_VERSION_1_1) { + if !matches!( + server.protocol_version.as_deref(), + Some(PROTOCOL_VERSION_1_1) | Some(PROTOCOL_VERSION_1_2) + ) { return Err(HandlerError::Std(-32602, "Invalid params")); } if !server.caps.project_loading { @@ -881,35 +901,72 @@ fn load_project(entry: &ProjectEntry) -> Result<(HashMap, Stri )); }; let entry_path = std::fs::canonicalize(&entry_path).map_err(|_| { + let (reason, message) = match entry.kind { + ProjectTargetKind::File => ("entryNotFound", "project entry could not be loaded"), + ProjectTargetKind::Directory => { + ("targetNotFound", "project target could not be loaded") + } + }; HandlerError::Lpp( "projectLoadFailed", json!({ "entryUri": entry.uri, - "reason": "entryNotFound", + "reason": reason, "uri": entry.uri, }), - "project entry could not be loaded".to_string(), + message.to_string(), ) })?; - if !entry_path.is_file() { - return Err(HandlerError::Lpp( - "projectLoadFailed", - json!({ - "entryUri": entry.uri, - "reason": "entryNotFile", - "uri": entry.uri, - }), - "project entry is not a file".to_string(), - )); - } - let canonical_entry_uri = path_to_file_uri(&entry_path).ok_or_else(|| { + let (project_root, selected_entry) = match entry.kind { + ProjectTargetKind::File => { + if !entry_path.is_file() { + return Err(HandlerError::Lpp( + "projectLoadFailed", + json!({ + "entryUri": entry.uri, + "reason": "entryNotFile", + "uri": entry.uri, + }), + "project entry is not a file".to_string(), + )); + } + let root = entry_path.parent().expect("a file has a parent"); + (root.to_path_buf(), entry_path) + } + ProjectTargetKind::Directory => { + if !entry_path.is_dir() { + return Err(HandlerError::Lpp( + "projectLoadFailed", + json!({ + "entryUri": entry.uri, + "reason": "targetNotDirectory", + "uri": entry.uri, + }), + "project target is not a directory".to_string(), + )); + } + let selected = entry_path.join("entry.xdl"); + if !selected.is_file() { + return Err(HandlerError::Lpp( + "projectLoadFailed", + json!({ + "entryUri": entry.uri, + "reason": "defaultEntryNotFound", + "uri": entry.uri, + }), + "project directory has no default entry".to_string(), + )); + } + (entry_path, selected.canonicalize().expect("entry exists")) + } + }; + let canonical_entry_uri = path_to_file_uri(&selected_entry).ok_or_else(|| { HandlerError::Lpp( "projectLoadFailed", json!({ "entryUri": entry.uri, "reason": "sourceIdentityUnavailable" }), "project entry has no stable URI".to_string(), ) })?; - let project_root = entry_path.parent().expect("a file has a parent"); let mut paths: Vec = std::fs::read_dir(project_root) .map_err(|error| { HandlerError::Lpp( @@ -924,7 +981,7 @@ fn load_project(entry: &ProjectEntry) -> Result<(HashMap, Stri .filter_map(Result::ok) .map(|directory_entry| directory_entry.path()) .filter(|path| { - path == &entry_path || path.extension().is_some_and(|extension| extension == "xdl") + path == &selected_entry || path.extension().is_some_and(|extension| extension == "xdl") }) .collect(); paths.sort(); @@ -945,7 +1002,7 @@ fn load_project(entry: &ProjectEntry) -> Result<(HashMap, Stri HandlerError::Lpp( "projectLoadFailed", json!({ - "entryUri": entry.uri, + "entryUri": entry.uri, "reason": "sourceIdentityUnavailable", }), "source file has no stable URI".to_string(), diff --git a/spec/lpp-v1.md b/spec/lpp-v1.md index 52a260b..f3b6112 100644 --- a/spec/lpp-v1.md +++ b/spec/lpp-v1.md @@ -2,7 +2,7 @@ | | | | --- | --- | -| Protocol versions | `1.0`, `1.1` | +| Protocol versions | `1.0`, `1.1`, `1.2` | | Status | Normative for protocol major version 1 | | Transport | JSON-RPC 2.0 over stdio, newline-delimited framing | | Conformance | `conformance/fixtures/v1/` + `conformance/runner` + `conformance/mock-provider` | @@ -244,7 +244,8 @@ client MUST include every document the request may need; the provider MUST NOT assume any document exists outside the set and MUST NOT return edits for documents it did not receive. LPP 1.1 entry-based `check` and `compile` requests are the explicit filesystem-loading exception defined in -[Section 8.1](#81-entry-based-project-requests-lpp-11). +[Section 8.1](#81-entry-based-project-requests-lpp-11) and extended to +directory targets in [Section 8.2](#82-directory-project-requests-lpp-12). ### 6.6 Diagnostic @@ -316,6 +317,9 @@ provider-owned filesystem project load: filesystem snapshot for this request. It is echoed in every source result. It is not a filesystem content hash and does not provide cross-request stale detection. +* `kind`: REQUIRED for LPP 1.2 directory-capable requests and omitted by LPP + 1.0/1.1 clients. It is `"file"` for the existing entry behavior or + `"directory"` when the provider must discover the effective project entry. The entry identifies the user's selected source target only. The provider determines the effective project root and source closure according to the @@ -342,7 +346,7 @@ Initialization and capability negotiation. The client MUST send | Field | Type | Description | | --- | --- | --- | -| `protocolVersion` | string | The protocol version the client wants to speak: `"1.0"` or `"1.1"`. | +| `protocolVersion` | string | The protocol version the client wants to speak: `"1.0"`, `"1.1"`, or `"1.2"`. | | `clientInfo` | object, OPTIONAL | `{ "name": string, "version": string }` identifying the client. | ### 7.2 Result @@ -369,10 +373,10 @@ Initialization and capability negotiation. The client MUST send | Field | Type | Description | | --- | --- | --- | -| `protocolVersion` | string | The protocol version the provider will speak: `"1.0"` or `"1.1"`. | +| `protocolVersion` | string | The protocol version the provider will speak: `"1.0"`, `"1.1"`, or `"1.2"`. | | `serverInfo` | object | `{ "name": string, "version": string }` identifying the provider. | | `languages` | array | One entry per source language the provider serves. | -| `capabilities` | object | One boolean field per capability. LPP 1.0 requires the eight fields listed below; LPP 1.1 additionally requires `projectLoading`. | +| `capabilities` | object | One boolean field per capability. LPP 1.0 requires the eight fields listed below; LPP 1.1 and 1.2 additionally require `projectLoading`. | Each language entry: `{ "id": string, "extensions": [string] }`. `extensions` is the list of file extensions the provider associates with the language, @@ -394,7 +398,7 @@ extension. | `references` | `lpp/references` | Find references to the symbol at a position. | | `rename` | `lpp/rename` | Compute source edits for a semantic rename. | | `editValidation` | `lpp/validateEdits` | Validate a set of source edits against a document. | -| `projectLoading` | `lpp/check`, `lpp/compile` | Accept a client-selected entry and load its filesystem-backed source project. LPP 1.1 only. | +| `projectLoading` | `lpp/check`, `lpp/compile` | Accept a client-selected entry or directory target and load its filesystem-backed source project. LPP 1.1 and 1.2. | * The provider MUST set each capability to `true` only if it fully implements the corresponding method(s). @@ -451,7 +455,7 @@ Document-scoped methods share this parameter shape: In LPP 1.1, `lpp/check` and `lpp/compile` accept either `documents` or `entry`, but not both. An `entry` request is available only when the provider -accepted protocol version `1.1` and advertised `projectLoading: true`. +accepted protocol version `1.1` or `1.2` and advertised `projectLoading: true`. The optional `projectRoot` field remains legal and is informational; the provider accepts it but determines the effective project root and source closure from the entry and the source language's rules. @@ -493,6 +497,31 @@ an LPP error of kind `projectLoadFailed`. The `details` object MUST contain `entryUri` and a provider-defined `reason`; a required-file failure SHOULD also include the affected `uri`. +### 8.2 Directory project requests (LPP 1.2) + +LPP 1.2 extends the `entry` object with `kind: "directory"`: + +```json +{ + "entry": { + "uri": "file:///project", + "languageId": "opy", + "version": 7, + "kind": "directory" + } +} +``` + +The URI MUST identify an absolute filesystem directory. The provider owns +selection of the effective project entry, project root, and source closure +according to the source language's rules. The client MUST NOT enumerate files, +parse project manifests, or select an entry file to emulate this behavior. +Providers MUST return canonical source identities for every loaded document and +MUST fail with `projectLoadFailed` if the directory or its language-owned +default entry cannot be loaded. A `kind` of `"file"` has the same semantics as +the LPP 1.1 entry request. LPP 1.1 clients omit `kind` and therefore always +request file-entry behavior. + ## 9. lpp/check Produce diagnostics for a set of documents. The provider MUST parse and @@ -929,8 +958,8 @@ All LPP-defined errors use JSON-RPC error code `-32000` and carry a structured | `invalidRequest` | `{ "reason": string }` | Session violations: `notInitialized`, `alreadyInitialized`, `notificationNotSupported`. | | `invalidLanguage` | `{ "languageId": string }` | A document's `languageId` is not served by the provider. | | `invalidDocument` | `{ "uri"?: string, "reason": string }` | A document is unusable (for example a negative version). | -| `invalidEntry` | `{ "entryUri": string, "reason": string }` | A project entry has an unsupported URI or language. LPP 1.1 only. | -| `projectLoadFailed` | `{ "entryUri": string, "reason": string, "uri"?: string }` | A filesystem-backed project entry or required source file could not be loaded. LPP 1.1 only. | +| `invalidEntry` | `{ "entryUri": string, "reason": string }` | A project entry/target has an unsupported URI, language, or kind. LPP 1.1+. | +| `projectLoadFailed` | `{ "entryUri": string, "reason": string, "uri"?: string }` | A filesystem-backed project entry/target or required source file could not be loaded. LPP 1.1+. | | `invalidPosition` | `{ "uri": string, "position": Position }` | A position outside the document. | | `invalidArtifact` | `{ "reason": string }` | An artifact in a supported format whose content is malformed. | | `capabilityUnavailable` | `{ "capability": string, "method": string }` | A method was invoked whose capability was not negotiated. | @@ -955,8 +984,8 @@ clients MUST NOT parse `message`. ### 19.1 Versioning scheme * Protocol versions are strings of the form `MAJOR.MINOR` (for example - `"1.0"`). LPP 1.0 is the first published version and LPP 1.1 is an additive - revision of the same protocol major version. + `"1.0"`). LPP 1.0 is the first published version; LPP 1.1 adds file-entry + project loading and LPP 1.2 adds directory targets. * `MAJOR` changes are breaking: message shapes, method semantics, or framing may change. A breaking change always produces a new MAJOR version, and clients and providers speaking different MAJOR versions are never expected @@ -973,8 +1002,9 @@ clients MUST NOT parse `message`. * The provider either accepts it (echoing the version in the result) or fails with `protocolVersionMismatch` listing `supportedProtocolVersions`. * A client that receives the mismatch MUST pick the highest mutually supported - version and restart the session, or terminate. LPP 1.1 clients MAY use the - `projectLoading` capability; clients that need it MUST request `"1.1"`. + version and restart the session, or terminate. LPP 1.1 and 1.2 clients MAY + use the `projectLoading` capability; clients that need directory targets MUST + request `"1.2"`. * A provider MUST support at least one of the versions it lists in `supportedProtocolVersions`. @@ -1000,8 +1030,8 @@ contract: initialization, capability negotiation, diagnostics, check, compile, reconstruct, symbols, definition, references, rename, edit validation, project loading, errors/refusals, protocol mismatch, malformed messages, - and shutdown. The same directory covers LPP 1.0 and its LPP 1.1 additive - revision. + and shutdown. The same directory covers LPP 1.0, its LPP 1.1 file-entry + revision, and its LPP 1.2 directory-target revision. * `conformance/runner/`: a runner that replays fixtures against any provider binary and compares responses exactly. * `conformance/mock-provider/`: the reference provider for the demonstration @@ -1022,8 +1052,8 @@ Methods: | --- | --- | --- | --- | | `lpp/initialize` | none | `{ protocolVersion, clientInfo? }` | `{ protocolVersion, serverInfo, languages, capabilities }` | | `lpp/shutdown` | none | `{}` | `null` | -| `lpp/check` | `check`; plus `projectLoading` for an LPP 1.1 `entry` request | `{ documents, projectRoot? }` or `{ entry, projectRoot? }` | `{ documents: [{ uri, version, diagnostics }] }` | -| `lpp/compile` | `compile`; plus `projectLoading` for an LPP 1.1 `entry` request | `{ documents, projectRoot? }` or `{ entry, projectRoot? }` | `{ diagnostics: [{ uri, version, diagnostics }], artifact }` | +| `lpp/check` | `check`; plus `projectLoading` for an LPP 1.1/1.2 `entry` request | `{ documents, projectRoot? }` or `{ entry, projectRoot? }` | `{ documents: [{ uri, version, diagnostics }] }` | +| `lpp/compile` | `compile`; plus `projectLoading` for an LPP 1.1/1.2 `entry` request | `{ documents, projectRoot? }` or `{ entry, projectRoot? }` | `{ diagnostics: [{ uri, version, diagnostics }], artifact }` | | `lpp/reconstruct` | `reconstruct` | `{ artifact }` | `{ source, uri? }` | | `lpp/symbols` | `symbols` | `{ documents, projectRoot? }` | `{ documents: [{ uri, version, symbols }] }` | | `lpp/definition` | `definition` | `{ document, position }` | `{ locations }` | From 9d85976833b71ecf5dc3457dbb5f09ccf43f3750 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sat, 12 Sep 2026 19:53:29 +0800 Subject: [PATCH 2/4] fix(protocol): enforce directory target version boundary --- README.md | 2 +- conformance/README.md | 2 +- ...roject-loading-directory-version-gate.json | 15 +++++ conformance/mock-provider/src/main.rs | 56 +++++++++++++++---- spec/lpp-v1.md | 42 ++++++++------ 5 files changed, 87 insertions(+), 30 deletions(-) create mode 100644 conformance/fixtures/v1/42-project-loading-directory-version-gate.json diff --git a/README.md b/README.md index 9602b1a..8b1084e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ its frontend, compiler, reconstruction, or Workshop integration internally. ## Status -- Protocol versions: **1.0** and additive **1.1** (specified in [`spec/lpp-v1.md`](spec/lpp-v1.md)). +- Protocol versions: **1.0**, additive **1.1** file-entry loading, and additive **1.2** directory targets (specified in [`spec/lpp-v1.md`](spec/lpp-v1.md)). - Repository state: initial published contract and conformance suite. - Wright is a client/consumer of the protocol; LPP is not a dependency from the language implementation back into Wright tooling internals. diff --git a/conformance/README.md b/conformance/README.md index 3742ab7..df4d519 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -10,7 +10,7 @@ mock-provider/ Reference provider for the "x-demo-lang" equation DSL (Rus runner/ Conformance runner that replays fixtures against any provider binary ``` -* **Fixtures** (`fixtures/v1/`): one JSON file per scenario. Each scenario defines a session with request/response steps, optional CLI flags, and the expected exit code. Responses are compared after JSON parsing so key order does not matter. The directory contains both LPP 1.0 and LPP 1.1 scenarios. +* **Fixtures** (`fixtures/v1/`): one JSON file per scenario. Each scenario defines a session with request/response steps, optional CLI flags, and the expected exit code. Responses are compared after JSON parsing so key order does not matter. The directory contains LPP 1.0, LPP 1.1, and LPP 1.2 scenarios. * **Mock provider** (`mock-provider/`): a small Rust binary implementing the full LPP v1 surface for a demonstration language distinct from OPY and OSTW. It runs over stdio so clients (like the Wright LPP client in wrightkit/wright#142) can test against it directly. * **Runner** (`runner/`): spawns a fresh provider process per scenario, feeds requests over stdin, validates stdout responses against expectations, and checks the process exit code. diff --git a/conformance/fixtures/v1/42-project-loading-directory-version-gate.json b/conformance/fixtures/v1/42-project-loading-directory-version-gate.json new file mode 100644 index 0000000..b071f83 --- /dev/null +++ b/conformance/fixtures/v1/42-project-loading-directory-version-gate.json @@ -0,0 +1,15 @@ +{ + "name": "project-loading-directory-version-gate", + "description": "A directory target is rejected in an LPP 1.1 session instead of receiving LPP 1.2 directory semantics.", + "scope": "protocol", + "providerArgs": ["--protocol-version", "1.1"], + "projectFiles": { + "entry.xdl": "puzzle entry {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n" + }, + "steps": [ + {"request":{"jsonrpc":"2.0","id":1,"method":"lpp/initialize","params":{"protocolVersion":"1.1"}},"expectResponse":{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"1.1","serverInfo":{"name":"lpp-mock-provider","version":"0.1.0"},"languages":[{"id":"x-demo-lang","extensions":["xdl"]}],"capabilities":{"check":true,"compile":true,"reconstruct":true,"symbols":true,"definition":true,"references":true,"rename":true,"editValidation":true,"projectLoading":true}}}}, + {"request":{"jsonrpc":"2.0","id":2,"method":"lpp/check","params":{"entry":{"uri":"${PROJECT_URI}","languageId":"x-demo-lang","version":7,"kind":"directory"}}},"expectResponse":{"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"directory project targets require protocol version 1.2","data":{"lpp":{"kind":"invalidEntry","details":{"entryUri":"${PROJECT_URI}","reason":"unsupportedKind"}}}}}}, + {"request":{"jsonrpc":"2.0","id":3,"method":"lpp/shutdown","params":{}},"expectResponse":{"jsonrpc":"2.0","id":3,"result":null}} + ], + "expectExitCode": 0 +} diff --git a/conformance/mock-provider/src/main.rs b/conformance/mock-provider/src/main.rs index 00347ca..ad39846 100644 --- a/conformance/mock-provider/src/main.rs +++ b/conformance/mock-provider/src/main.rs @@ -141,14 +141,11 @@ struct ProjectEntry { uri: String, language_id: String, version: i64, - #[serde(default)] - kind: ProjectTargetKind, + kind: Option, } -#[derive(Debug, Deserialize, Default)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone, Copy)] enum ProjectTargetKind { - #[default] File, Directory, } @@ -875,14 +872,41 @@ fn documents_for_request( "project entry version must be a non-negative integer".to_string(), )); } - let (documents, canonical_entry_uri) = load_project(&entry)?; + let target_kind = + project_target_kind(&entry, server.protocol_version.as_deref().unwrap())?; + let (documents, canonical_entry_uri) = load_project(&entry, target_kind)?; Ok((documents, Some(canonical_entry_uri))) } _ => Err(HandlerError::Std(-32602, "Invalid params")), } } -fn load_project(entry: &ProjectEntry) -> Result<(HashMap, String), HandlerError> { +fn project_target_kind( + entry: &ProjectEntry, + protocol_version: &str, +) -> Result { + match entry.kind.as_deref() { + None | Some("file") => Ok(ProjectTargetKind::File), + Some("directory") if protocol_version == PROTOCOL_VERSION_1_2 => { + Ok(ProjectTargetKind::Directory) + } + Some("directory") => Err(HandlerError::Lpp( + "invalidEntry", + json!({ "entryUri": entry.uri, "reason": "unsupportedKind" }), + "directory project targets require protocol version 1.2".to_string(), + )), + Some(_) => Err(HandlerError::Lpp( + "invalidEntry", + json!({ "entryUri": entry.uri, "reason": "unsupportedKind" }), + "project target kind is not supported".to_string(), + )), + } +} + +fn load_project( + entry: &ProjectEntry, + target_kind: ProjectTargetKind, +) -> Result<(HashMap, String), HandlerError> { if entry.language_id != LANGUAGE_ID { return Err(HandlerError::Lpp( "invalidEntry", @@ -901,7 +925,7 @@ fn load_project(entry: &ProjectEntry) -> Result<(HashMap, Stri )); }; let entry_path = std::fs::canonicalize(&entry_path).map_err(|_| { - let (reason, message) = match entry.kind { + let (reason, message) = match target_kind { ProjectTargetKind::File => ("entryNotFound", "project entry could not be loaded"), ProjectTargetKind::Directory => { ("targetNotFound", "project target could not be loaded") @@ -917,7 +941,7 @@ fn load_project(entry: &ProjectEntry) -> Result<(HashMap, Stri message.to_string(), ) })?; - let (project_root, selected_entry) = match entry.kind { + let (project_root, selected_entry) = match target_kind { ProjectTargetKind::File => { if !entry_path.is_file() { return Err(HandlerError::Lpp( @@ -957,7 +981,19 @@ fn load_project(entry: &ProjectEntry) -> Result<(HashMap, Stri "project directory has no default entry".to_string(), )); } - (entry_path, selected.canonicalize().expect("entry exists")) + let selected = selected.canonicalize().map_err(|error| { + let uri = path_to_file_uri(&selected).unwrap_or_else(|| entry.uri.clone()); + HandlerError::Lpp( + "projectLoadFailed", + json!({ + "entryUri": entry.uri, + "reason": "defaultEntryUnreadable", + "uri": uri, + }), + format!("default project entry could not be resolved: {error}"), + ) + })?; + (entry_path, selected) } }; let canonical_entry_uri = path_to_file_uri(&selected_entry).ok_or_else(|| { diff --git a/spec/lpp-v1.md b/spec/lpp-v1.md index f3b6112..d6ccf29 100644 --- a/spec/lpp-v1.md +++ b/spec/lpp-v1.md @@ -298,8 +298,8 @@ directory targets in [Section 8.2](#82-directory-project-requests-lpp-12). ### 6.10 Project entry -A project entry identifies the source file selected by the client for a -provider-owned filesystem project load: +A project entry identifies the source target selected by the client for a +provider-owned filesystem project load. The example below selects a file: ```json { @@ -317,9 +317,10 @@ provider-owned filesystem project load: filesystem snapshot for this request. It is echoed in every source result. It is not a filesystem content hash and does not provide cross-request stale detection. -* `kind`: REQUIRED for LPP 1.2 directory-capable requests and omitted by LPP - 1.0/1.1 clients. It is `"file"` for the existing entry behavior or - `"directory"` when the provider must discover the effective project entry. +* `kind`: OPTIONAL for LPP 1.1 and LPP 1.2. When omitted, it requests the + existing file-entry behavior. LPP 1.2 clients MUST use `"directory"` when + the provider must discover the effective project entry from a directory; + `"file"` may be used explicitly for file-entry behavior. The entry identifies the user's selected source target only. The provider determines the effective project root and source closure according to the @@ -437,8 +438,9 @@ when practical: ``` The client then decides whether to terminate the session or restart with a -supported version. LPP 1.0 clients MUST send `"1.0"`; clients using the -project-loading extension MUST send `"1.1"`. +supported version. LPP 1.0 clients MUST send `"1.0"`; clients using file-entry +project loading MUST send `"1.1"` or `"1.2"`; clients using directory targets +MUST send `"1.2"`. ## 8. Common request parameters @@ -447,13 +449,13 @@ Document-scoped methods share this parameter shape: | Field | Type | Methods | Description | | --- | --- | --- | --- | | `documents` | DocumentSet | `check`, `compile`, `symbols`, `rename` | The documents to operate on. | -| `entry` | Project entry | `check`, `compile` in LPP 1.1 | Alternative to `documents`; asks the provider to load the source closure from the selected entry. | +| `entry` | Project entry | `check`, `compile` in LPP 1.1 and 1.2 | Alternative to `documents`; asks the provider to load the source closure from the selected entry or directory target. | | `document` | Document | `definition`, `references`, `validateEdits` | The single document to operate on. | | `projectRoot` | string, OPTIONAL | `check`, `compile`, `symbols`, `rename` | URI identifying the project the documents belong to. Purely informational in v1; providers MUST accept and MAY use it. | -### 8.1 Entry-based project requests (LPP 1.1) +### 8.1 Entry-based project requests (LPP 1.1 and 1.2) -In LPP 1.1, `lpp/check` and `lpp/compile` accept either `documents` or +In LPP 1.1 and 1.2, `lpp/check` and `lpp/compile` accept either `documents` or `entry`, but not both. An `entry` request is available only when the provider accepted protocol version `1.1` or `1.2` and advertised `projectLoading: true`. The optional `projectRoot` field remains legal and is informational; the @@ -475,12 +477,13 @@ closure from the entry and the source language's rules. } ``` -The provider MUST load the entry and every additional source file required by -the source language's project rules, then perform the requested operation on -that complete source closure. It MUST NOT require the client to list those -files in advance. The provider MUST read only the filesystem project -identified by the entry and MUST NOT treat the client's working directory as a -project root unless that is the source language's documented rule. +For a file target, the provider MUST load the entry and every additional +source file required by the source language's project rules, then perform the +requested operation on that complete source closure. It MUST NOT require the +client to list those files in advance. The provider MUST read only the +filesystem project identified by the entry and MUST NOT treat the client's +working directory as a project root unless that is the source language's +documented rule. The result uses the normal `lpp/check` or `lpp/compile` shape. It MUST include diagnostics for every loaded source document, including documents that contain @@ -520,7 +523,10 @@ Providers MUST return canonical source identities for every loaded document and MUST fail with `projectLoadFailed` if the directory or its language-owned default entry cannot be loaded. A `kind` of `"file"` has the same semantics as the LPP 1.1 entry request. LPP 1.1 clients omit `kind` and therefore always -request file-entry behavior. +request file-entry behavior. A directory target sent in an LPP 1.1 session +MUST be rejected as `invalidEntry`; the provider MUST NOT perform directory +entry selection. Any other `kind` value MUST also be rejected as +`invalidEntry`. ## 9. lpp/check @@ -569,7 +575,7 @@ analyze every document in the set and MUST report all diagnostics found. ## 10. lpp/compile -Compile a document set into a single Workshop artifact. In LPP 1.1, an +Compile a document set into a single Workshop artifact. In LPP 1.1 and 1.2, an entry-based request compiles the provider-loaded source closure as one unit; the `compile.requiresSingleDocument` refusal applies only to a document-supplied request that contains more than one document. From 7dff09baaca9acbece04b059bfaa8b8d694bdc46 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sun, 13 Sep 2026 00:07:12 +0800 Subject: [PATCH 3/4] feat(protocol): return provider source identity Refs wrightkit/wright#317 --- Cargo.lock | 84 +++++++++++++++++++ .../v1/36-project-loading-success.json | 2 +- .../40-project-loading-canonical-entry.json | 2 +- .../41-project-loading-directory-target.json | 2 +- conformance/mock-provider/Cargo.toml | 1 + conformance/mock-provider/src/main.rs | 16 +++- spec/lpp-v1.md | 8 +- 7 files changed, 108 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea4e5b4..b36a42b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,72 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + [[package]] name = "lpp-conformance-runner" version = "0.1.0" @@ -22,6 +82,7 @@ version = "0.1.0" dependencies = [ "serde", "serde_json", + "sha2", ] [[package]] @@ -91,6 +152,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "syn" version = "3.0.3" @@ -102,12 +174,24 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "zmij" version = "1.0.23" diff --git a/conformance/fixtures/v1/36-project-loading-success.json b/conformance/fixtures/v1/36-project-loading-success.json index d745e48..a6d5c63 100644 --- a/conformance/fixtures/v1/36-project-loading-success.json +++ b/conformance/fixtures/v1/36-project-loading-success.json @@ -10,7 +10,7 @@ "steps": [ {"request":{"jsonrpc":"2.0","id":1,"method":"lpp/initialize","params":{"protocolVersion":"1.1"}},"expectResponse":{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"1.1","serverInfo":{"name":"lpp-mock-provider","version":"0.1.0"},"languages":[{"id":"x-demo-lang","extensions":["xdl"]}],"capabilities":{"check":true,"compile":true,"reconstruct":true,"symbols":true,"definition":true,"references":true,"rename":true,"editValidation":true,"projectLoading":true}}}}, {"request":{"jsonrpc":"2.0","id":2,"method":"lpp/check","params":{"entry":{"uri":"${PROJECT_URI}/entry.xdl","languageId":"x-demo-lang","version":7},"projectRoot":"${PROJECT_URI}"}},"expectResponse":{"jsonrpc":"2.0","id":2,"result":{"documents":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]},{"uri":"${PROJECT_URI}/support.xdl","version":7,"diagnostics":[]}]}}}, - {"request":{"jsonrpc":"2.0","id":3,"method":"lpp/compile","params":{"entry":{"uri":"${PROJECT_URI}/entry.xdl","languageId":"x-demo-lang","version":7},"projectRoot":"${PROJECT_URI}"}},"expectResponse":{"jsonrpc":"2.0","id":3,"result":{"diagnostics":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]},{"uri":"${PROJECT_URI}/support.xdl","version":7,"diagnostics":[]}],"artifact":{"format":"x-demo/puzzle-eval-v1","content":"{\"name\":\"entry\",\"ops\":[{\"arg\":2,\"name\":\"double\",\"op\":\"*\"},{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"double\",\"double\"],\"start\":10,\"target\":40,\"value\":40}"}}}}, + {"request":{"jsonrpc":"2.0","id":3,"method":"lpp/compile","params":{"entry":{"uri":"${PROJECT_URI}/entry.xdl","languageId":"x-demo-lang","version":7},"projectRoot":"${PROJECT_URI}"}},"expectResponse":{"jsonrpc":"2.0","id":3,"result":{"diagnostics":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]},{"uri":"${PROJECT_URI}/support.xdl","version":7,"diagnostics":[]}],"sourceIdentity":"7666c532326b88d133426337a297ada665086b3f66b9455cad59e1ed48c669fd","artifact":{"format":"x-demo/puzzle-eval-v1","content":"{\"name\":\"entry\",\"ops\":[{\"arg\":2,\"name\":\"double\",\"op\":\"*\"},{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"double\",\"double\"],\"start\":10,\"target\":40,\"value\":40}"}}}}, {"request":{"jsonrpc":"2.0","id":4,"method":"lpp/shutdown","params":{}},"expectResponse":{"jsonrpc":"2.0","id":4,"result":null}} ], "expectExitCode": 0 diff --git a/conformance/fixtures/v1/40-project-loading-canonical-entry.json b/conformance/fixtures/v1/40-project-loading-canonical-entry.json index 48b3ff5..10833ac 100644 --- a/conformance/fixtures/v1/40-project-loading-canonical-entry.json +++ b/conformance/fixtures/v1/40-project-loading-canonical-entry.json @@ -9,7 +9,7 @@ "steps": [ {"request":{"jsonrpc":"2.0","id":1,"method":"lpp/initialize","params":{"protocolVersion":"1.1"}},"expectResponse":{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"1.1","serverInfo":{"name":"lpp-mock-provider","version":"0.1.0"},"languages":[{"id":"x-demo-lang","extensions":["xdl"]}],"capabilities":{"check":true,"compile":true,"reconstruct":true,"symbols":true,"definition":true,"references":true,"rename":true,"editValidation":true,"projectLoading":true}}}}, {"request":{"jsonrpc":"2.0","id":2,"method":"lpp/check","params":{"entry":{"uri":"${PROJECT_URI}/./entry.xdl","languageId":"x-demo-lang","version":7}}},"expectResponse":{"jsonrpc":"2.0","id":2,"result":{"documents":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]}]}}}, - {"request":{"jsonrpc":"2.0","id":3,"method":"lpp/compile","params":{"entry":{"uri":"${PROJECT_URI}/./entry.xdl","languageId":"x-demo-lang","version":7}}},"expectResponse":{"jsonrpc":"2.0","id":3,"result":{"diagnostics":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]}],"artifact":{"format":"x-demo/puzzle-eval-v1","content":"{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}}}, + {"request":{"jsonrpc":"2.0","id":3,"method":"lpp/compile","params":{"entry":{"uri":"${PROJECT_URI}/./entry.xdl","languageId":"x-demo-lang","version":7}}},"expectResponse":{"jsonrpc":"2.0","id":3,"result":{"diagnostics":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]}],"sourceIdentity":"a92a4daf6631cbb574095cef0296d3d8217459a48e7899f98d832bb63f510822","artifact":{"format":"x-demo/puzzle-eval-v1","content":"{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}}}, {"request":{"jsonrpc":"2.0","id":4,"method":"lpp/shutdown","params":{}},"expectResponse":{"jsonrpc":"2.0","id":4,"result":null}} ], "expectExitCode": 0 diff --git a/conformance/fixtures/v1/41-project-loading-directory-target.json b/conformance/fixtures/v1/41-project-loading-directory-target.json index 2c5570f..7dfa760 100644 --- a/conformance/fixtures/v1/41-project-loading-directory-target.json +++ b/conformance/fixtures/v1/41-project-loading-directory-target.json @@ -18,7 +18,7 @@ }, { "request": {"jsonrpc": "2.0", "id": 3, "method": "lpp/compile", "params": {"entry": {"uri": "${PROJECT_URI}", "languageId": "x-demo-lang", "version": 7, "kind": "directory"}}}, - "expectResponse": {"jsonrpc": "2.0", "id": 3, "result": {"diagnostics": [{"uri": "${PROJECT_URI}/entry.xdl", "version": 7, "diagnostics": []}, {"uri": "${PROJECT_URI}/support.xdl", "version": 7, "diagnostics": []}], "artifact": {"format": "x-demo/puzzle-eval-v1", "content": "{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}} + "expectResponse": {"jsonrpc": "2.0", "id": 3, "result": {"diagnostics": [{"uri": "${PROJECT_URI}/entry.xdl", "version": 7, "diagnostics": []}, {"uri": "${PROJECT_URI}/support.xdl", "version": 7, "diagnostics": []}], "sourceIdentity": "a92a4daf6631cbb574095cef0296d3d8217459a48e7899f98d832bb63f510822", "artifact": {"format": "x-demo/puzzle-eval-v1", "content": "{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}} }, { "request": {"jsonrpc": "2.0", "id": 4, "method": "lpp/shutdown", "params": {}}, diff --git a/conformance/mock-provider/Cargo.toml b/conformance/mock-provider/Cargo.toml index b8ae3df..ec83e5c 100644 --- a/conformance/mock-provider/Cargo.toml +++ b/conformance/mock-provider/Cargo.toml @@ -8,6 +8,7 @@ description = "LPP v1 conformance mock provider for the x-demo-lang puzzle/equat publish = false [dependencies] +sha2 = "0.10" serde = { workspace = true, features = ["derive"] } serde_json.workspace = true diff --git a/conformance/mock-provider/src/main.rs b/conformance/mock-provider/src/main.rs index ad39846..c636e1b 100644 --- a/conformance/mock-provider/src/main.rs +++ b/conformance/mock-provider/src/main.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; use serde::Deserialize; use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; use puzzle::{ ARTIFACT_FORMAT, KIND_OP, KIND_PUZZLE, ParseOutput, Range, SourceText, compile_artifact, @@ -488,7 +489,7 @@ impl Server { )); } let doc = match entry_uri { - Some(uri) => documents_set.get(&uri).expect("loaded entry is present"), + Some(ref uri) => documents_set.get(uri).expect("loaded entry is present"), None => documents_set.values().next().expect("len == 1"), }; check_document(doc)?; @@ -515,10 +516,19 @@ impl Server { .expect("artifact serializes"); json!({ "format": ARTIFACT_FORMAT, "content": content }) }; - Ok(json!({ + let source_identity = entry_uri.as_ref().map(|uri| { + let mut hasher = Sha256::new(); + hasher.update(documents_set[uri].text.as_bytes()); + format!("{:x}", hasher.finalize()) + }); + let mut result = json!({ "diagnostics": diagnostics, "artifact": artifact, - })) + }); + if let Some(source_identity) = source_identity { + result["sourceIdentity"] = Value::String(source_identity); + } + Ok(result) } fn reconstruct(&self, params: Value) -> Result { diff --git a/spec/lpp-v1.md b/spec/lpp-v1.md index d6ccf29..09c6050 100644 --- a/spec/lpp-v1.md +++ b/spec/lpp-v1.md @@ -601,11 +601,17 @@ document-supplied request that contains more than one document. "diagnostics": [ { "uri": "file:///project/puzzle.xdl", "version": 3, "diagnostics": [] } ], + "sourceIdentity": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "artifact": { "format": "x-demo/puzzle-eval-v1", "content": "{\"name\":\"clean\",\"ops\":[{\"arg\":2,\"name\":\"double\",\"op\":\"*\"},{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"double\",\"double\"],\"start\":10,\"target\":40,\"value\":40}" } } ``` * `diagnostics`: same shape as the `lpp/check` result. +* `sourceIdentity`: an OPTIONAL lower-case SHA-256 hex digest of the + provider-selected primary source text. For an entry-based request, the + provider MUST return this identity when the client requires source identity + for the resolved project target; the provider owns effective entry + selection. Document-supplied requests MAY omit it. * `artifact`: the compiled Workshop artifact, or `null`. * The `artifact` MUST be `null` whenever any error-severity diagnostic is reported. The provider MAY return `null` artifact in other failure cases. @@ -1059,7 +1065,7 @@ Methods: | `lpp/initialize` | none | `{ protocolVersion, clientInfo? }` | `{ protocolVersion, serverInfo, languages, capabilities }` | | `lpp/shutdown` | none | `{}` | `null` | | `lpp/check` | `check`; plus `projectLoading` for an LPP 1.1/1.2 `entry` request | `{ documents, projectRoot? }` or `{ entry, projectRoot? }` | `{ documents: [{ uri, version, diagnostics }] }` | -| `lpp/compile` | `compile`; plus `projectLoading` for an LPP 1.1/1.2 `entry` request | `{ documents, projectRoot? }` or `{ entry, projectRoot? }` | `{ diagnostics: [{ uri, version, diagnostics }], artifact }` | +| `lpp/compile` | `compile`; plus `projectLoading` for an LPP 1.1/1.2 `entry` request | `{ documents, projectRoot? }` or `{ entry, projectRoot? }` | `{ diagnostics: [{ uri, version, diagnostics }], sourceIdentity?, artifact }` | | `lpp/reconstruct` | `reconstruct` | `{ artifact }` | `{ source, uri? }` | | `lpp/symbols` | `symbols` | `{ documents, projectRoot? }` | `{ documents: [{ uri, version, symbols }] }` | | `lpp/definition` | `definition` | `{ document, position }` | `{ locations }` | From bdf20a29e33d373b1fb386f42b021cb5e7d0ce3c Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sun, 13 Sep 2026 04:07:41 +0800 Subject: [PATCH 4/4] fix(protocol): negotiate source identity in 1.3 Preserve the released LPP 1.1 and 1.2 compile response shapes while adding an advertised sourceIdentity capability for LPP 1.3 entry-based compilation. --- README.md | 2 +- conformance/README.md | 4 +- .../v1/36-project-loading-success.json | 2 +- .../40-project-loading-canonical-entry.json | 2 +- .../41-project-loading-directory-target.json | 2 +- .../43-project-loading-source-identity.json | 16 +++++ ...t-loading-source-identity-unavailable.json | 16 +++++ conformance/mock-provider/src/main.rs | 30 ++++++-- spec/lpp-v1.md | 69 +++++++++++-------- 9 files changed, 102 insertions(+), 41 deletions(-) create mode 100644 conformance/fixtures/v1/43-project-loading-source-identity.json create mode 100644 conformance/fixtures/v1/44-project-loading-source-identity-unavailable.json diff --git a/README.md b/README.md index 31fbda5..5ce2156 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ its frontend, compiler, reconstruction, or Workshop integration internally. ## Status -- Protocol versions: **1.0**, additive **1.1** file-entry loading, and additive **1.2** directory targets (specified in [`spec/lpp-v1.md`](spec/lpp-v1.md)). +- Protocol versions: **1.0**, additive **1.1** file-entry loading, additive **1.2** directory targets, and additive **1.3** source identity for entry-based compilation (specified in [`spec/lpp-v1.md`](spec/lpp-v1.md)). - Repository state: initial published contract and conformance suite. - Wright is a client/consumer of the protocol; LPP is not a dependency from the language implementation back into Wright tooling internals. diff --git a/conformance/README.md b/conformance/README.md index df4d519..1674e69 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -1,6 +1,6 @@ # LPP v1 Conformance Suite -This directory contains the test suite and fixtures for the Language Provider Protocol v1 wire contract, including the LPP 1.1 file-entry and LPP 1.2 directory-target project-loading revisions (see [`../spec/lpp-v1.md`](../spec/lpp-v1.md)). +This directory contains the test suite and fixtures for the Language Provider Protocol v1 wire contract, including the LPP 1.1 file-entry, LPP 1.2 directory-target project-loading, and LPP 1.3 source-identity revisions (see [`../spec/lpp-v1.md`](../spec/lpp-v1.md)). ## Layout @@ -10,7 +10,7 @@ mock-provider/ Reference provider for the "x-demo-lang" equation DSL (Rus runner/ Conformance runner that replays fixtures against any provider binary ``` -* **Fixtures** (`fixtures/v1/`): one JSON file per scenario. Each scenario defines a session with request/response steps, optional CLI flags, and the expected exit code. Responses are compared after JSON parsing so key order does not matter. The directory contains LPP 1.0, LPP 1.1, and LPP 1.2 scenarios. +* **Fixtures** (`fixtures/v1/`): one JSON file per scenario. Each scenario defines a session with request/response steps, optional CLI flags, and the expected exit code. Responses are compared after JSON parsing so key order does not matter. The directory contains LPP 1.0, LPP 1.1, LPP 1.2, and LPP 1.3 scenarios. * **Mock provider** (`mock-provider/`): a small Rust binary implementing the full LPP v1 surface for a demonstration language distinct from OPY and OSTW. It runs over stdio so clients (like the Wright LPP client in wrightkit/wright#142) can test against it directly. * **Runner** (`runner/`): spawns a fresh provider process per scenario, feeds requests over stdin, validates stdout responses against expectations, and checks the process exit code. diff --git a/conformance/fixtures/v1/36-project-loading-success.json b/conformance/fixtures/v1/36-project-loading-success.json index a6d5c63..d745e48 100644 --- a/conformance/fixtures/v1/36-project-loading-success.json +++ b/conformance/fixtures/v1/36-project-loading-success.json @@ -10,7 +10,7 @@ "steps": [ {"request":{"jsonrpc":"2.0","id":1,"method":"lpp/initialize","params":{"protocolVersion":"1.1"}},"expectResponse":{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"1.1","serverInfo":{"name":"lpp-mock-provider","version":"0.1.0"},"languages":[{"id":"x-demo-lang","extensions":["xdl"]}],"capabilities":{"check":true,"compile":true,"reconstruct":true,"symbols":true,"definition":true,"references":true,"rename":true,"editValidation":true,"projectLoading":true}}}}, {"request":{"jsonrpc":"2.0","id":2,"method":"lpp/check","params":{"entry":{"uri":"${PROJECT_URI}/entry.xdl","languageId":"x-demo-lang","version":7},"projectRoot":"${PROJECT_URI}"}},"expectResponse":{"jsonrpc":"2.0","id":2,"result":{"documents":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]},{"uri":"${PROJECT_URI}/support.xdl","version":7,"diagnostics":[]}]}}}, - {"request":{"jsonrpc":"2.0","id":3,"method":"lpp/compile","params":{"entry":{"uri":"${PROJECT_URI}/entry.xdl","languageId":"x-demo-lang","version":7},"projectRoot":"${PROJECT_URI}"}},"expectResponse":{"jsonrpc":"2.0","id":3,"result":{"diagnostics":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]},{"uri":"${PROJECT_URI}/support.xdl","version":7,"diagnostics":[]}],"sourceIdentity":"7666c532326b88d133426337a297ada665086b3f66b9455cad59e1ed48c669fd","artifact":{"format":"x-demo/puzzle-eval-v1","content":"{\"name\":\"entry\",\"ops\":[{\"arg\":2,\"name\":\"double\",\"op\":\"*\"},{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"double\",\"double\"],\"start\":10,\"target\":40,\"value\":40}"}}}}, + {"request":{"jsonrpc":"2.0","id":3,"method":"lpp/compile","params":{"entry":{"uri":"${PROJECT_URI}/entry.xdl","languageId":"x-demo-lang","version":7},"projectRoot":"${PROJECT_URI}"}},"expectResponse":{"jsonrpc":"2.0","id":3,"result":{"diagnostics":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]},{"uri":"${PROJECT_URI}/support.xdl","version":7,"diagnostics":[]}],"artifact":{"format":"x-demo/puzzle-eval-v1","content":"{\"name\":\"entry\",\"ops\":[{\"arg\":2,\"name\":\"double\",\"op\":\"*\"},{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"double\",\"double\"],\"start\":10,\"target\":40,\"value\":40}"}}}}, {"request":{"jsonrpc":"2.0","id":4,"method":"lpp/shutdown","params":{}},"expectResponse":{"jsonrpc":"2.0","id":4,"result":null}} ], "expectExitCode": 0 diff --git a/conformance/fixtures/v1/40-project-loading-canonical-entry.json b/conformance/fixtures/v1/40-project-loading-canonical-entry.json index 10833ac..48b3ff5 100644 --- a/conformance/fixtures/v1/40-project-loading-canonical-entry.json +++ b/conformance/fixtures/v1/40-project-loading-canonical-entry.json @@ -9,7 +9,7 @@ "steps": [ {"request":{"jsonrpc":"2.0","id":1,"method":"lpp/initialize","params":{"protocolVersion":"1.1"}},"expectResponse":{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"1.1","serverInfo":{"name":"lpp-mock-provider","version":"0.1.0"},"languages":[{"id":"x-demo-lang","extensions":["xdl"]}],"capabilities":{"check":true,"compile":true,"reconstruct":true,"symbols":true,"definition":true,"references":true,"rename":true,"editValidation":true,"projectLoading":true}}}}, {"request":{"jsonrpc":"2.0","id":2,"method":"lpp/check","params":{"entry":{"uri":"${PROJECT_URI}/./entry.xdl","languageId":"x-demo-lang","version":7}}},"expectResponse":{"jsonrpc":"2.0","id":2,"result":{"documents":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]}]}}}, - {"request":{"jsonrpc":"2.0","id":3,"method":"lpp/compile","params":{"entry":{"uri":"${PROJECT_URI}/./entry.xdl","languageId":"x-demo-lang","version":7}}},"expectResponse":{"jsonrpc":"2.0","id":3,"result":{"diagnostics":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]}],"sourceIdentity":"a92a4daf6631cbb574095cef0296d3d8217459a48e7899f98d832bb63f510822","artifact":{"format":"x-demo/puzzle-eval-v1","content":"{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}}}, + {"request":{"jsonrpc":"2.0","id":3,"method":"lpp/compile","params":{"entry":{"uri":"${PROJECT_URI}/./entry.xdl","languageId":"x-demo-lang","version":7}}},"expectResponse":{"jsonrpc":"2.0","id":3,"result":{"diagnostics":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]}],"artifact":{"format":"x-demo/puzzle-eval-v1","content":"{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}}}, {"request":{"jsonrpc":"2.0","id":4,"method":"lpp/shutdown","params":{}},"expectResponse":{"jsonrpc":"2.0","id":4,"result":null}} ], "expectExitCode": 0 diff --git a/conformance/fixtures/v1/41-project-loading-directory-target.json b/conformance/fixtures/v1/41-project-loading-directory-target.json index 7dfa760..2c5570f 100644 --- a/conformance/fixtures/v1/41-project-loading-directory-target.json +++ b/conformance/fixtures/v1/41-project-loading-directory-target.json @@ -18,7 +18,7 @@ }, { "request": {"jsonrpc": "2.0", "id": 3, "method": "lpp/compile", "params": {"entry": {"uri": "${PROJECT_URI}", "languageId": "x-demo-lang", "version": 7, "kind": "directory"}}}, - "expectResponse": {"jsonrpc": "2.0", "id": 3, "result": {"diagnostics": [{"uri": "${PROJECT_URI}/entry.xdl", "version": 7, "diagnostics": []}, {"uri": "${PROJECT_URI}/support.xdl", "version": 7, "diagnostics": []}], "sourceIdentity": "a92a4daf6631cbb574095cef0296d3d8217459a48e7899f98d832bb63f510822", "artifact": {"format": "x-demo/puzzle-eval-v1", "content": "{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}} + "expectResponse": {"jsonrpc": "2.0", "id": 3, "result": {"diagnostics": [{"uri": "${PROJECT_URI}/entry.xdl", "version": 7, "diagnostics": []}, {"uri": "${PROJECT_URI}/support.xdl", "version": 7, "diagnostics": []}], "artifact": {"format": "x-demo/puzzle-eval-v1", "content": "{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}} }, { "request": {"jsonrpc": "2.0", "id": 4, "method": "lpp/shutdown", "params": {}}, diff --git a/conformance/fixtures/v1/43-project-loading-source-identity.json b/conformance/fixtures/v1/43-project-loading-source-identity.json new file mode 100644 index 0000000..b8443d5 --- /dev/null +++ b/conformance/fixtures/v1/43-project-loading-source-identity.json @@ -0,0 +1,16 @@ +{ + "name": "project-loading-source-identity", + "description": "LPP 1.3 advertises source identity and returns it for an owner-selected directory compile target.", + "scope": "semantics", + "providerArgs": ["--protocol-version", "1.3"], + "projectFiles": { + "entry.xdl": "puzzle entry {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n", + "support.xdl": "puzzle support {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n" + }, + "steps": [ + {"request":{"jsonrpc":"2.0","id":1,"method":"lpp/initialize","params":{"protocolVersion":"1.3"}},"expectResponse":{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"1.3","serverInfo":{"name":"lpp-mock-provider","version":"0.1.0"},"languages":[{"id":"x-demo-lang","extensions":["xdl"]}],"capabilities":{"check":true,"compile":true,"reconstruct":true,"symbols":true,"definition":true,"references":true,"rename":true,"editValidation":true,"projectLoading":true,"sourceIdentity":true}}}}, + {"request":{"jsonrpc":"2.0","id":2,"method":"lpp/compile","params":{"entry":{"uri":"${PROJECT_URI}","languageId":"x-demo-lang","version":7,"kind":"directory"}}},"expectResponse":{"jsonrpc":"2.0","id":2,"result":{"diagnostics":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]},{"uri":"${PROJECT_URI}/support.xdl","version":7,"diagnostics":[]}],"sourceIdentity":"a92a4daf6631cbb574095cef0296d3d8217459a48e7899f98d832bb63f510822","artifact":{"format":"x-demo/puzzle-eval-v1","content":"{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}}}, + {"request":{"jsonrpc":"2.0","id":3,"method":"lpp/shutdown","params":{}},"expectResponse":{"jsonrpc":"2.0","id":3,"result":null}} + ], + "expectExitCode": 0 +} diff --git a/conformance/fixtures/v1/44-project-loading-source-identity-unavailable.json b/conformance/fixtures/v1/44-project-loading-source-identity-unavailable.json new file mode 100644 index 0000000..4616e53 --- /dev/null +++ b/conformance/fixtures/v1/44-project-loading-source-identity-unavailable.json @@ -0,0 +1,16 @@ +{ + "name": "project-loading-source-identity-unavailable", + "description": "An LPP 1.3 provider may decline source identity independently; entry compilation remains available without the optional field.", + "scope": "protocol", + "providerArgs": ["--protocol-version", "1.3", "--without", "sourceIdentity"], + "projectFiles": { + "entry.xdl": "puzzle entry {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n", + "support.xdl": "puzzle support {\n target = 2\n start = 1\n ops {\n plus1: x => x + 1\n }\n solution = [ plus1 ]\n}\n" + }, + "steps": [ + {"request":{"jsonrpc":"2.0","id":1,"method":"lpp/initialize","params":{"protocolVersion":"1.3"}},"expectResponse":{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"1.3","serverInfo":{"name":"lpp-mock-provider","version":"0.1.0"},"languages":[{"id":"x-demo-lang","extensions":["xdl"]}],"capabilities":{"check":true,"compile":true,"reconstruct":true,"symbols":true,"definition":true,"references":true,"rename":true,"editValidation":true,"projectLoading":true,"sourceIdentity":false}}}}, + {"request":{"jsonrpc":"2.0","id":2,"method":"lpp/compile","params":{"entry":{"uri":"${PROJECT_URI}","languageId":"x-demo-lang","version":7,"kind":"directory"}}},"expectResponse":{"jsonrpc":"2.0","id":2,"result":{"diagnostics":[{"uri":"${PROJECT_URI}/entry.xdl","version":7,"diagnostics":[]},{"uri":"${PROJECT_URI}/support.xdl","version":7,"diagnostics":[]}],"artifact":{"format":"x-demo/puzzle-eval-v1","content":"{\"name\":\"entry\",\"ops\":[{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"plus1\"],\"start\":1,\"target\":2,\"value\":2}"}}}}, + {"request":{"jsonrpc":"2.0","id":3,"method":"lpp/shutdown","params":{}},"expectResponse":{"jsonrpc":"2.0","id":3,"result":null}} + ], + "expectExitCode": 0 +} diff --git a/conformance/mock-provider/src/main.rs b/conformance/mock-provider/src/main.rs index c636e1b..939e5e2 100644 --- a/conformance/mock-provider/src/main.rs +++ b/conformance/mock-provider/src/main.rs @@ -16,6 +16,7 @@ use puzzle::{ const DEFAULT_PROTOCOL_VERSION: &str = "1.0"; const PROTOCOL_VERSION_1_1: &str = "1.1"; const PROTOCOL_VERSION_1_2: &str = "1.2"; +const PROTOCOL_VERSION_1_3: &str = "1.3"; const SERVER_NAME: &str = "lpp-mock-provider"; const LANGUAGE_ID: &str = "x-demo-lang"; const LANGUAGE_EXTENSIONS: [&str; 1] = ["xdl"]; @@ -31,6 +32,7 @@ struct Capabilities { rename: bool, edit_validation: bool, project_loading: bool, + source_identity: bool, } impl Capabilities { @@ -45,6 +47,7 @@ impl Capabilities { rename: true, edit_validation: true, project_loading: true, + source_identity: true, } } @@ -59,6 +62,7 @@ impl Capabilities { "rename" => &mut self.rename, "editValidation" => &mut self.edit_validation, "projectLoading" => &mut self.project_loading, + "sourceIdentity" => &mut self.source_identity, _ => return false, }; *field = false; @@ -76,6 +80,7 @@ impl Capabilities { "rename" => self.rename, "editValidation" => self.edit_validation, "projectLoading" => self.project_loading, + "sourceIdentity" => self.source_identity, _ => false, } } @@ -93,10 +98,13 @@ impl Capabilities { }); if matches!( protocol_version, - PROTOCOL_VERSION_1_1 | PROTOCOL_VERSION_1_2 + PROTOCOL_VERSION_1_1 | PROTOCOL_VERSION_1_2 | PROTOCOL_VERSION_1_3 ) { capabilities["projectLoading"] = json!(self.project_loading); } + if protocol_version == PROTOCOL_VERSION_1_3 { + capabilities["sourceIdentity"] = json!(self.source_identity); + } capabilities } } @@ -291,6 +299,7 @@ fn parse_args() -> (String, Capabilities) { if version != DEFAULT_PROTOCOL_VERSION && version != PROTOCOL_VERSION_1_1 && version != PROTOCOL_VERSION_1_2 + && version != PROTOCOL_VERSION_1_3 { eprintln!("lpp-mock-provider: unsupported protocol version '{version}'"); std::process::exit(2); @@ -525,8 +534,12 @@ impl Server { "diagnostics": diagnostics, "artifact": artifact, }); - if let Some(source_identity) = source_identity { - result["sourceIdentity"] = Value::String(source_identity); + if self.protocol_version.as_deref() == Some(PROTOCOL_VERSION_1_3) + && self.caps.source_identity + { + if let Some(source_identity) = source_identity { + result["sourceIdentity"] = Value::String(source_identity); + } } Ok(result) } @@ -864,7 +877,9 @@ fn documents_for_request( (None, Some(entry)) => { if !matches!( server.protocol_version.as_deref(), - Some(PROTOCOL_VERSION_1_1) | Some(PROTOCOL_VERSION_1_2) + Some(PROTOCOL_VERSION_1_1) + | Some(PROTOCOL_VERSION_1_2) + | Some(PROTOCOL_VERSION_1_3) ) { return Err(HandlerError::Std(-32602, "Invalid params")); } @@ -897,7 +912,12 @@ fn project_target_kind( ) -> Result { match entry.kind.as_deref() { None | Some("file") => Ok(ProjectTargetKind::File), - Some("directory") if protocol_version == PROTOCOL_VERSION_1_2 => { + Some("directory") + if matches!( + protocol_version, + PROTOCOL_VERSION_1_2 | PROTOCOL_VERSION_1_3 + ) => + { Ok(ProjectTargetKind::Directory) } Some("directory") => Err(HandlerError::Lpp( diff --git a/spec/lpp-v1.md b/spec/lpp-v1.md index 2ba8830..202b5db 100644 --- a/spec/lpp-v1.md +++ b/spec/lpp-v1.md @@ -2,7 +2,7 @@ | | | | --- | --- | -| Protocol versions | `1.0`, `1.1`, `1.2` | +| Protocol versions | `1.0`, `1.1`, `1.2`, `1.3` | | Status | Normative for protocol major version 1 | | Transport | JSON-RPC 2.0 over stdio, newline-delimited framing | | Conformance | `conformance/fixtures/v1/` + `conformance/runner` + `conformance/mock-provider` | @@ -321,8 +321,8 @@ provider-owned filesystem project load. The example below selects a file: filesystem snapshot for this request. It is echoed in every source result. It is not a filesystem content hash and does not provide cross-request stale detection. -* `kind`: OPTIONAL for LPP 1.1 and LPP 1.2. When omitted, it requests the - existing file-entry behavior. LPP 1.2 clients MUST use `"directory"` when +* `kind`: OPTIONAL for LPP 1.1, LPP 1.2, and LPP 1.3. When omitted, it requests the + existing file-entry behavior. LPP 1.2 and 1.3 clients MUST use `"directory"` when the provider must discover the effective project entry from a directory; `"file"` may be used explicitly for file-entry behavior. @@ -351,7 +351,7 @@ Initialization and capability negotiation. The client MUST send | Field | Type | Description | | --- | --- | --- | -| `protocolVersion` | string | The protocol version the client wants to speak: `"1.0"`, `"1.1"`, or `"1.2"`. | +| `protocolVersion` | string | The protocol version the client wants to speak: `"1.0"`, `"1.1"`, `"1.2"`, or `"1.3"`. | | `clientInfo` | object, OPTIONAL | `{ "name": string, "version": string }` identifying the client. | ### 7.2 Result @@ -378,10 +378,10 @@ Initialization and capability negotiation. The client MUST send | Field | Type | Description | | --- | --- | --- | -| `protocolVersion` | string | The protocol version the provider will speak: `"1.0"`, `"1.1"`, or `"1.2"`. | +| `protocolVersion` | string | The protocol version the provider will speak: `"1.0"`, `"1.1"`, `"1.2"`, or `"1.3"`. | | `serverInfo` | object | `{ "name": string, "version": string }` identifying the provider. | | `languages` | array | One entry per source language the provider serves. | -| `capabilities` | object | One boolean field per capability. LPP 1.0 requires the eight fields listed below; LPP 1.1 and 1.2 additionally require `projectLoading`. | +| `capabilities` | object | One boolean field per capability. LPP 1.0 requires the eight fields listed below; LPP 1.1, 1.2, and 1.3 additionally require `projectLoading`; LPP 1.3 also defines `sourceIdentity`. | Each language entry: `{ "id": string, "extensions": [string] }`. `extensions` is the list of file extensions the provider associates with the language, @@ -403,7 +403,8 @@ extension. | `references` | `lpp/references` | Find references to the symbol at a position. | | `rename` | `lpp/rename` | Compute source edits for a semantic rename. | | `editValidation` | `lpp/validateEdits` | Validate a set of source edits against a document. | -| `projectLoading` | `lpp/check`, `lpp/compile` | Accept a client-selected entry or directory target and load its filesystem-backed source project. LPP 1.1 and 1.2. | +| `projectLoading` | `lpp/check`, `lpp/compile` | Accept a client-selected entry or directory target and load its filesystem-backed source project. LPP 1.1, 1.2, and 1.3. | +| `sourceIdentity` | `lpp/compile` | Return the provider-selected primary source identity for an entry-based compile result. LPP 1.3. | * The provider MUST set each capability to `true` only if it fully implements the corresponding method(s). @@ -443,8 +444,10 @@ when practical: The client then decides whether to terminate the session or restart with a supported version. LPP 1.0 clients MUST send `"1.0"`; clients using file-entry -project loading MUST send `"1.1"` or `"1.2"`; clients using directory targets -MUST send `"1.2"`. +project loading MUST send `"1.1"`, `"1.2"`, or `"1.3"`; clients using directory +targets MUST send `"1.2"` or `"1.3"`. A client that requires the +`sourceIdentity` capability MUST request `"1.3"` and require the provider to +advertise `sourceIdentity: true` before using entry-based compile results. ## 8. Common request parameters @@ -453,15 +456,16 @@ Document-scoped methods share this parameter shape: | Field | Type | Methods | Description | | --- | --- | --- | --- | | `documents` | DocumentSet | `check`, `compile`, `symbols`, `rename` | The documents to operate on. | -| `entry` | Project entry | `check`, `compile` in LPP 1.1 and 1.2 | Alternative to `documents`; asks the provider to load the source closure from the selected entry or directory target. | +| `entry` | Project entry | `check`, `compile` in LPP 1.1, 1.2, and 1.3 | Alternative to `documents`; asks the provider to load the source closure from the selected entry or directory target. | | `document` | Document | `definition`, `references`, `validateEdits` | The single document to operate on. | | `projectRoot` | string, OPTIONAL | `check`, `compile`, `symbols`, `rename` | URI identifying the project the documents belong to. Purely informational in v1; providers MUST accept and MAY use it. | -### 8.1 Entry-based project requests (LPP 1.1 and 1.2) +### 8.1 Entry-based project requests (LPP 1.1, 1.2, and 1.3) -In LPP 1.1 and 1.2, `lpp/check` and `lpp/compile` accept either `documents` or +In LPP 1.1, 1.2, and 1.3, `lpp/check` and `lpp/compile` accept either `documents` or `entry`, but not both. An `entry` request is available only when the provider -accepted protocol version `1.1` or `1.2` and advertised `projectLoading: true`. +accepted protocol version `1.1`, `1.2`, or `1.3` and advertised +`projectLoading: true`. The optional `projectRoot` field remains legal and is informational; the provider accepts it but determines the effective project root and source closure from the entry and the source language's rules. @@ -504,9 +508,9 @@ an LPP error of kind `projectLoadFailed`. The `details` object MUST contain `entryUri` and a provider-defined `reason`; a required-file failure SHOULD also include the affected `uri`. -### 8.2 Directory project requests (LPP 1.2) +### 8.2 Directory project requests (LPP 1.2 and 1.3) -LPP 1.2 extends the `entry` object with `kind: "directory"`: +LPP 1.2 and 1.3 extend the `entry` object with `kind: "directory"`: ```json { @@ -579,8 +583,9 @@ analyze every document in the set and MUST report all diagnostics found. ## 10. lpp/compile -Compile a document set into a single Workshop artifact. In LPP 1.1 and 1.2, an -entry-based request compiles the provider-loaded source closure as one unit; +Compile a document set into a single Workshop artifact. In LPP 1.1, 1.2, and +1.3, an entry-based request compiles the provider-loaded source closure as one +unit; the `compile.requiresSingleDocument` refusal applies only to a document-supplied request that contains more than one document. @@ -605,17 +610,18 @@ document-supplied request that contains more than one document. "diagnostics": [ { "uri": "file:///project/puzzle.xdl", "version": 3, "diagnostics": [] } ], - "sourceIdentity": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "artifact": { "format": "x-demo/puzzle-eval-v1", "content": "{\"name\":\"clean\",\"ops\":[{\"arg\":2,\"name\":\"double\",\"op\":\"*\"},{\"arg\":1,\"name\":\"plus1\",\"op\":\"+\"}],\"solution\":[\"double\",\"double\"],\"start\":10,\"target\":40,\"value\":40}" } } ``` * `diagnostics`: same shape as the `lpp/check` result. -* `sourceIdentity`: an OPTIONAL lower-case SHA-256 hex digest of the - provider-selected primary source text. For an entry-based request, the - provider MUST return this identity when the client requires source identity - for the resolved project target; the provider owns effective entry - selection. Document-supplied requests MAY omit it. +* `sourceIdentity`: defined only in LPP 1.3. In an LPP 1.3 session, the + provider MUST advertise the `sourceIdentity` capability. If it advertises + `sourceIdentity: true`, an entry-based compile result MUST include a + lower-case SHA-256 hex digest of the provider-selected primary source text. + The provider owns effective entry selection. If the capability is false, the + field MUST be omitted. Document-supplied requests MAY omit it. LPP 1.1 and + 1.2 compile results MUST NOT include this field. * `artifact`: the compiled Workshop artifact, or `null`. * The `artifact` MUST be `null` whenever any error-severity diagnostic is reported. The provider MAY return `null` artifact in other failure cases. @@ -1001,7 +1007,8 @@ clients MUST NOT parse `message`. * Protocol versions are strings of the form `MAJOR.MINOR` (for example `"1.0"`). LPP 1.0 is the first published version; LPP 1.1 adds file-entry - project loading and LPP 1.2 adds directory targets. + project loading, LPP 1.2 adds directory targets, and LPP 1.3 adds the + optional `sourceIdentity` capability for entry-based compile results. * `MAJOR` changes are breaking: message shapes, method semantics, or framing may change. A breaking change always produces a new MAJOR version, and clients and providers speaking different MAJOR versions are never expected @@ -1018,9 +1025,10 @@ clients MUST NOT parse `message`. * The provider either accepts it (echoing the version in the result) or fails with `protocolVersionMismatch` listing `supportedProtocolVersions`. * A client that receives the mismatch MUST pick the highest mutually supported - version and restart the session, or terminate. LPP 1.1 and 1.2 clients MAY - use the `projectLoading` capability; clients that need directory targets MUST - request `"1.2"`. + version and restart the session, or terminate. LPP 1.1, 1.2, and 1.3 clients + MAY use the `projectLoading` capability; clients that need directory targets + MUST request `"1.2"` or `"1.3"`. Clients that need source identity MUST + request `"1.3"` and require `sourceIdentity: true` in the result capabilities. * A provider MUST support at least one of the versions it lists in `supportedProtocolVersions`. @@ -1047,7 +1055,8 @@ contract: reconstruct, symbols, definition, references, rename, edit validation, project loading, errors/refusals, protocol mismatch, malformed messages, and shutdown. The same directory covers LPP 1.0, its LPP 1.1 file-entry - revision, and its LPP 1.2 directory-target revision. + revision, its LPP 1.2 directory-target revision, and its LPP 1.3 + source-identity revision. * `conformance/runner/`: a runner that replays fixtures against any provider binary and compares responses exactly. * `conformance/mock-provider/`: the reference provider for the demonstration @@ -1068,8 +1077,8 @@ Methods: | --- | --- | --- | --- | | `lpp/initialize` | none | `{ protocolVersion, clientInfo? }` | `{ protocolVersion, serverInfo, languages, capabilities }` | | `lpp/shutdown` | none | `{}` | `null` | -| `lpp/check` | `check`; plus `projectLoading` for an LPP 1.1/1.2 `entry` request | `{ documents, projectRoot? }` or `{ entry, projectRoot? }` | `{ documents: [{ uri, version, diagnostics }] }` | -| `lpp/compile` | `compile`; plus `projectLoading` for an LPP 1.1/1.2 `entry` request | `{ documents, projectRoot? }` or `{ entry, projectRoot? }` | `{ diagnostics: [{ uri, version, diagnostics }], sourceIdentity?, artifact }` | +| `lpp/check` | `check`; plus `projectLoading` for an LPP 1.1/1.2/1.3 `entry` request | `{ documents, projectRoot? }` or `{ entry, projectRoot? }` | `{ documents: [{ uri, version, diagnostics }] }` | +| `lpp/compile` | `compile`; plus `projectLoading` for an LPP 1.1/1.2/1.3 `entry` request and `sourceIdentity` for an LPP 1.3 entry result | `{ documents, projectRoot? }` or `{ entry, projectRoot? }` | `{ diagnostics: [{ uri, version, diagnostics }], sourceIdentity?, artifact }` | | `lpp/reconstruct` | `reconstruct` | `{ artifact }` | `{ source, uri? }` | | `lpp/symbols` | `symbols` | `{ documents, projectRoot? }` | `{ documents: [{ uri, version, symbols }] }` | | `lpp/definition` | `definition` | `{ document, position }` | `{ locations }` |