From 79f8e5599c99fe1bd7f3e7a0102ba12c01bc024b Mon Sep 17 00:00:00 2001 From: konojunya Date: Wed, 2 Sep 2026 14:37:53 +0900 Subject: [PATCH 1/2] spec: define compiler interchange conformance Standardize normalized IR and diagnostic JSON, add canonical valid and invalid fixtures, and validate the public suite in CI. --- .github/dependabot.yml | 10 + .github/workflows/ci.yml | 50 ++++ INTERCHANGE.md | 185 ++++++++++++ README.md | 14 + SPECIFICATION.md | 8 +- conformance/README.md | 32 +++ .../expected.diagnostics.json | 21 ++ .../unknown-edge-endpoint/source.stack | 6 + .../valid/complete-semantics/expected.ir.json | 242 ++++++++++++++++ .../valid/complete-semantics/source.stack | 97 +++++++ .../default-normalization/expected.ir.json | 48 ++++ .../valid/default-normalization/source.stack | 7 + ...ze-compiler-interchange-and-conformance.md | 75 +++++ requirements-dev.txt | 1 + schemas/diagnostic-expectations.schema.json | 69 +++++ schemas/diagnostic.schema.json | 84 ++++++ schemas/normalized-ir.schema.json | 270 ++++++++++++++++++ 17 files changed, 1218 insertions(+), 1 deletion(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 INTERCHANGE.md create mode 100644 conformance/README.md create mode 100644 conformance/invalid/unknown-edge-endpoint/expected.diagnostics.json create mode 100644 conformance/invalid/unknown-edge-endpoint/source.stack create mode 100644 conformance/valid/complete-semantics/expected.ir.json create mode 100644 conformance/valid/complete-semantics/source.stack create mode 100644 conformance/valid/default-normalization/expected.ir.json create mode 100644 conformance/valid/default-normalization/source.stack create mode 100644 docs/decisions/0003-standardize-compiler-interchange-and-conformance.md create mode 100644 requirements-dev.txt create mode 100644 schemas/diagnostic-expectations.schema.json create mode 100644 schemas/diagnostic.schema.json create mode 100644 schemas/normalized-ir.schema.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..abd2e58 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + - package-ecosystem: pip + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..568f6d2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + conformance-data: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.14" + cache: pip + + - name: Install validation tools + run: python -m pip install --requirement requirements-dev.txt + + - name: Validate fixture layout + shell: bash + run: | + set -euo pipefail + for case in conformance/valid/*; do + test -d "$case" + test -f "$case/source.stack" + test -f "$case/expected.ir.json" + done + for case in conformance/invalid/*; do + test -d "$case" + test -f "$case/source.stack" + test -f "$case/expected.diagnostics.json" + done + + - name: Validate JSON Schemas + run: check-jsonschema --check-metaschema schemas/*.json + + - name: Validate normalized IR fixtures + run: check-jsonschema --schemafile schemas/normalized-ir.schema.json conformance/valid/*/expected.ir.json + + - name: Validate diagnostic fixtures + run: find conformance -name expected.diagnostics.json -print0 | xargs -0 check-jsonschema --schemafile schemas/diagnostic-expectations.schema.json diff --git a/INTERCHANGE.md b/INTERCHANGE.md new file mode 100644 index 0000000..fa61319 --- /dev/null +++ b/INTERCHANGE.md @@ -0,0 +1,185 @@ +# Stack Compiler Interchange Specification + +## Status + +This document is a normative part of the draft Stack 1.0 specification. It defines portable JSON representations for normalized compiler output, diagnostics, and canonical conformance expectations. + +The JSON Schemas in [`schemas/`](./schemas) are normative for document shape. This document is normative for field meaning, ordering, and processing behavior. + +## 1. Scope + +The compiler interchange boundary follows language processing stages 1 through 4: + +```text +UTF-8 source + -> tokenization and parsing + -> identifier and default resolution + -> semantic and complexity validation + -> normalized diagram IR +``` + +Theme and icon resolution, layout solving, and rendering occur after this boundary. They MUST NOT add fields to normalized compiler IR. + +A native compiler API MAY expose implementation-specific types. An implementation claiming portable conformance MUST be able to map its successful output and diagnostics to the JSON contracts defined here. + +## 2. Interchange Version + +Every interchange document contains `schemaVersion`. Stack 1.0 defines schema version `1.0`. + +The schema version is independent of the Stack language version: + +- `languageVersion` identifies the source grammar and semantics; +- `schemaVersion` identifies the portable JSON representation. + +Adding an optional field is a schema minor change. Removing a field, changing a field meaning, changing an enum meaning, or making an optional field required is a schema major change. + +Consumers MUST reject unsupported schema major versions. They MUST NOT silently ignore unknown fields because an unknown field may carry required meaning. + +## 3. Normalized Diagram IR + +A compiler produces normalized IR only when stages 1 through 4 contain no error diagnostics. Warning diagnostics may accompany successful IR. + +The normalized document conforms to [`normalized-ir.schema.json`](./schemas/normalized-ir.schema.json). + +### 3.1 Normalization Rules + +Normalized IR MUST: + +- include the declared language version; +- apply `default` when no theme is authored; +- apply `service` when no node kind is authored; +- apply `flow` when no edge kind is authored; +- preserve node, group, edge, child, same-rank, and order list declaration order; +- represent absent optional values as JSON `null`; +- represent collections as arrays, including when empty; +- use source identifiers without case conversion; +- contain decoded string values rather than source escapes. + +Normalized IR MUST NOT contain: + +- source locations, comments, or formatting trivia; +- unresolved or partial declarations from invalid source; +- theme or icon catalog resolution results; +- coordinates, dimensions, edge paths, colors, typography, or SVG; +- implementation-specific filesystem, network, memory, or object handles. + +JSON object member order has no meaning. Array order is normative. + +### 3.2 Diagram Fields + +| Field | Meaning | +| --- | --- | +| `schemaVersion` | Compiler interchange version; `1.0` for this document | +| `languageVersion` | Major and minor version declared by the source | +| `title` | Decoded visible diagram title | +| `themeId` | Authored theme identifier or `default` | +| `children` | Direct diagram children in declaration order | +| `nodes` | Every node in depth-first declaration order | +| `groups` | Every group in depth-first declaration order | +| `edges` | Diagram edges in declaration order | +| `layout` | Normalized diagram layout or `null` | + +Each child reference contains `type` and `id`. The type is `node` or `group`. Although Stack identifiers are globally unique, the explicit type prevents consumers from inferring element category from another array. + +### 3.3 Nodes and Groups + +A node contains its effective semantic kind and nearest containing group. `parentGroupId` is `null` for a root node. `iconId` and `detail` remain `null` when omitted; icon fallback and visual detail treatment are downstream concerns. + +A group contains its nearest containing group, direct children, and group-scoped layout. `parentGroupId` is `null` for a root group. Group entries use depth-first declaration order: a parent precedes all descendants. + +Containment is intentionally represented in both directions: + +- `children` supports ordered traversal of one scope; +- `parentGroupId` supports direct lookup from a node or group. + +Both representations MUST agree. + +### 3.4 Edges + +`from` and `to` preserve the authored left and right endpoint order. `direction` is one of: + +- `forward` for `->`; +- `bidirectional` for `<->`; +- `association` for `--`. + +Bidirectional and association edges preserve authored endpoint order even though duplicate-edge validation treats their endpoint order as equivalent. + +### 3.5 Layout + +Layout contains: + +- `direction`: `right`, `down`, or `null`; +- `sameRanks`: same-rank identifier lists in declaration order; +- `order`: the authored order list or `null`. + +The identifiers remain scoped to the direct children of the diagram or group that owns the layout. Normalized IR does not contract edges or solve positions. + +## 4. Diagnostic Interchange + +A portable diagnostic conforms to [`diagnostic.schema.json`](./schemas/diagnostic.schema.json). + +### 4.1 Positions and Ranges + +A position contains: + +- `byteOffset`: zero-based UTF-8 byte offset in the original source; +- `line`: one-based line number; +- `column`: one-based Unicode scalar column. + +A range contains an inclusive `start` position and an exclusive `end` position. A point diagnostic has identical start and end positions. LF and CRLF each advance the line once; byte offsets still count their original encoded bytes. + +### 4.2 Fields + +`code` and its normative meaning are assigned by the language specification. `severity` is `error` or `warning`. `message` is concise human-readable text but its exact wording is not a compatibility guarantee. + +`help` is either corrective guidance or `null`. `related` is always an array and identifies other source ranges involved in the diagnostic. Related-information message wording is not a compatibility guarantee. + +Implementations may emit non-`STK` diagnostics. Canonical fixtures only require portable `STK` diagnostics unless a case explicitly documents an implementation extension. + +## 5. Canonical Conformance Suite + +The canonical suite lives in [`conformance/`](./conformance). Each case is one directory named with a lowercase ASCII identifier. + +### 5.1 Valid Cases + +A valid case contains: + +```text +conformance/valid//source.stack +conformance/valid//expected.ir.json +conformance/valid//expected.diagnostics.json # optional +``` + +The source MUST compile to normalized IR semantically equal to `expected.ir.json`. If `expected.diagnostics.json` is absent, no portable diagnostics are expected. When present, it normally contains warning expectations. + +### 5.2 Invalid Cases + +An invalid case contains: + +```text +conformance/invalid//source.stack +conformance/invalid//expected.diagnostics.json +``` + +The source MUST NOT produce normalized IR. Its portable diagnostics MUST match the expectation document. + +### 5.3 Diagnostic Expectations + +Expectation documents conform to [`diagnostic-expectations.schema.json`](./schemas/diagnostic-expectations.schema.json). Each expected diagnostic requires code, severity, and range. + +Runners compare diagnostics in the deterministic order emitted by the compiler. They MUST compare the number of diagnostics and MUST NOT ignore additional portable diagnostics. They do not compare message, help, or related information. + +### 5.4 Runner Behavior + +A conforming runner MUST: + +1. discover case directories in bytewise identifier order; +2. read `source.stack` as bytes so encoding fixtures remain possible; +3. compile with catalog, layout, and renderer stages disabled; +4. validate successful normalized output against the normalized IR schema; +5. validate expectation documents against their schemas; +6. compare JSON values semantically rather than by object-member order or whitespace; +7. fail a case when required files are missing or unexpected files use reserved names; +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. diff --git a/README.md b/README.md index 609cf01..f5db645 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,12 @@ The language is currently a proposal for Stack 1.0. No compatibility guarantee a ## Documents - [Language specification](./SPECIFICATION.md) +- [Compiler interchange specification](./INTERCHANGE.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) - [Examples](./examples) +- [Conformance suite](./conformance) ## Example @@ -49,6 +52,17 @@ diagram "Checkout" { Stack sources conventionally use the `.stack` extension. +## Validation + +Install the development requirements and validate the portable schemas and conformance data: + +```sh +python -m pip install --requirement requirements-dev.txt +check-jsonschema --check-metaschema schemas/*.json +check-jsonschema --schemafile schemas/normalized-ir.schema.json conformance/valid/*/expected.ir.json +find conformance -name expected.diagnostics.json -print0 | xargs -0 check-jsonschema --schemafile schemas/diagnostic-expectations.schema.json +``` + ## Design Principles - Describe architecture, not pixels. diff --git a/SPECIFICATION.md b/SPECIFICATION.md index cd04b5c..5dccc89 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -548,7 +548,7 @@ Example diagnostic shape: } ``` -The JSON shape is illustrative in Stack 1.0; parser APIs may expose diagnostics in another representation. Assigned codes and their meanings are normative. +The portable JSON representation is defined by the [Stack Compiler Interchange Specification](./INTERCHANGE.md). Native compiler APIs may expose diagnostics in another representation, but conformance adapters MUST preserve the portable field meanings. Assigned codes and their meanings are normative. ### 11.3 Recovery @@ -577,6 +577,12 @@ At minimum, validators MUST distinguish: Implementations should collect independent semantic errors in one pass rather than stopping after the first error. +### 11.5 Normalized IR and Conformance + +A document that completes processing stages 1 through 4 without errors produces normalized, renderer-independent diagram IR. Normalization applies language defaults and makes containment, semantic kinds, edge directionality, and layout input explicit. It does not resolve themes or icons, solve layout, or contain renderer state. + +The normative portable IR, diagnostic interchange, and conformance fixture contracts are defined in the [Stack Compiler Interchange Specification](./INTERCHANGE.md) and the JSON Schemas in [`schemas/`](./schemas). Canonical fixture data belongs to this specification repository. Implementations consume that data and record the specification release or revision they support. + ## 12. Versioning and Backwards Compatibility ### 12.1 Language Version diff --git a/conformance/README.md b/conformance/README.md new file mode 100644 index 0000000..0c275e8 --- /dev/null +++ b/conformance/README.md @@ -0,0 +1,32 @@ +# Stack Conformance Suite + +This directory contains implementation-independent compiler conformance cases for the Stack language. + +## Layout + +Each case is a directory with a lowercase ASCII identifier: + +```text +valid//source.stack +valid//expected.ir.json +valid//expected.diagnostics.json # optional + +invalid//source.stack +invalid//expected.diagnostics.json +``` + +`source.stack` must be read as bytes. This permits future encoding-error fixtures even though valid Stack documents are UTF-8. + +Expected IR documents conform to [`normalized-ir.schema.json`](../schemas/normalized-ir.schema.json). Diagnostic expectation documents conform to [`diagnostic-expectations.schema.json`](../schemas/diagnostic-expectations.schema.json). + +## Comparison + +- JSON values are compared semantically; formatting and object-member order do not matter. +- Array order is significant. +- A valid case must produce the expected normalized IR. +- An absent valid-case diagnostic file means no portable diagnostics are expected. +- An invalid case must not produce normalized IR. +- Diagnostic expectations compare code, severity, and range in emitted order. +- Diagnostic message, help, and related-information wording are not compared. + +The complete normative behavior is defined in the [Stack Compiler Interchange Specification](../INTERCHANGE.md). diff --git a/conformance/invalid/unknown-edge-endpoint/expected.diagnostics.json b/conformance/invalid/unknown-edge-endpoint/expected.diagnostics.json new file mode 100644 index 0000000..28a0cb7 --- /dev/null +++ b/conformance/invalid/unknown-edge-endpoint/expected.diagnostics.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": "1.0", + "diagnostics": [ + { + "code": "STK3003", + "severity": "error", + "range": { + "start": { + "byteOffset": 71, + "line": 5, + "column": 15 + }, + "end": { + "byteOffset": 78, + "line": 5, + "column": 22 + } + } + } + ] +} diff --git a/conformance/invalid/unknown-edge-endpoint/source.stack b/conformance/invalid/unknown-edge-endpoint/source.stack new file mode 100644 index 0000000..f70d86f --- /dev/null +++ b/conformance/invalid/unknown-edge-endpoint/source.stack @@ -0,0 +1,6 @@ +stack 1.0 + +diagram "Unknown endpoint" { + node api "API" + edge api -> missing +} diff --git a/conformance/valid/complete-semantics/expected.ir.json b/conformance/valid/complete-semantics/expected.ir.json new file mode 100644 index 0000000..d36ffe3 --- /dev/null +++ b/conformance/valid/complete-semantics/expected.ir.json @@ -0,0 +1,242 @@ +{ + "schemaVersion": "1.0", + "languageVersion": { + "major": 1, + "minor": 0 + }, + "title": "Complete semantics", + "themeId": "dark", + "children": [ + { + "type": "node", + "id": "user" + }, + { + "type": "group", + "id": "system" + } + ], + "nodes": [ + { + "id": "user", + "label": "User", + "kind": "actor", + "iconId": null, + "detail": "Purchaser", + "parentGroupId": null + }, + { + "id": "web", + "label": "Web app", + "kind": "client", + "iconId": "browser", + "detail": null, + "parentGroupId": "system" + }, + { + "id": "api", + "label": "API", + "kind": "service", + "iconId": null, + "detail": null, + "parentGroupId": "system" + }, + { + "id": "function", + "label": "Function", + "kind": "function", + "iconId": null, + "detail": null, + "parentGroupId": "compute" + }, + { + "id": "worker", + "label": "Worker", + "kind": "worker", + "iconId": null, + "detail": null, + "parentGroupId": "compute" + }, + { + "id": "database", + "label": "Database", + "kind": "database", + "iconId": null, + "detail": null, + "parentGroupId": "state" + }, + { + "id": "cache", + "label": "Cache", + "kind": "cache", + "iconId": null, + "detail": null, + "parentGroupId": "state" + }, + { + "id": "queue", + "label": "Queue", + "kind": "queue", + "iconId": null, + "detail": null, + "parentGroupId": "state" + }, + { + "id": "storage", + "label": "Storage", + "kind": "storage", + "iconId": null, + "detail": null, + "parentGroupId": "state" + }, + { + "id": "vendor", + "label": "External provider", + "kind": "external", + "iconId": null, + "detail": null, + "parentGroupId": "system" + } + ], + "groups": [ + { + "id": "system", + "label": "System", + "parentGroupId": null, + "children": [ + { + "type": "node", + "id": "web" + }, + { + "type": "node", + "id": "api" + }, + { + "type": "group", + "id": "compute" + }, + { + "type": "group", + "id": "state" + }, + { + "type": "node", + "id": "vendor" + } + ], + "layout": { + "direction": "down", + "sameRanks": [ + ["web", "api"] + ], + "order": ["web", "api"] + } + }, + { + "id": "compute", + "label": "Compute", + "parentGroupId": "system", + "children": [ + { + "type": "node", + "id": "function" + }, + { + "type": "node", + "id": "worker" + } + ], + "layout": null + }, + { + "id": "state", + "label": "State", + "parentGroupId": "system", + "children": [ + { + "type": "node", + "id": "database" + }, + { + "type": "node", + "id": "cache" + }, + { + "type": "node", + "id": "queue" + }, + { + "type": "node", + "id": "storage" + } + ], + "layout": null + } + ], + "edges": [ + { + "from": "user", + "to": "web", + "direction": "forward", + "kind": "request", + "label": "HTTPS" + }, + { + "from": "web", + "to": "api", + "direction": "bidirectional", + "kind": "flow", + "label": "WebSocket" + }, + { + "from": "api", + "to": "function", + "direction": "forward", + "kind": "dependency", + "label": "Invoke" + }, + { + "from": "function", + "to": "database", + "direction": "forward", + "kind": "data", + "label": "SQL" + }, + { + "from": "function", + "to": "queue", + "direction": "forward", + "kind": "event", + "label": "Job" + }, + { + "from": "worker", + "to": "storage", + "direction": "association", + "kind": "data", + "label": "Archive" + }, + { + "from": "api", + "to": "cache", + "direction": "forward", + "kind": "data", + "label": "Read" + }, + { + "from": "api", + "to": "vendor", + "direction": "forward", + "kind": "request", + "label": "API" + } + ], + "layout": { + "direction": "right", + "sameRanks": [ + ["user", "system"] + ], + "order": ["user", "system"] + } +} diff --git a/conformance/valid/complete-semantics/source.stack b/conformance/valid/complete-semantics/source.stack new file mode 100644 index 0000000..dde4695 --- /dev/null +++ b/conformance/valid/complete-semantics/source.stack @@ -0,0 +1,97 @@ +stack 1.0 + +diagram "Complete semantics" { + theme dark + + layout { + direction right + rank same [user, system] + order [user, system] + } + + node user "User" { + kind actor + detail "Purchaser" + } + + group system "System" { + layout { + direction down + rank same [web, api] + order [web, api] + } + + node web "Web app" { + kind client + icon "browser" + } + + node api "API" { + kind service + } + + group compute "Compute" { + node function "Function" { + kind function + } + + node worker "Worker" { + kind worker + } + } + + group state "State" { + node database "Database" { + kind database + } + + node cache "Cache" { + kind cache + } + + node queue "Queue" { + kind queue + } + + node storage "Storage" { + kind storage + } + } + + node vendor "External provider" { + kind external + } + } + + edge user -> web "HTTPS" { + kind request + } + + edge web <-> api "WebSocket" { + kind flow + } + + edge api -> function "Invoke" { + kind dependency + } + + edge function -> database "SQL" { + kind data + } + + edge function -> queue "Job" { + kind event + } + + edge worker -- storage "Archive" { + kind data + } + + edge api -> cache "Read" { + kind data + } + + edge api -> vendor "API" { + kind request + } +} diff --git a/conformance/valid/default-normalization/expected.ir.json b/conformance/valid/default-normalization/expected.ir.json new file mode 100644 index 0000000..cceeb14 --- /dev/null +++ b/conformance/valid/default-normalization/expected.ir.json @@ -0,0 +1,48 @@ +{ + "schemaVersion": "1.0", + "languageVersion": { + "major": 1, + "minor": 0 + }, + "title": "Default normalization", + "themeId": "default", + "children": [ + { + "type": "node", + "id": "web" + }, + { + "type": "node", + "id": "api" + } + ], + "nodes": [ + { + "id": "web", + "label": "Web app", + "kind": "service", + "iconId": null, + "detail": null, + "parentGroupId": null + }, + { + "id": "api", + "label": "API", + "kind": "service", + "iconId": null, + "detail": null, + "parentGroupId": null + } + ], + "groups": [], + "edges": [ + { + "from": "web", + "to": "api", + "direction": "forward", + "kind": "flow", + "label": null + } + ], + "layout": null +} diff --git a/conformance/valid/default-normalization/source.stack b/conformance/valid/default-normalization/source.stack new file mode 100644 index 0000000..d025213 --- /dev/null +++ b/conformance/valid/default-normalization/source.stack @@ -0,0 +1,7 @@ +stack 1.0 + +diagram "Default normalization" { + node web "Web app" + node api "API" + edge web -> api +} diff --git a/docs/decisions/0003-standardize-compiler-interchange-and-conformance.md b/docs/decisions/0003-standardize-compiler-interchange-and-conformance.md new file mode 100644 index 0000000..45ea1e4 --- /dev/null +++ b/docs/decisions/0003-standardize-compiler-interchange-and-conformance.md @@ -0,0 +1,75 @@ +# ADR-0003: Standardize Compiler Interchange and Conformance Fixtures + +## Status + +Accepted + +## Date + +2026-09-02 + +## Context + +Stack source is intended to be consumed by a reference Rust compiler, browser tools, command-line tools, editors, layout engines, and potentially independent implementations. The language specification currently defines source syntax and semantic diagnostics, but it does not define a portable normalized representation after parsing and validation. + +Without a normative interchange contract, each compiler can choose different defaults, containment models, ordering rules, enum names, and optional-value behavior while still claiming to implement the same source language. Prose examples and implementation-specific unit tests are not sufficient to detect that drift. + +Stack also needs a conformance suite that belongs to the language rather than to one implementation. The suite must be executable by any implementation and must avoid making human-readable diagnostic wording part of backwards compatibility. + +## Decision + +Stack will define two versioned JSON interchange documents: + +- normalized diagram IR produced after successful language stages 1 through 4; +- structured diagnostics produced by any compiler stage. + +JSON Schema Draft 2020-12 files in `schemas/` define their portable shapes. The normative field semantics, ordering guarantees, range conventions, and compatibility rules are defined in `INTERCHANGE.md`. + +Normalized IR includes specification-defined defaults, explicit containment references, deterministic declaration order, and layout constraints or hints. It excludes source spans, comments, theme or icon resolution results, layout coordinates, renderer state, and filesystem or network handles. + +Diagnostic interchange includes stable code, severity, message, an end-exclusive source range, optional help, and related information. Portable conformance expectations compare code, severity, and range. They do not compare message, help, or related-information wording unless a future fixture explicitly opts into an additional assertion. + +The specification repository owns conformance sources and expected documents. Each implementation owns its runner and records which specification revision or release it supports. Valid cases require normalized IR and may also expect warnings. Invalid cases require diagnostics and must not produce normalized IR. + +Compiler-native APIs may use language-specific types. Conformance adapters map those types to the portable contract without requiring the core compiler to parse or serialize JSON at runtime. + +## Alternatives Considered + +### Use the Rust IR types as the only contract + +- Pros: No additional schema or mapping layer. +- Cons: Couples every consumer and independent implementation to Rust naming and release mechanics. +- Rejected: The language contract must remain implementation-independent. + +### Store only valid and invalid source files + +- Pros: Very small fixture repository. +- Cons: Proves only acceptance or rejection; does not prove defaults, containment, directionality, ordering, or diagnostic locations. +- Rejected: Expected semantic outputs are necessary to detect meaningful drift. + +### Compare complete diagnostic JSON byte for byte + +- Pros: Maximally deterministic output. +- Cons: Makes message improvements and implementation-specific corrective guidance breaking changes. +- Rejected: Portable meaning is carried by code, severity, and source range. + +### Let the specification CI depend directly on the Rust compiler + +- Pros: One place executes the reference implementation. +- Cons: Makes the language repository depend on one implementation and blocks intentional specification changes until that implementation is updated. +- Rejected: Implementations consume the suite; the specification remains implementation-neutral. + +### Include resolved themes, icons, or layout geometry in normalized IR + +- Pros: One document could be passed directly to SVG rendering. +- Cons: Mixes language normalization with catalog versions, visual metrics, and renderer-specific algorithms. +- Rejected: Those stages are downstream of the compiler interchange boundary. + +## Consequences + +- Independent implementations can demonstrate equivalent source semantics with one canonical suite. +- Layout and renderer projects receive an explicit, versioned input contract. +- Compiler implementations need a small adapter for conformance JSON. +- Expected IR documents are reviewable but must be updated whenever a normative default or field changes. +- A backwards-incompatible interchange change requires a new interchange major version even when source syntax is unchanged. +- Specification revisions can land before implementations support them; each implementation must identify the revision or release it tests. diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..b0c215f --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +check-jsonschema==0.38.0 diff --git a/schemas/diagnostic-expectations.schema.json b/schemas/diagnostic-expectations.schema.json new file mode 100644 index 0000000..71c936e --- /dev/null +++ b/schemas/diagnostic-expectations.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Stack Conformance Diagnostic Expectations", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "diagnostics"], + "properties": { + "schemaVersion": { + "const": "1.0" + }, + "diagnostics": { + "type": "array", + "items": { + "$ref": "#/$defs/diagnosticExpectation" + } + } + }, + "$defs": { + "position": { + "type": "object", + "additionalProperties": false, + "required": ["byteOffset", "line", "column"], + "properties": { + "byteOffset": { + "type": "integer", + "minimum": 0 + }, + "line": { + "type": "integer", + "minimum": 1 + }, + "column": { + "type": "integer", + "minimum": 1 + } + } + }, + "range": { + "type": "object", + "additionalProperties": false, + "required": ["start", "end"], + "properties": { + "start": { + "$ref": "#/$defs/position" + }, + "end": { + "$ref": "#/$defs/position" + } + } + }, + "diagnosticExpectation": { + "type": "object", + "additionalProperties": false, + "required": ["code", "severity", "range"], + "properties": { + "code": { + "type": "string", + "pattern": "^STK[0-9]{4}$" + }, + "severity": { + "enum": ["error", "warning"] + }, + "range": { + "$ref": "#/$defs/range" + } + } + } + } +} diff --git a/schemas/diagnostic.schema.json b/schemas/diagnostic.schema.json new file mode 100644 index 0000000..ad5ad83 --- /dev/null +++ b/schemas/diagnostic.schema.json @@ -0,0 +1,84 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Stack Compiler Diagnostic", + "$ref": "#/$defs/diagnostic", + "$defs": { + "position": { + "type": "object", + "additionalProperties": false, + "required": ["byteOffset", "line", "column"], + "properties": { + "byteOffset": { + "type": "integer", + "minimum": 0 + }, + "line": { + "type": "integer", + "minimum": 1 + }, + "column": { + "type": "integer", + "minimum": 1 + } + } + }, + "range": { + "type": "object", + "additionalProperties": false, + "required": ["start", "end"], + "properties": { + "start": { + "$ref": "#/$defs/position" + }, + "end": { + "$ref": "#/$defs/position" + } + } + }, + "relatedInformation": { + "type": "object", + "additionalProperties": false, + "required": ["message", "range"], + "properties": { + "message": { + "type": "string", + "minLength": 1 + }, + "range": { + "$ref": "#/$defs/range" + } + } + }, + "diagnostic": { + "type": "object", + "additionalProperties": false, + "required": ["code", "severity", "message", "range", "help", "related"], + "properties": { + "code": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9_-]*[0-9]{4}$" + }, + "severity": { + "enum": ["error", "warning"] + }, + "message": { + "type": "string", + "minLength": 1 + }, + "range": { + "$ref": "#/$defs/range" + }, + "help": { + "type": ["string", "null"], + "minLength": 1 + }, + "related": { + "type": "array", + "items": { + "$ref": "#/$defs/relatedInformation" + } + } + } + } + } +} diff --git a/schemas/normalized-ir.schema.json b/schemas/normalized-ir.schema.json new file mode 100644 index 0000000..2b0d580 --- /dev/null +++ b/schemas/normalized-ir.schema.json @@ -0,0 +1,270 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Stack Normalized Diagram IR", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "languageVersion", + "title", + "themeId", + "children", + "nodes", + "groups", + "edges", + "layout" + ], + "properties": { + "schemaVersion": { + "const": "1.0" + }, + "languageVersion": { + "$ref": "#/$defs/languageVersion" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "themeId": { + "$ref": "#/$defs/identifier" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/elementReference" + }, + "minItems": 1 + }, + "nodes": { + "type": "array", + "items": { + "$ref": "#/$defs/node" + }, + "minItems": 1, + "maxItems": 40 + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/$defs/group" + }, + "maxItems": 12 + }, + "edges": { + "type": "array", + "items": { + "$ref": "#/$defs/edge" + }, + "maxItems": 80 + }, + "layout": { + "$ref": "#/$defs/nullableLayout" + } + }, + "$defs": { + "identifier": { + "type": "string", + "pattern": "^[a-z][a-z0-9_-]{0,63}$" + }, + "iconIdentifier": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]{0,63}$" + }, + "languageVersion": { + "type": "object", + "additionalProperties": false, + "required": ["major", "minor"], + "properties": { + "major": { + "type": "integer", + "minimum": 0 + }, + "minor": { + "type": "integer", + "minimum": 0 + } + } + }, + "elementReference": { + "type": "object", + "additionalProperties": false, + "required": ["type", "id"], + "properties": { + "type": { + "enum": ["node", "group"] + }, + "id": { + "$ref": "#/$defs/identifier" + } + } + }, + "node": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "label", + "kind", + "iconId", + "detail", + "parentGroupId" + ], + "properties": { + "id": { + "$ref": "#/$defs/identifier" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 60 + }, + "kind": { + "enum": [ + "actor", + "client", + "service", + "function", + "worker", + "database", + "cache", + "queue", + "storage", + "external" + ] + }, + "iconId": { + "oneOf": [ + { + "$ref": "#/$defs/iconIdentifier" + }, + { + "type": "null" + } + ] + }, + "detail": { + "type": ["string", "null"], + "minLength": 1, + "maxLength": 80 + }, + "parentGroupId": { + "oneOf": [ + { + "$ref": "#/$defs/identifier" + }, + { + "type": "null" + } + ] + } + } + }, + "group": { + "type": "object", + "additionalProperties": false, + "required": ["id", "label", "parentGroupId", "children", "layout"], + "properties": { + "id": { + "$ref": "#/$defs/identifier" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 60 + }, + "parentGroupId": { + "oneOf": [ + { + "$ref": "#/$defs/identifier" + }, + { + "type": "null" + } + ] + }, + "children": { + "type": "array", + "items": { + "$ref": "#/$defs/elementReference" + }, + "minItems": 1 + }, + "layout": { + "$ref": "#/$defs/nullableLayout" + } + } + }, + "edge": { + "type": "object", + "additionalProperties": false, + "required": ["from", "to", "direction", "kind", "label"], + "properties": { + "from": { + "$ref": "#/$defs/identifier" + }, + "to": { + "$ref": "#/$defs/identifier" + }, + "direction": { + "enum": ["forward", "bidirectional", "association"] + }, + "kind": { + "enum": ["flow", "request", "event", "data", "dependency"] + }, + "label": { + "type": ["string", "null"], + "minLength": 1, + "maxLength": 40 + } + } + }, + "nullableLayout": { + "oneOf": [ + { + "$ref": "#/$defs/layout" + }, + { + "type": "null" + } + ] + }, + "layout": { + "type": "object", + "additionalProperties": false, + "required": ["direction", "sameRanks", "order"], + "properties": { + "direction": { + "type": ["string", "null"], + "enum": ["right", "down", null] + }, + "sameRanks": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/$defs/identifier" + }, + "minItems": 2, + "uniqueItems": true + } + }, + "order": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/identifier" + }, + "minItems": 2, + "uniqueItems": true + }, + { + "type": "null" + } + ] + } + } + } + } +} From 23e8748201c831e0e9ef94e7d84501e630a4096f Mon Sep 17 00:00:00 2001 From: konojunya Date: Wed, 2 Sep 2026 14:45:02 +0900 Subject: [PATCH 2/2] ci: configure validator dependency cache --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 568f6d2..af7a038 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: with: python-version: "3.14" cache: pip + cache-dependency-path: requirements-dev.txt - name: Install validation tools run: python -m pip install --requirement requirements-dev.txt