From 48dc82cb98f0b69f6a31d0fe9d45a52d36fe8dd0 Mon Sep 17 00:00:00 2001 From: konojunya Date: Sat, 5 Sep 2026 14:38:45 +0900 Subject: [PATCH] Define the language intelligence contract --- .github/workflows/ci.yml | 10 + INTERCHANGE.md | 6 + LANGUAGE_INTELLIGENCE.md | 174 ++++++++ README.md | 4 + conformance/README.md | 10 +- .../language-intelligence/format/fixture.json | 31 ++ .../language-intelligence/format/source.stack | 1 + .../partial/fixture.json | 84 ++++ .../partial/source.stack | 5 + .../semantic/fixture.json | 277 +++++++++++++ .../semantic/source.stack | 20 + .../0006-standardize-language-intelligence.md | 70 ++++ .../language-intelligence-fixture.schema.json | 32 ++ schemas/language-intelligence.schema.json | 375 ++++++++++++++++++ scripts/validate-language-intelligence.py | 236 +++++++++++ 15 files changed, 1333 insertions(+), 2 deletions(-) create mode 100644 LANGUAGE_INTELLIGENCE.md create mode 100644 conformance/language-intelligence/format/fixture.json create mode 100644 conformance/language-intelligence/format/source.stack create mode 100644 conformance/language-intelligence/partial/fixture.json create mode 100644 conformance/language-intelligence/partial/source.stack create mode 100644 conformance/language-intelligence/semantic/fixture.json create mode 100644 conformance/language-intelligence/semantic/source.stack create mode 100644 docs/decisions/0006-standardize-language-intelligence.md create mode 100644 schemas/language-intelligence-fixture.schema.json create mode 100644 schemas/language-intelligence.schema.json create mode 100644 scripts/validate-language-intelligence.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f67fb36..9bb5b33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,11 @@ jobs: test -f "$case/expected.stack" test -f "$case/expected.ir.json" done + for case in conformance/language-intelligence/*; do + test -d "$case" + test -f "$case/source.stack" + test -f "$case/fixture.json" + done - name: Validate JSON Schemas run: check-jsonschema --check-metaschema schemas/*.json @@ -67,6 +72,11 @@ jobs: - name: Validate formatter fixtures run: python scripts/validate-formatter-fixtures.py + - name: Validate language intelligence fixtures + run: | + check-jsonschema --schemafile schemas/language-intelligence-fixture.schema.json conformance/language-intelligence/*/fixture.json + python scripts/validate-language-intelligence.py + language-package: runs-on: ubuntu-latest steps: diff --git a/INTERCHANGE.md b/INTERCHANGE.md index 9e75d60..5480e45 100644 --- a/INTERCHANGE.md +++ b/INTERCHANGE.md @@ -187,3 +187,9 @@ A conforming runner MUST: 8. report the case identifier and mismatch location. An implementation MUST record the specification release or commit revision used for its conformance run. Passing an older suite does not claim support for a newer specification revision. + +## 6. Language Intelligence Companion Contract + +Diagnostics and source ranges defined here are reused by the [Stack Language Intelligence Specification](./LANGUAGE_INTELLIGENCE.md). Completion, hover, document symbols, format edits, caller-owned document versions, and partial-document behavior belong to that companion contract rather than normalized IR. + +Language intelligence does not change compiler stages 1 through 4 or add source-oriented fields to normalized IR. Native and WebAssembly consumers use the same canonical fixtures while protocol adapters remain responsible for transport lifecycle and coordinate conversion. diff --git a/LANGUAGE_INTELLIGENCE.md b/LANGUAGE_INTELLIGENCE.md new file mode 100644 index 0000000..a176529 --- /dev/null +++ b/LANGUAGE_INTELLIGENCE.md @@ -0,0 +1,174 @@ +# Stack Language Intelligence Specification + +## Status + +This document is a normative part of the draft Stack 1.0 specification. It defines the protocol-neutral operations and portable data shared by native tools, WebAssembly adapters, language servers, browser editors, command-line tools, and agents. + +The JSON Schema in [`schemas/language-intelligence.schema.json`](./schemas/language-intelligence.schema.json) is normative for request and response shape. This document is normative for field meaning, ordering, recovery, and ownership. + +## 1. Scope + +Stack language intelligence covers these source-oriented operations: + +- compiler diagnostics; +- completion; +- hover; +- document symbols; +- canonical-format text edits. + +The contract is not a transport protocol. It does not define JSON-RPC, process lifecycle, document storage, filesystem access, network access, cancellation messages, or editor UI. In particular, it does not copy the Language Server Protocol data model. An LSP adapter maps this contract to the capabilities negotiated with its client. + +Theme resolution, provider-pack loading, layout, rendering, and artifact paths remain outside the compiler boundary. A caller may supply already validated icon identifiers as completion catalog data, but language intelligence MUST NOT fetch a catalog or interpret catalog text as instructions. + +## 2. Contract Version and Envelopes + +Every request and response contains `schemaVersion`. Stack 1.0 defines language-intelligence schema version `1.0`. + +Every operation also contains: + +- `kind`: `request` or `response`; +- `documentVersion`: a non-negative caller-owned snapshot version; +- `feature`: `diagnostics`, `completion`, `hover`, `documentSymbols`, or `format`. + +A response MUST echo the request's `schemaVersion`, `documentVersion`, and `feature`. Consumers MUST discard a response when its document version is no longer current. Implementations MUST reject unsupported schema major versions and MUST NOT silently ignore unknown fields. + +Adding an optional field is a schema minor change. Removing a field, changing a field meaning or enum meaning, or making an optional field required is a schema major change. + +## 3. Source Positions and Ranges + +Language intelligence reuses the source position and range contract from the [compiler interchange specification](./INTERCHANGE.md#41-positions-and-ranges): + +- `byteOffset` is a zero-based UTF-8 byte offset in the complete source snapshot; +- `line` is one-based; +- `column` is a one-based Unicode scalar column; +- range start is inclusive and range end is exclusive. + +A request position MUST identify a UTF-8 scalar boundary and its three coordinates MUST agree. A position at the end of the source is valid. A response range MUST belong to the exact source snapshot identified by `documentVersion`. + +Protocol adapters own coordinate conversion. For example, an LSP adapter converts between this contract and the position encoding negotiated with the client. The compiler MUST NOT expose LSP-specific zero-based or UTF-16 coordinates. + +## 4. Diagnostics + +Every response contains `diagnostics`, even when empty. Each entry conforms to [`diagnostic.schema.json`](./schemas/diagnostic.schema.json) and retains the diagnostic ordering defined by the compiler interchange specification. + +The compiler returns every portable diagnostic available for the snapshot. Syntax recovery may limit that set; Stack 1.0 does not require a partial syntax tree or multiple syntax errors. Human-readable message, help, hover prose, and related-information wording are not compatibility guarantees. Code, severity, source range, and any fixture-pinned `expected` values are portable meaning. + +An operation MAY return useful completion data alongside error diagnostics. Hover and document symbols MAY be empty when the implementation cannot recover a trustworthy construct. Formatting MUST return no edits after an encoding, lexical, or syntax error; a syntactically valid document with semantic errors MAY still be formatted. + +## 5. Completion + +A completion request contains a position and an explicit `completionCatalog`. The catalog contains zero or more caller-owned icon entries. An entry contains its exact Stack icon identifier, a display label, and optional plain-text detail and documentation. Implementations MUST bound catalog size and text length before processing untrusted input. + +A completion response contains `isIncomplete` and `items`: + +- `isIncomplete` is `true` when additional typing or a more complete snapshot may produce a materially different list; +- `items` are ordered by ascending `sortText`, then bytewise `label`; +- duplicate pairs of `label` and `edit` are not allowed. + +Each item has one semantic kind: + +- `keyword`: a grammatical Stack keyword; +- `property`: a property or layout statement valid in the current block; +- `enumValue`: a closed value from the language specification; +- `identifier`: a document-local node or group identifier valid at the position; +- `icon`: an identifier supplied through the completion catalog. + +`filterText` is the plain string a consumer filters against. `detail` and `documentation` are plain text or `null`; they MUST NOT contain Markdown, HTML, commands, or executable links. The required `edit` replaces the complete source token or incomplete token fragment relevant to the item. Its range contains the request position and its `newText` is literal Stack source, not a snippet. + +Closed language values use specification order. Document identifiers use declaration order. Catalog icons use bytewise identifier order. An implementation MAY omit candidates it cannot establish safely, but MUST NOT invent syntax, enum values, identifiers, or icon IDs. + +For an icon item, `label` is the catalog entry's display label, `filterText`, `sortText`, and edit text use its exact `id`, and optional detail and documentation are copied without interpretation. + +Completion SHOULD remain available from a recognizable lexical context when the complete document does not parse. Such a response sets `isIncomplete` when recovery cannot establish the enclosing construct unambiguously. + +## 6. Hover + +A hover response contains `hover`, either `null` or an object with: + +- the exact source range being described; +- a semantic kind: `diagram`, `group`, `node`, `edge`, or `property`; +- a short label; +- optional plain-text detail and documentation. + +Hover text is presentation-neutral plain text. It MUST NOT contain raw HTML, executable commands, or fetched content. A reference hover describes the declaration resolved by compiler semantics while retaining the range of the reference under the request position. + +When multiple constructs cover a position, the smallest trustworthy construct wins. An implementation returns `null` instead of guessing when parsing or name resolution cannot identify one construct. + +## 7. Document Symbols + +A document-symbol response contains `symbols` in source order. Each symbol has: + +- `name`: the user-visible label or a concise edge description; +- `kind`: `diagram`, `group`, `node`, or `edge`; +- `detail`: stable plain-text secondary information or `null`; +- `range`: the complete declaration; +- `selectionRange`: the most useful authored token within that declaration; +- `children`: directly nested symbols in source order. + +`selectionRange` MUST be contained by `range`. A child range MUST be contained by its parent range. The diagram is the root symbol; groups contain their direct node and group declarations; diagram-scope edges are children of the diagram. Symbol output reflects syntax structure and MAY be returned for a syntactically valid document with semantic errors. + +## 8. Format Edits + +A format response contains `edits`. Each edit has an end-exclusive range in the input snapshot and literal `newText`. + +Edits MUST be ordered by ascending range start and MUST NOT overlap. All ranges are interpreted against the unchanged input snapshot; consumers apply multiple edits from the end of the document toward the beginning. Stack's canonical formatter SHOULD return either: + +- an empty array when the source is already canonical; or +- one whole-document edit when canonical output differs. + +The formatter owns canonical source generation. The compiler owns the shared `TextEdit` representation but MUST NOT duplicate formatter behavior. + +## 9. Snapshot and Incremental Boundaries + +The compiler core is stateless. Every operation evaluates one complete, caller-owned UTF-8 snapshot. It does not retain document text, apply deltas, schedule work, or decide whether a result is stale. + +A stateful adapter MAY accept incremental changes. Before calling the compiler it MUST: + +1. validate that the change applies to the expected document version; +2. convert protocol positions to valid UTF-8 byte boundaries; +3. apply the change to its private snapshot; +4. invoke the compiler with the complete updated source; +5. echo the updated version in the portable response; +6. suppress a cancelled or stale result before publication. + +This boundary lets native and WebAssembly consumers share deterministic compiler semantics without forcing one document store or transport into the compiler crate. + +## 10. Ownership + +| Owner | Responsibility | +| --- | --- | +| `stack-sh/specification` | Normative prose, JSON Schemas, canonical fixtures, language values, portable compatibility | +| `stack-sh/compiler` | Stateless diagnostics, semantic completion, hover resolution, document symbols, source ranges, dependency-free native types | +| `stack-sh/engine` formatter | Canonical formatting and conversion of changed output to portable text edits | +| WebAssembly adapter | Validated JSON / JavaScript boundary, document-version echo, catalog input bounds, serialization of compiler-owned results | +| Native LSP adapter | JSON-RPC lifecycle, client capabilities, position-encoding conversion, incremental document store, cancellation, stale-result suppression | +| Editor / CLI / agent consumer | Current document version, edit application, UI presentation, filesystem and process behavior | + +The LSP adapter MUST translate rather than redefine diagnostic codes or compiler semantics. WebAssembly and native adapters MUST be able to run the same fixtures under [`conformance/language-intelligence/`](./conformance/language-intelligence). + +## 11. Conformance Fixtures + +Each case is a lowercase ASCII directory containing exactly: + +```text +conformance/language-intelligence//source.stack +conformance/language-intelligence//fixture.json +``` + +`fixture.json` conforms to [`language-intelligence-fixture.schema.json`](./schemas/language-intelligence-fixture.schema.json). It identifies `source.stack` and contains ordered request / expected-response pairs. A runner MUST: + +1. read source as bytes and reject an unsupported encoding before interpreting positions; +2. preserve operation order from the fixture; +3. validate each request and response against the portable schema; +4. invoke the feature with the exact source snapshot, version, position, and catalog; +5. compare the complete semantic JSON value, except prose fields explicitly marked non-normative by a fixture runner; +6. report the case ID, operation ID, and first mismatch; +7. record the exact specification revision used. + +The canonical suite includes a valid semantic case, an incomplete syntax case that still returns completion, and a formatting case. Implementations MAY provide more information than a non-exhaustive runtime request normally requires, but canonical fixture responses are exact for claiming conformance to that fixture revision. + +## 12. Security and Resource Limits + +Source and completion catalogs are untrusted data. Implementations MUST apply the Stack document limits before unbounded analysis and MUST bound catalog entry count and string length. They MUST NOT execute source, render response prose as trusted markup, fetch an icon or URL, access the filesystem, or retain caller data implicitly. + +Errors at a host, transport, or serialization boundary are operational failures, not fabricated `STK` diagnostics. Adapters expose such failures through their own typed error channel while preserving the compiler diagnostic contract for source problems. diff --git a/README.md b/README.md index 370a72a..5306077 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,13 @@ The language is currently a proposal for Stack 1.0. No compatibility guarantee a - [Language specification](./SPECIFICATION.md) - [Canonical formatter specification](./FORMATTER.md) - [Compiler interchange specification](./INTERCHANGE.md) +- [Language intelligence specification](./LANGUAGE_INTELLIGENCE.md) - [ADR-0001: Adopt a constrained declarative topology language](./docs/decisions/0001-constrained-declarative-language.md) - [ADR-0002: Make the canonical theme catalog own icons](./docs/decisions/0002-theme-owned-icons.md) - [ADR-0003: Standardize compiler interchange and conformance fixtures](./docs/decisions/0003-standardize-compiler-interchange-and-conformance.md) - [ADR-0004: Distribute shared editor language assets from the specification](./docs/decisions/0004-distribute-editor-language-assets.md) - [ADR-0005: Separate provider icon namespaces from theme icons](./docs/decisions/0005-provider-icon-namespaces.md) +- [ADR-0006: Standardize protocol-neutral language intelligence](./docs/decisions/0006-standardize-language-intelligence.md) - [Examples](./examples) - [Conformance suite](./conformance) - [`@stack-sh/language`](./packages/language) @@ -67,9 +69,11 @@ check-jsonschema --check-metaschema schemas/*.json check-jsonschema --schemafile schemas/example-catalog.schema.json examples/catalog.json check-jsonschema --schemafile schemas/normalized-ir.schema.json conformance/valid/*/expected.ir.json conformance/formatter/*/expected.ir.json find conformance -name expected.diagnostics.json -print0 | xargs -0 check-jsonschema --schemafile schemas/diagnostic-expectations.schema.json +check-jsonschema --schemafile schemas/language-intelligence-fixture.schema.json conformance/language-intelligence/*/fixture.json python scripts/validate-example-catalog.py python scripts/validate-compiler-diagnostics.py python scripts/validate-formatter-fixtures.py +python scripts/validate-language-intelligence.py npm ci npm run test:language npm run pack:check diff --git a/conformance/README.md b/conformance/README.md index 09b9a72..7f0aaa7 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -1,6 +1,6 @@ # Stack Conformance Suite -This directory contains implementation-independent compiler and formatter conformance cases for the Stack language. +This directory contains implementation-independent compiler, formatter, and language-intelligence conformance cases for the Stack language. ## Layout @@ -17,6 +17,9 @@ invalid//expected.diagnostics.json formatter//input.stack formatter//expected.stack formatter//expected.ir.json + +language-intelligence//source.stack +language-intelligence//fixture.json ``` `source.stack` must be read as bytes. This permits future encoding-error fixtures even though valid Stack documents are UTF-8. @@ -29,6 +32,8 @@ The encoding cases intentionally include raw invalid UTF-8, a UTF-8 byte order m Formatter inputs are compiler-valid Stack documents. A formatter runner compares canonical source bytes, formats the expected source again to verify idempotence, and compiles both input and expected source to verify that each is semantically equal to `expected.ir.json`. The complete formatter behavior is defined in the [Stack Canonical Formatter Specification](../FORMATTER.md). +Language-intelligence fixtures contain ordered protocol-neutral request / expected-response pairs. They cover diagnostics, context-aware completion, caller-owned icon catalogs, hover, nested document symbols, incomplete syntax, and canonical format edits. Native and WebAssembly adapters consume the same files and pin the exact specification revision. The complete operation, recovery, ownership, and incremental-boundary behavior is defined in the [Stack Language Intelligence Specification](../LANGUAGE_INTELLIGENCE.md). + ## Comparison - JSON values are compared semantically; formatting and object-member order do not matter. @@ -38,5 +43,6 @@ Formatter inputs are compiler-valid Stack documents. A formatter runner compares - An invalid case must not produce normalized IR. - Diagnostic expectations compare code, severity, and range in emitted order. A fixture may also require exact `expected` values and ordering. - Diagnostic message, help, and related-information wording are not compared. +- Language-intelligence request and response versions and features must match, all ranges belong to the source snapshot, and arrays retain fixture order. -The complete normative behavior is defined in the [Stack Compiler Interchange Specification](../INTERCHANGE.md). +The complete normative behavior is defined in the [Stack Compiler Interchange Specification](../INTERCHANGE.md), [Stack Canonical Formatter Specification](../FORMATTER.md), and [Stack Language Intelligence Specification](../LANGUAGE_INTELLIGENCE.md). diff --git a/conformance/language-intelligence/format/fixture.json b/conformance/language-intelligence/format/fixture.json new file mode 100644 index 0000000..f6e8d2e --- /dev/null +++ b/conformance/language-intelligence/format/fixture.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": "1.0", + "source": "source.stack", + "operations": [ + { + "id": "whole-document-format", + "request": { + "schemaVersion": "1.0", + "kind": "request", + "documentVersion": 12, + "feature": "format" + }, + "response": { + "schemaVersion": "1.0", + "kind": "response", + "documentVersion": 12, + "feature": "format", + "diagnostics": [], + "edits": [ + { + "range": { + "start": { "byteOffset": 0, "line": 1, "column": 1 }, + "end": { "byteOffset": 57, "line": 2, "column": 1 } + }, + "newText": "stack 1.0\n\ndiagram \"Format\" {\n node api \"API\" {\n kind service\n }\n}\n" + } + ] + } + } + ] +} diff --git a/conformance/language-intelligence/format/source.stack b/conformance/language-intelligence/format/source.stack new file mode 100644 index 0000000..b2a498a --- /dev/null +++ b/conformance/language-intelligence/format/source.stack @@ -0,0 +1 @@ +stack 1.0 diagram "Format"{node api "API"{kind service}} diff --git a/conformance/language-intelligence/partial/fixture.json b/conformance/language-intelligence/partial/fixture.json new file mode 100644 index 0000000..7f782a7 --- /dev/null +++ b/conformance/language-intelligence/partial/fixture.json @@ -0,0 +1,84 @@ +{ + "schemaVersion": "1.0", + "source": "source.stack", + "operations": [ + { + "id": "diagnostics", + "request": { + "schemaVersion": "1.0", + "kind": "request", + "documentVersion": 9, + "feature": "diagnostics" + }, + "response": { + "schemaVersion": "1.0", + "kind": "response", + "documentVersion": 9, + "feature": "diagnostics", + "diagnostics": [ + { + "code": "STK2002", + "severity": "error", + "message": "Expected a node property.", + "range": { + "start": { "byteOffset": 54, "line": 5, "column": 5 }, + "end": { "byteOffset": 56, "line": 5, "column": 7 } + }, + "expected": ["kind", "icon", "detail", "}"], + "help": "Use one of: kind, icon, detail, }.", + "related": [] + } + ] + } + }, + { + "id": "partial-property-completion", + "request": { + "schemaVersion": "1.0", + "kind": "request", + "documentVersion": 9, + "feature": "completion", + "position": { "byteOffset": 56, "line": 5, "column": 7 }, + "completionCatalog": { "icons": [] } + }, + "response": { + "schemaVersion": "1.0", + "kind": "response", + "documentVersion": 9, + "feature": "completion", + "diagnostics": [ + { + "code": "STK2002", + "severity": "error", + "message": "Expected a node property.", + "range": { + "start": { "byteOffset": 54, "line": 5, "column": 5 }, + "end": { "byteOffset": 56, "line": 5, "column": 7 } + }, + "expected": ["kind", "icon", "detail", "}"], + "help": "Use one of: kind, icon, detail, }.", + "related": [] + } + ], + "isIncomplete": true, + "items": [ + { + "label": "kind", + "kind": "property", + "detail": "node property", + "documentation": null, + "filterText": "kind", + "sortText": "0001:kind", + "edit": { + "range": { + "start": { "byteOffset": 54, "line": 5, "column": 5 }, + "end": { "byteOffset": 56, "line": 5, "column": 7 } + }, + "newText": "kind" + } + } + ] + } + } + ] +} diff --git a/conformance/language-intelligence/partial/source.stack b/conformance/language-intelligence/partial/source.stack new file mode 100644 index 0000000..1faa006 --- /dev/null +++ b/conformance/language-intelligence/partial/source.stack @@ -0,0 +1,5 @@ +stack 1.0 + +diagram "Partial" { + node api "API" { + ki diff --git a/conformance/language-intelligence/semantic/fixture.json b/conformance/language-intelligence/semantic/fixture.json new file mode 100644 index 0000000..8b36be9 --- /dev/null +++ b/conformance/language-intelligence/semantic/fixture.json @@ -0,0 +1,277 @@ +{ + "schemaVersion": "1.0", + "source": "source.stack", + "operations": [ + { + "id": "diagnostics", + "request": { + "schemaVersion": "1.0", + "kind": "request", + "documentVersion": 7, + "feature": "diagnostics" + }, + "response": { + "schemaVersion": "1.0", + "kind": "response", + "documentVersion": 7, + "feature": "diagnostics", + "diagnostics": [] + } + }, + { + "id": "node-kind-completion", + "request": { + "schemaVersion": "1.0", + "kind": "request", + "documentVersion": 7, + "feature": "completion", + "position": { "byteOffset": 78, "line": 7, "column": 14 }, + "completionCatalog": { "icons": [] } + }, + "response": { + "schemaVersion": "1.0", + "kind": "response", + "documentVersion": 7, + "feature": "completion", + "diagnostics": [], + "isIncomplete": false, + "items": [ + { + "label": "service", + "kind": "enumValue", + "detail": "node kind", + "documentation": null, + "filterText": "service", + "sortText": "0003:service", + "edit": { + "range": { + "start": { "byteOffset": 74, "line": 7, "column": 10 }, + "end": { "byteOffset": 81, "line": 7, "column": 17 } + }, + "newText": "service" + } + } + ] + } + }, + { + "id": "icon-completion", + "request": { + "schemaVersion": "1.0", + "kind": "request", + "documentVersion": 7, + "feature": "completion", + "position": { "byteOffset": 97, "line": 8, "column": 16 }, + "completionCatalog": { + "icons": [ + { + "id": "aws:sqs", + "label": "Amazon SQS", + "detail": "AWS provider icon", + "documentation": null + }, + { + "id": "aws:s3", + "label": "Amazon S3", + "detail": "AWS provider icon", + "documentation": null + } + ] + } + }, + "response": { + "schemaVersion": "1.0", + "kind": "response", + "documentVersion": 7, + "feature": "completion", + "diagnostics": [], + "isIncomplete": false, + "items": [ + { + "label": "Amazon S3", + "kind": "icon", + "detail": "AWS provider icon", + "documentation": null, + "filterText": "aws:s3", + "sortText": "aws:s3", + "edit": { + "range": { + "start": { "byteOffset": 92, "line": 8, "column": 11 }, + "end": { "byteOffset": 98, "line": 8, "column": 17 } + }, + "newText": "aws:s3" + } + }, + { + "label": "Amazon SQS", + "kind": "icon", + "detail": "AWS provider icon", + "documentation": null, + "filterText": "aws:sqs", + "sortText": "aws:sqs", + "edit": { + "range": { + "start": { "byteOffset": 92, "line": 8, "column": 11 }, + "end": { "byteOffset": 98, "line": 8, "column": 17 } + }, + "newText": "aws:sqs" + } + } + ] + } + }, + { + "id": "edge-endpoint-completion", + "request": { + "schemaVersion": "1.0", + "kind": "request", + "documentVersion": 7, + "feature": "completion", + "position": { "byteOffset": 206, "line": 17, "column": 18 }, + "completionCatalog": { "icons": [] } + }, + "response": { + "schemaVersion": "1.0", + "kind": "response", + "documentVersion": 7, + "feature": "completion", + "diagnostics": [], + "isIncomplete": false, + "items": [ + { + "label": "database", + "kind": "identifier", + "detail": "node · Database", + "documentation": null, + "filterText": "database", + "sortText": "0002:database", + "edit": { + "range": { + "start": { "byteOffset": 203, "line": 17, "column": 15 }, + "end": { "byteOffset": 211, "line": 17, "column": 23 } + }, + "newText": "database" + } + } + ] + } + }, + { + "id": "reference-hover", + "request": { + "schemaVersion": "1.0", + "kind": "request", + "documentVersion": 7, + "feature": "hover", + "position": { "byteOffset": 197, "line": 17, "column": 9 } + }, + "response": { + "schemaVersion": "1.0", + "kind": "response", + "documentVersion": 7, + "feature": "hover", + "diagnostics": [], + "hover": { + "range": { + "start": { "byteOffset": 196, "line": 17, "column": 8 }, + "end": { "byteOffset": 199, "line": 17, "column": 11 } + }, + "kind": "node", + "label": "API", + "detail": "node api · service", + "documentation": null + } + } + }, + { + "id": "document-symbols", + "request": { + "schemaVersion": "1.0", + "kind": "request", + "documentVersion": 7, + "feature": "documentSymbols" + }, + "response": { + "schemaVersion": "1.0", + "kind": "response", + "documentVersion": 7, + "feature": "documentSymbols", + "diagnostics": [], + "symbols": [ + { + "name": "Checkout", + "kind": "diagram", + "detail": "Stack 1.0 diagram", + "range": { + "start": { "byteOffset": 11, "line": 3, "column": 1 }, + "end": { "byteOffset": 239, "line": 20, "column": 2 } + }, + "selectionRange": { + "start": { "byteOffset": 19, "line": 3, "column": 9 }, + "end": { "byteOffset": 29, "line": 3, "column": 19 } + }, + "children": [ + { + "name": "API", + "kind": "node", + "detail": "node api · service", + "range": { + "start": { "byteOffset": 48, "line": 6, "column": 3 }, + "end": { "byteOffset": 103, "line": 9, "column": 4 } + }, + "selectionRange": { + "start": { "byteOffset": 53, "line": 6, "column": 8 }, + "end": { "byteOffset": 56, "line": 6, "column": 11 } + }, + "children": [] + }, + { + "name": "Data", + "kind": "group", + "detail": "group data", + "range": { + "start": { "byteOffset": 107, "line": 11, "column": 3 }, + "end": { "byteOffset": 187, "line": 15, "column": 4 } + }, + "selectionRange": { + "start": { "byteOffset": 113, "line": 11, "column": 9 }, + "end": { "byteOffset": 117, "line": 11, "column": 13 } + }, + "children": [ + { + "name": "Database", + "kind": "node", + "detail": "node database · database", + "range": { + "start": { "byteOffset": 131, "line": 12, "column": 5 }, + "end": { "byteOffset": 183, "line": 14, "column": 6 } + }, + "selectionRange": { + "start": { "byteOffset": 136, "line": 12, "column": 10 }, + "end": { "byteOffset": 144, "line": 12, "column": 18 } + }, + "children": [] + } + ] + }, + { + "name": "api -> database", + "kind": "edge", + "detail": "forward edge · data", + "range": { + "start": { "byteOffset": 191, "line": 17, "column": 3 }, + "end": { "byteOffset": 237, "line": 19, "column": 4 } + }, + "selectionRange": { + "start": { "byteOffset": 196, "line": 17, "column": 8 }, + "end": { "byteOffset": 211, "line": 17, "column": 23 } + }, + "children": [] + } + ] + } + ] + } + } + ] +} diff --git a/conformance/language-intelligence/semantic/source.stack b/conformance/language-intelligence/semantic/source.stack new file mode 100644 index 0000000..ad697ef --- /dev/null +++ b/conformance/language-intelligence/semantic/source.stack @@ -0,0 +1,20 @@ +stack 1.0 + +diagram "Checkout" { + theme dark + + node api "API" { + kind service + icon "aws:s3" + } + + group data "Data" { + node database "Database" { + kind database + } + } + + edge api -> database "SQL" { + kind data + } +} diff --git a/docs/decisions/0006-standardize-language-intelligence.md b/docs/decisions/0006-standardize-language-intelligence.md new file mode 100644 index 0000000..faed18b --- /dev/null +++ b/docs/decisions/0006-standardize-language-intelligence.md @@ -0,0 +1,70 @@ +# ADR-0006: Standardize Protocol-Neutral Language Intelligence + +## Status + +Accepted + +## Date + +2026-09-05 + +## Context + +Stack diagnostics, completion, hover, document symbols, and formatting will be consumed by a native language server, the browser playground, the CLI, and agents. If each integration defines its own positions, completion meanings, partial-document behavior, or text edits, the same source will behave differently across products. + +The Language Server Protocol is an important adapter target, but its JSON-RPC lifecycle, negotiated position encoding, capabilities, and incremental document synchronization are transport concerns. Making LSP types the compiler API would couple browser and non-editor consumers to one protocol and would put mutable document state in the dependency-free compiler core. + +Stack already has portable diagnostics and end-exclusive UTF-8 source ranges. The compiler also has a lossless source model and semantic source-map sidecar. These contracts provide the correct foundation for language features without changing normalized IR. + +## Decision + +Define language-intelligence schema version 1.0 in the specification repository. Requests and responses are protocol-neutral, identify one caller-owned document version, and cover diagnostics, completion, hover, document symbols, and format edits. + +All portable positions reuse the existing zero-based UTF-8 byte offset plus one-based Unicode scalar line and column. Protocol adapters convert their client coordinates at the boundary. Responses echo the document version so callers can suppress stale work. + +The compiler remains stateless and dependency-free. It owns semantic diagnostics, completion, hover, and document symbols over one complete source snapshot. It exposes native types and does not parse or serialize JSON at runtime. Completion catalog entries are explicit caller-owned data; the compiler never loads themes or provider packs. + +The canonical formatter owns formatted source and maps a changed result to non-overlapping text edits. A WebAssembly adapter owns validated JavaScript / JSON serialization. A native LSP adapter owns JSON-RPC, incremental document state, cancellation, position conversion, and capability negotiation. Both adapters consume the same compiler semantics and canonical fixtures. + +The specification repository owns JSON Schemas and fixtures. Each implementation pins the exact specification revision it passes. + +## Alternatives Considered + +### Use LSP types as the compiler contract + +- Pros: A native language server would require less mapping code. +- Cons: Couples every consumer to JSON-RPC-era position, capability, and lifecycle choices; browser and agent callers inherit editor protocol concerns. +- Rejected: LSP is an adapter boundary, not Stack language semantics. + +### Put language intelligence in the Web application + +- Pros: The first interactive consumer could ship quickly. +- Cons: Native tools would duplicate parsing and behavior, while product UI would become the accidental language owner. +- Rejected: Shared semantic behavior belongs below product integrations. + +### Make the compiler retain incremental documents + +- Pros: The core could optimize reparsing immediately. +- Cons: Introduces mutable lifecycle, version, cancellation, and memory-policy concerns before performance evidence requires them. +- Rejected: A stateless snapshot contract is deterministic and leaves future incremental internals additive. + +### Serialize JSON from the compiler library + +- Pros: Adapters could forward one ready-made payload. +- Cons: Adds a runtime dependency and commits the native API to one serialization library. +- Rejected: Native types plus conformance-only adapters preserve a smaller core. + +### Allow Markdown hover and completion documentation + +- Pros: Rich editor presentation. +- Cons: Creates rendering and injection risk and makes consumers disagree about supported markup. +- Rejected: Portable prose is plain text; adapters may add trusted presentation outside the semantic contract. + +## Consequences + +- Native, WebAssembly, CLI, editor, and agent consumers can compare the same semantic fixture output. +- LSP adapters perform explicit coordinate and lifecycle conversion instead of leaking protocol types into the compiler. +- Completion catalogs remain caller-owned and can include installed provider icons without adding I/O to compiler code. +- Snapshot analysis may reparse complete documents initially; incremental compiler internals can be added later without changing the public operation model. +- Format edits share one data model while canonical formatting remains owned by the formatter. +- New optional language features can evolve within schema 1.x; breaking observable behavior requires a new major schema. diff --git a/schemas/language-intelligence-fixture.schema.json b/schemas/language-intelligence-fixture.schema.json new file mode 100644 index 0000000..a0f2142 --- /dev/null +++ b/schemas/language-intelligence-fixture.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Stack Language Intelligence Conformance Fixture", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "source", "operations"], + "properties": { + "schemaVersion": { "const": "1.0" }, + "source": { "const": "source.stack" }, + "operations": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "request", "response"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "request": { + "$ref": "language-intelligence.schema.json#/$defs/request" + }, + "response": { + "$ref": "language-intelligence.schema.json#/$defs/response" + } + } + } + } + } +} diff --git a/schemas/language-intelligence.schema.json b/schemas/language-intelligence.schema.json new file mode 100644 index 0000000..4540227 --- /dev/null +++ b/schemas/language-intelligence.schema.json @@ -0,0 +1,375 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Stack Language Intelligence Operation", + "oneOf": [ + { "$ref": "#/$defs/request" }, + { "$ref": "#/$defs/response" } + ], + "$defs": { + "position": { + "$ref": "diagnostic.schema.json#/$defs/position" + }, + "range": { + "$ref": "diagnostic.schema.json#/$defs/range" + }, + "diagnostic": { + "$ref": "diagnostic.schema.json#/$defs/diagnostic" + }, + "textEdit": { + "type": "object", + "additionalProperties": false, + "required": ["range", "newText"], + "properties": { + "range": { "$ref": "#/$defs/range" }, + "newText": { + "type": "string", + "maxLength": 1048576 + } + } + }, + "completionCatalogEntry": { + "type": "object", + "additionalProperties": false, + "required": ["id", "label", "detail", "documentation"], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*(?::[a-z][a-z0-9-]*)?$", + "maxLength": 129 + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "detail": { + "type": ["string", "null"], + "minLength": 1, + "maxLength": 240 + }, + "documentation": { + "type": ["string", "null"], + "minLength": 1, + "maxLength": 1000 + } + } + }, + "completionCatalog": { + "type": "object", + "additionalProperties": false, + "required": ["icons"], + "properties": { + "icons": { + "type": "array", + "maxItems": 4096, + "items": { "$ref": "#/$defs/completionCatalogEntry" } + } + } + }, + "completionItem": { + "type": "object", + "additionalProperties": false, + "required": [ + "label", + "kind", + "detail", + "documentation", + "filterText", + "sortText", + "edit" + ], + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "kind": { + "enum": ["keyword", "property", "enumValue", "identifier", "icon"] + }, + "detail": { + "type": ["string", "null"], + "minLength": 1, + "maxLength": 240 + }, + "documentation": { + "type": ["string", "null"], + "minLength": 1, + "maxLength": 1000 + }, + "filterText": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "sortText": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "edit": { "$ref": "#/$defs/textEdit" } + } + }, + "hover": { + "type": "object", + "additionalProperties": false, + "required": ["range", "kind", "label", "detail", "documentation"], + "properties": { + "range": { "$ref": "#/$defs/range" }, + "kind": { + "enum": ["diagram", "group", "node", "edge", "property"] + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "detail": { + "type": ["string", "null"], + "minLength": 1, + "maxLength": 240 + }, + "documentation": { + "type": ["string", "null"], + "minLength": 1, + "maxLength": 1000 + } + } + }, + "documentSymbol": { + "type": "object", + "additionalProperties": false, + "required": ["name", "kind", "detail", "range", "selectionRange", "children"], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "kind": { + "enum": ["diagram", "group", "node", "edge"] + }, + "detail": { + "type": ["string", "null"], + "minLength": 1, + "maxLength": 240 + }, + "range": { "$ref": "#/$defs/range" }, + "selectionRange": { "$ref": "#/$defs/range" }, + "children": { + "type": "array", + "items": { "$ref": "#/$defs/documentSymbol" } + } + } + }, + "requestBase": { + "type": "object", + "required": ["schemaVersion", "kind", "documentVersion", "feature"], + "properties": { + "schemaVersion": { "const": "1.0" }, + "kind": { "const": "request" }, + "documentVersion": { + "type": "integer", + "minimum": 0 + }, + "feature": { + "enum": ["diagnostics", "completion", "hover", "documentSymbols", "format"] + } + } + }, + "positionRequest": { + "allOf": [ + { "$ref": "#/$defs/requestBase" }, + { + "type": "object", + "required": ["position"], + "properties": { + "position": { "$ref": "#/$defs/position" } + } + } + ] + }, + "diagnosticsRequest": { + "allOf": [ + { "$ref": "#/$defs/requestBase" }, + { + "type": "object", + "properties": { + "feature": { "const": "diagnostics" } + } + } + ], + "unevaluatedProperties": false + }, + "completionRequest": { + "allOf": [ + { "$ref": "#/$defs/positionRequest" }, + { + "type": "object", + "required": ["completionCatalog"], + "properties": { + "feature": { "const": "completion" }, + "completionCatalog": { "$ref": "#/$defs/completionCatalog" } + } + } + ], + "unevaluatedProperties": false + }, + "hoverRequest": { + "allOf": [ + { "$ref": "#/$defs/positionRequest" }, + { + "type": "object", + "properties": { + "feature": { "const": "hover" } + } + } + ], + "unevaluatedProperties": false + }, + "documentSymbolsRequest": { + "allOf": [ + { "$ref": "#/$defs/requestBase" }, + { + "type": "object", + "properties": { + "feature": { "const": "documentSymbols" } + } + } + ], + "unevaluatedProperties": false + }, + "formatRequest": { + "allOf": [ + { "$ref": "#/$defs/requestBase" }, + { + "type": "object", + "properties": { + "feature": { "const": "format" } + } + } + ], + "unevaluatedProperties": false + }, + "request": { + "oneOf": [ + { "$ref": "#/$defs/diagnosticsRequest" }, + { "$ref": "#/$defs/completionRequest" }, + { "$ref": "#/$defs/hoverRequest" }, + { "$ref": "#/$defs/documentSymbolsRequest" }, + { "$ref": "#/$defs/formatRequest" } + ] + }, + "responseBase": { + "type": "object", + "required": ["schemaVersion", "kind", "documentVersion", "feature", "diagnostics"], + "properties": { + "schemaVersion": { "const": "1.0" }, + "kind": { "const": "response" }, + "documentVersion": { + "type": "integer", + "minimum": 0 + }, + "feature": { + "enum": ["diagnostics", "completion", "hover", "documentSymbols", "format"] + }, + "diagnostics": { + "type": "array", + "items": { "$ref": "#/$defs/diagnostic" } + } + } + }, + "diagnosticsResponse": { + "allOf": [ + { "$ref": "#/$defs/responseBase" }, + { + "type": "object", + "properties": { + "feature": { "const": "diagnostics" } + } + } + ], + "unevaluatedProperties": false + }, + "completionResponse": { + "allOf": [ + { "$ref": "#/$defs/responseBase" }, + { + "type": "object", + "required": ["isIncomplete", "items"], + "properties": { + "feature": { "const": "completion" }, + "isIncomplete": { "type": "boolean" }, + "items": { + "type": "array", + "items": { "$ref": "#/$defs/completionItem" } + } + } + } + ], + "unevaluatedProperties": false + }, + "hoverResponse": { + "allOf": [ + { "$ref": "#/$defs/responseBase" }, + { + "type": "object", + "required": ["hover"], + "properties": { + "feature": { "const": "hover" }, + "hover": { + "oneOf": [ + { "type": "null" }, + { "$ref": "#/$defs/hover" } + ] + } + } + } + ], + "unevaluatedProperties": false + }, + "documentSymbolsResponse": { + "allOf": [ + { "$ref": "#/$defs/responseBase" }, + { + "type": "object", + "required": ["symbols"], + "properties": { + "feature": { "const": "documentSymbols" }, + "symbols": { + "type": "array", + "items": { "$ref": "#/$defs/documentSymbol" } + } + } + } + ], + "unevaluatedProperties": false + }, + "formatResponse": { + "allOf": [ + { "$ref": "#/$defs/responseBase" }, + { + "type": "object", + "required": ["edits"], + "properties": { + "feature": { "const": "format" }, + "edits": { + "type": "array", + "items": { "$ref": "#/$defs/textEdit" } + } + } + } + ], + "unevaluatedProperties": false + }, + "response": { + "oneOf": [ + { "$ref": "#/$defs/diagnosticsResponse" }, + { "$ref": "#/$defs/completionResponse" }, + { "$ref": "#/$defs/hoverResponse" }, + { "$ref": "#/$defs/documentSymbolsResponse" }, + { "$ref": "#/$defs/formatResponse" } + ] + } + } +} diff --git a/scripts/validate-language-intelligence.py b/scripts/validate-language-intelligence.py new file mode 100644 index 0000000..b6e86fc --- /dev/null +++ b/scripts/validate-language-intelligence.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 + +import json +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FIXTURES = ROOT / "conformance" / "language-intelligence" +REQUIRED_FILES = {"source.stack", "fixture.json"} +FEATURES = {"diagnostics", "completion", "hover", "documentSymbols", "format"} + + +def source_position(source: bytes, byte_offset: int) -> tuple[int, int]: + if byte_offset < 0 or byte_offset > len(source): + raise ValueError(f"byte offset {byte_offset} is outside the source") + try: + source[:byte_offset].decode("utf-8") + except UnicodeDecodeError as error: + raise ValueError(f"byte offset {byte_offset} is not a UTF-8 scalar boundary") from error + + line = 1 + column = 1 + cursor = 0 + while cursor < byte_offset: + if source[cursor : cursor + 2] == b"\r\n": + if cursor + 1 == byte_offset: + raise ValueError(f"byte offset {byte_offset} splits a CRLF sequence") + line += 1 + column = 1 + cursor += 2 + continue + + first = source[cursor] + width = 1 + if first >= 0xF0: + width = 4 + elif first >= 0xE0: + width = 3 + elif first >= 0xC0: + width = 2 + character = source[cursor : cursor + width].decode("utf-8") + if character == "\n": + line += 1 + column = 1 + else: + column += 1 + cursor += width + return line, column + + +def validate_position(source: bytes, position: dict, context: str) -> int: + byte_offset = position["byteOffset"] + line, column = source_position(source, byte_offset) + if position["line"] != line or position["column"] != column: + raise ValueError( + f"{context}: byte offset {byte_offset} resolves to {line}:{column}, " + f"not {position['line']}:{position['column']}" + ) + return byte_offset + + +def validate_range(source: bytes, source_range: dict, context: str) -> tuple[int, int]: + start = validate_position(source, source_range["start"], f"{context}.start") + end = validate_position(source, source_range["end"], f"{context}.end") + if start > end: + raise ValueError(f"{context}: range start follows range end") + return start, end + + +def walk_ranges(source: bytes, value: object, context: str) -> None: + if isinstance(value, list): + for index, item in enumerate(value): + walk_ranges(source, item, f"{context}[{index}]") + return + if not isinstance(value, dict): + return + if set(value) == {"start", "end"} and all( + isinstance(value[key], dict) and "byteOffset" in value[key] + for key in ("start", "end") + ): + validate_range(source, value, context) + return + if {"byteOffset", "line", "column"}.issubset(value): + validate_position(source, value, context) + return + for key, item in value.items(): + walk_ranges(source, item, f"{context}.{key}") + + +def validate_completion(source: bytes, operation: dict, context: str) -> None: + request = operation["request"] + response = operation["response"] + cursor = validate_position(source, request["position"], f"{context}.request.position") + + icon_ids = [entry["id"] for entry in request["completionCatalog"]["icons"]] + if len(icon_ids) != len(set(icon_ids)): + raise ValueError(f"{context}: completion catalog repeats an icon id") + + items = response["items"] + ordering = [(item["sortText"].encode(), item["label"].encode()) for item in items] + if ordering != sorted(ordering): + raise ValueError(f"{context}: completion items are not in deterministic order") + + identities = set() + for index, item in enumerate(items): + start, end = validate_range(source, item["edit"]["range"], f"{context}.items[{index}].edit.range") + if not start <= cursor <= end: + raise ValueError(f"{context}: completion edit does not contain the request position") + identity = (item["label"], start, end, item["edit"]["newText"]) + if identity in identities: + raise ValueError(f"{context}: completion response contains a duplicate item") + identities.add(identity) + if item["kind"] == "icon" and item["edit"]["newText"] not in icon_ids: + raise ValueError(f"{context}: completion invented an icon outside the request catalog") + + +def validate_hover(source: bytes, operation: dict, context: str) -> None: + cursor = validate_position(source, operation["request"]["position"], f"{context}.request.position") + hover = operation["response"]["hover"] + if hover is None: + return + start, end = validate_range(source, hover["range"], f"{context}.response.hover.range") + if not start <= cursor <= end: + raise ValueError(f"{context}: hover range does not contain the request position") + + +def validate_symbols(source: bytes, symbols: list, parent: tuple[int, int] | None, context: str) -> None: + starts = [] + for index, symbol in enumerate(symbols): + symbol_context = f"{context}[{index}]" + start, end = validate_range(source, symbol["range"], f"{symbol_context}.range") + selection_start, selection_end = validate_range( + source, symbol["selectionRange"], f"{symbol_context}.selectionRange" + ) + if not start <= selection_start <= selection_end <= end: + raise ValueError(f"{symbol_context}: selection range escapes the symbol") + if parent is not None and not parent[0] <= start <= end <= parent[1]: + raise ValueError(f"{symbol_context}: child range escapes its parent") + starts.append(start) + validate_symbols(source, symbol["children"], (start, end), f"{symbol_context}.children") + if starts != sorted(starts): + raise ValueError(f"{context}: symbols are not in source order") + + +def validate_format(source: bytes, edits: list, context: str) -> None: + ranges = [] + for index, edit in enumerate(edits): + start, end = validate_range(source, edit["range"], f"{context}.edits[{index}].range") + ranges.append((start, end, edit["newText"].encode("utf-8"))) + if ranges != sorted(ranges, key=lambda item: (item[0], item[1])): + raise ValueError(f"{context}: format edits are not ordered by source range") + for previous, current in zip(ranges, ranges[1:]): + if previous[1] > current[0]: + raise ValueError(f"{context}: format edits overlap") + + formatted = source + for start, end, new_text in reversed(ranges): + formatted = formatted[:start] + new_text + formatted[end:] + formatted.decode("utf-8") + if ranges and (not formatted.endswith(b"\n") or formatted.endswith(b"\n\n") or b"\r" in formatted): + raise ValueError(f"{context}: canonical fixture output must end with exactly one LF") + + +def main() -> None: + cases = sorted(path for path in FIXTURES.iterdir() if path.is_dir()) + if not cases: + raise ValueError(f"{FIXTURES}: no language-intelligence cases found") + + covered_features = set() + has_partial_completion = False + has_catalog_completion = False + has_hover = False + has_format_edit = False + + for case in cases: + actual_files = {path.name for path in case.iterdir() if path.is_file()} + if actual_files != REQUIRED_FILES: + raise ValueError( + f"{case}: expected files {sorted(REQUIRED_FILES)}, found {sorted(actual_files)}" + ) + + source = (case / "source.stack").read_bytes() + if source.startswith(b"\xef\xbb\xbf"): + raise ValueError(f"{case}: source must not contain a byte order mark") + source.decode("utf-8") + fixture = json.loads((case / "fixture.json").read_text(encoding="utf-8")) + operation_ids = [operation["id"] for operation in fixture["operations"]] + if len(operation_ids) != len(set(operation_ids)): + raise ValueError(f"{case}: operation ids must be unique") + + for operation in fixture["operations"]: + context = f"{case.name}/{operation['id']}" + request = operation["request"] + response = operation["response"] + for field in ("schemaVersion", "documentVersion", "feature"): + if request[field] != response[field]: + raise ValueError(f"{context}: response does not echo request {field}") + covered_features.add(request["feature"]) + walk_ranges(source, operation, context) + + if request["feature"] == "completion": + validate_completion(source, operation, context) + has_catalog_completion = has_catalog_completion or bool( + request["completionCatalog"]["icons"] + ) + has_partial_completion = has_partial_completion or bool(response["diagnostics"]) + elif request["feature"] == "hover": + validate_hover(source, operation, context) + has_hover = has_hover or response["hover"] is not None + elif request["feature"] == "documentSymbols": + validate_symbols(source, response["symbols"], None, f"{context}.response.symbols") + elif request["feature"] == "format": + validate_format(source, response["edits"], f"{context}.response") + has_format_edit = has_format_edit or bool(response["edits"]) + + if covered_features != FEATURES: + raise ValueError( + f"language-intelligence fixtures cover {sorted(covered_features)}, expected {sorted(FEATURES)}" + ) + if not has_partial_completion: + raise ValueError("fixtures must include completion alongside an invalid-document diagnostic") + if not has_catalog_completion: + raise ValueError("fixtures must include caller-owned icon completion") + if not has_hover: + raise ValueError("fixtures must include a resolved hover") + if not has_format_edit: + raise ValueError("fixtures must include a canonical format edit") + + print( + f"Validated {len(cases)} language-intelligence cases across " + f"{len(covered_features)} features." + ) + + +if __name__ == "__main__": + main()