From 46a4dfca7a33304c624736156a42cd06aafc1b05 Mon Sep 17 00:00:00 2001 From: konojunya Date: Thu, 3 Sep 2026 20:58:09 +0900 Subject: [PATCH 1/2] Carry diagnostic expectations through the engine --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- crates/stack-engine-wasm/Cargo.toml | 2 +- crates/stack-engine-wasm/src/lib.rs | 11 +++++++++++ crates/stack-engine/Cargo.toml | 2 +- crates/stack-engine/src/lib.rs | 9 ++++++++- packages/engine/THIRD_PARTY_LICENSES.md | 2 +- tests/specification-revision | 2 +- 8 files changed, 27 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed65bb5..5e628ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,11 +112,11 @@ dependencies = [ [[package]] name = "stack-compiler" version = "0.1.0" -source = "git+https://github.com/stack-sh/compiler.git?rev=17a0abe9c35e641761ff08fdf59b29a42828d9fd#17a0abe9c35e641761ff08fdf59b29a42828d9fd" +source = "git+https://github.com/stack-sh/compiler.git?rev=3d2379483da1edaeb24a26d43743587a4f5bd645#3d2379483da1edaeb24a26d43743587a4f5bd645" [[package]] name = "stack-engine" -version = "0.1.0" +version = "0.2.0" dependencies = [ "stack-compiler", "stack-formatter", @@ -125,7 +125,7 @@ dependencies = [ [[package]] name = "stack-engine-wasm" -version = "0.1.0" +version = "0.2.0" dependencies = [ "js-sys", "serde", diff --git a/Cargo.toml b/Cargo.toml index 7350cad..e9c3d41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,6 @@ repository = "https://github.com/stack-sh/engine" [workspace.dependencies] serde = { version = "=1.0.229", features = ["derive"] } serde_json = "=1.0.151" -stack-compiler = { git = "https://github.com/stack-sh/compiler.git", rev = "17a0abe9c35e641761ff08fdf59b29a42828d9fd" } +stack-compiler = { git = "https://github.com/stack-sh/compiler.git", rev = "3d2379483da1edaeb24a26d43743587a4f5bd645" } stack-formatter = { path = "crates/stack-formatter" } stack-theme = { git = "https://github.com/stack-sh/theme.git", rev = "ed6c500762fc9ccffc8777172ac672a716dcd916" } diff --git a/crates/stack-engine-wasm/Cargo.toml b/crates/stack-engine-wasm/Cargo.toml index 86fe670..135bd04 100644 --- a/crates/stack-engine-wasm/Cargo.toml +++ b/crates/stack-engine-wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stack-engine-wasm" -version = "0.1.0" +version = "0.2.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/stack-engine-wasm/src/lib.rs b/crates/stack-engine-wasm/src/lib.rs index 64033b1..c4d2727 100644 --- a/crates/stack-engine-wasm/src/lib.rs +++ b/crates/stack-engine-wasm/src/lib.rs @@ -86,6 +86,8 @@ pub struct Diagnostic { pub message: String, /// Primary end-exclusive source range. pub range: SourceRange, + /// Ordered source values or constructs valid at the primary range. + pub expected: Vec, /// Optional corrective guidance. pub help: Option, /// Other source locations involved in the diagnostic. @@ -217,6 +219,7 @@ impl From for Diagnostic { severity: Severity::from(diagnostic.severity), message: diagnostic.message, range: SourceRange::from(diagnostic.range), + expected: diagnostic.expected, help: diagnostic.help, related: diagnostic .related @@ -291,6 +294,7 @@ export interface Diagnostic { readonly severity: Severity; readonly message: string; readonly range: SourceRange; + readonly expected: readonly string[]; readonly help: string | null; readonly related: readonly RelatedInformation[]; } @@ -433,6 +437,11 @@ fn diagnostic_to_js(diagnostic: Diagnostic) -> Result { )?; set(&output, "message", diagnostic.message.into())?; set(&output, "range", range_to_js(diagnostic.range)?)?; + let expected = Array::new(); + for value in diagnostic.expected { + expected.push(&value.into()); + } + set(&output, "expected", expected.into())?; set_optional_string(&output, "help", diagnostic.help)?; let related = Array::new(); for information in diagnostic.related { @@ -553,6 +562,7 @@ mod tests { severity: stack_engine::Severity::Warning, message: "fallback used".to_owned(), range, + expected: vec!["available-resource".to_owned()], help: Some("install the resource".to_owned()), related: vec![stack_engine::RelatedInformation { message: "requested here".to_owned(), @@ -560,6 +570,7 @@ mod tests { }], }); assert_eq!(converted.severity, Severity::Warning); + assert_eq!(converted.expected, ["available-resource"]); assert_eq!(converted.help.as_deref(), Some("install the resource")); assert_eq!(converted.related[0].message, "requested here"); assert_eq!(converted.related[0].range.start.byte_offset, 1); diff --git a/crates/stack-engine/Cargo.toml b/crates/stack-engine/Cargo.toml index 310d28a..e37c918 100644 --- a/crates/stack-engine/Cargo.toml +++ b/crates/stack-engine/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stack-engine" -version = "0.1.0" +version = "0.2.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/stack-engine/src/lib.rs b/crates/stack-engine/src/lib.rs index 993d3b0..7ec6a24 100644 --- a/crates/stack-engine/src/lib.rs +++ b/crates/stack-engine/src/lib.rs @@ -258,6 +258,7 @@ fn resource_diagnostic( severity: Severity::Warning, message, range: SourceRange::from(span), + expected: Vec::new(), help: Some(help.to_owned()), related: Vec::new(), }) @@ -285,6 +286,7 @@ fn order_diagnostic( severity: Severity::Warning, message: "order hint could not be satisfied by deterministic layout".to_owned(), range: SourceRange::from(span), + expected: Vec::new(), help: Some("Adjust the order hint or same-rank constraints.".to_owned()), related: Vec::new(), }) @@ -383,6 +385,8 @@ pub struct Diagnostic { pub message: String, /// Primary end-exclusive source range. pub range: SourceRange, + /// Ordered source values or constructs valid at the primary range. + pub expected: Vec, /// Optional corrective guidance. pub help: Option, /// Other source locations involved in the diagnostic. @@ -459,6 +463,7 @@ impl From for Diagnostic { }, message: diagnostic.message, range: SourceRange::from(diagnostic.span), + expected: diagnostic.expected, help: diagnostic.help, related: diagnostic .related @@ -780,7 +785,7 @@ mod tests { } #[test] - fn diagnostic_conversion_keeps_warning_help_and_related_ranges() { + fn diagnostic_conversion_keeps_expected_help_and_related_ranges() { let start = compiler_diagnostic::SourcePosition { byte_offset: 3, line: 2, @@ -796,6 +801,7 @@ mod tests { severity: compiler_diagnostic::Severity::Warning, message: "warning".to_owned(), span: compiler_diagnostic::Span { start, end }, + expected: vec!["right".to_owned(), "down".to_owned()], help: Some("help".to_owned()), related: vec![compiler_diagnostic::RelatedInformation { message: "related".to_owned(), @@ -805,6 +811,7 @@ mod tests { let portable = Diagnostic::from(diagnostic); assert_eq!(portable.severity, Severity::Warning); + assert_eq!(portable.expected, ["right", "down"]); assert_eq!(portable.help.as_deref(), Some("help")); assert_eq!(portable.related[0].message, "related"); assert_eq!( diff --git a/packages/engine/THIRD_PARTY_LICENSES.md b/packages/engine/THIRD_PARTY_LICENSES.md index 283f3b2..2e2192f 100644 --- a/packages/engine/THIRD_PARTY_LICENSES.md +++ b/packages/engine/THIRD_PARTY_LICENSES.md @@ -4,7 +4,7 @@ | Component | Version / revision | Selected license | Source | | --- | --- | --- | --- | -| `stack-compiler` | `17a0abe9c35e641761ff08fdf59b29a42828d9fd` | Apache-2.0 | | +| `stack-compiler` | `3d2379483da1edaeb24a26d43743587a4f5bd645` | Apache-2.0 | | | `stack-theme` | `ed6c500762fc9ccffc8777172ac672a716dcd916` | Apache-2.0 | | | `serde` / `serde_core` | `1.0.229` | Apache-2.0 | | | `serde_json` | `1.0.151` | Apache-2.0 | | diff --git a/tests/specification-revision b/tests/specification-revision index 6da9620..20d6940 100644 --- a/tests/specification-revision +++ b/tests/specification-revision @@ -1 +1 @@ -f382069928c805fe69b7a192bfd6a877036bc036 +7f9154d22702ddf02f2713bbc06dde7bdf635806 From dc55dee4294016790f830f293cc3c72358dda763 Mon Sep 17 00:00:00 2001 From: konojunya Date: Thu, 3 Sep 2026 20:59:41 +0900 Subject: [PATCH 2/2] Prepare engine package 0.2.0 --- README.md | 2 +- .../snapshots/render/complete-semantics.svg | 4 ++-- .../render/default-normalization.svg | 4 ++-- package-lock.json | 6 +++--- package.json | 2 +- packages/engine/README.md | 2 +- packages/engine/package.json | 2 +- tests/fixtures/operation-cases.json | 7 +++++++ tests/types.test.ts | 1 + tests/wasm.test.mjs | 21 +++++++++++++++++++ 10 files changed, 40 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index cf5c05e..0096882 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ cargo doc --workspace --no-deps The renderer emits fixed-dimension standalone SVG with embedded catalog icons, local marker references, escaped authored text, accessible title and description metadata, and no script, event handler, external URL, host font measurement, or runtime I/O. Canonical SVG snapshots are byte-stable and parsed by `scripts/validate-svg.py`; set `UPDATE_STACK_SNAPSHOTS=1` only when intentionally regenerating them. CI also executes one exact numeric geometry fixture in both the native suite and a WASI build. -The npm package exports synchronous `format`, `check`, and `render` functions after asynchronous module initialization. Each operation accepts `string | Uint8Array` and returns a specific typed result with camel-case metadata and portable diagnostics. Invalid UTF-8 remains a normal `STK1001` result. Unsupported JavaScript input types and internal operational failures throw at the adapter boundary. Shared fixtures compare complete native and WebAssembly results, including formatted source, diagnostics, SVG, and metadata. Artifact validation audits WebAssembly imports and package contents; browser consumers retain responsibility for loading the module and performing any DOM, filesystem, network, or clock work. +The npm package exports synchronous `format`, `check`, and `render` functions after asynchronous module initialization. Each operation accepts `string | Uint8Array` and returns a specific typed result with camel-case metadata and portable diagnostics. Diagnostics preserve the compiler's primary range, ordered `expected` values, corrective help, and related source locations. Invalid UTF-8 remains a normal `STK1001` result. Unsupported JavaScript input types and internal operational failures throw at the adapter boundary. Shared fixtures compare complete native and WebAssembly results, including formatted source, diagnostics, SVG, and metadata. Artifact validation audits WebAssembly imports and package contents; browser consumers retain responsibility for loading the module and performing any DOM, filesystem, network, or clock work. Public npm releases are produced from GitHub Releases after the repository checks pass. See [RELEASING.md](./RELEASING.md) for the first-release bootstrap and subsequent trusted-publishing flow. diff --git a/crates/stack-engine/tests/snapshots/render/complete-semantics.svg b/crates/stack-engine/tests/snapshots/render/complete-semantics.svg index c5cc081..5c0cc80 100644 --- a/crates/stack-engine/tests/snapshots/render/complete-semantics.svg +++ b/crates/stack-engine/tests/snapshots/render/complete-semantics.svg @@ -1,8 +1,8 @@ - + Complete semantics Architecture diagram with 10 nodes, 3 groups, and 8 relationships. - stack-engine 0.1.0; language 1.0; theme 0.1.0 at sha256:b54ca3435931675d36d6a69b5154a0bd6ce2cbae1cfe14a20a095a8e24f8bae4 + stack-engine 0.2.0; language 1.0; theme 0.1.0 at sha256:b54ca3435931675d36d6a69b5154a0bd6ce2cbae1cfe14a20a095a8e24f8bae4 diff --git a/crates/stack-engine/tests/snapshots/render/default-normalization.svg b/crates/stack-engine/tests/snapshots/render/default-normalization.svg index 7d2146d..0f8de12 100644 --- a/crates/stack-engine/tests/snapshots/render/default-normalization.svg +++ b/crates/stack-engine/tests/snapshots/render/default-normalization.svg @@ -1,8 +1,8 @@ - + Default normalization Architecture diagram with 2 nodes, 0 groups, and 1 relationship. - stack-engine 0.1.0; language 1.0; theme 0.1.0 at sha256:b54ca3435931675d36d6a69b5154a0bd6ce2cbae1cfe14a20a095a8e24f8bae4 + stack-engine 0.2.0; language 1.0; theme 0.1.0 at sha256:b54ca3435931675d36d6a69b5154a0bd6ce2cbae1cfe14a20a095a8e24f8bae4 diff --git a/package-lock.json b/package-lock.json index 05baf3b..0ea53f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stack-engine-workspace", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stack-engine-workspace", - "version": "0.1.0", + "version": "0.2.0", "workspaces": [ "packages/engine" ], @@ -395,7 +395,7 @@ }, "packages/engine": { "name": "@stack-sh/engine", - "version": "0.1.0", + "version": "0.2.0", "license": "Apache-2.0" } } diff --git a/package.json b/package.json index 2ca8a52..cc74ea8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "stack-engine-workspace", "private": true, - "version": "0.1.0", + "version": "0.2.0", "workspaces": [ "packages/engine" ], diff --git a/packages/engine/README.md b/packages/engine/README.md index f10fe08..d4327bf 100644 --- a/packages/engine/README.md +++ b/packages/engine/README.md @@ -12,6 +12,6 @@ const checked = check(new TextEncoder().encode('stack 1.0 diagram "API" { node a const rendered = render('stack 1.0 diagram "API" { node api "API" }'); ``` -Each operation is synchronous after module initialization and accepts either a JavaScript string or `Uint8Array`. Invalid Stack source, including invalid UTF-8 bytes, returns normal portable diagnostics. A JavaScript value of any other type throws `TypeError` at the package boundary. +Each operation is synchronous after module initialization and accepts either a JavaScript string or `Uint8Array`. Invalid Stack source, including invalid UTF-8 bytes, returns normal portable diagnostics. Diagnostics include the primary range, ordered `expected` values, corrective help, and related source locations. A JavaScript value of any other type throws `TypeError` at the package boundary. The package does not read files, contact a network service, inspect the DOM, observe a clock, or measure host fonts. Consumers own module loading and all host I/O. diff --git a/packages/engine/package.json b/packages/engine/package.json index e869446..3d41e55 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -1,6 +1,6 @@ { "name": "@stack-sh/engine", - "version": "0.1.0", + "version": "0.2.0", "description": "Browser WebAssembly adapter for Stack diagram operations", "type": "module", "license": "Apache-2.0", diff --git a/tests/fixtures/operation-cases.json b/tests/fixtures/operation-cases.json index 460473b..8be7cbd 100644 --- a/tests/fixtures/operation-cases.json +++ b/tests/fixtures/operation-cases.json @@ -13,6 +13,13 @@ "value": "stack 1.0 diagram \"Fallback\" { theme neon node api \"API\" { icon \"missing\" } }" } }, + { + "name": "actionable-error-string", + "input": { + "kind": "string", + "value": "stack 1.0\ndiagram \"Example\" {\n node app \"App\"\n layout { direction hoo }\n}\n" + } + }, { "name": "invalid-utf8-bytes", "input": { diff --git a/tests/types.test.ts b/tests/types.test.ts index 6fa2f4a..97ee92a 100644 --- a/tests/types.test.ts +++ b/tests/types.test.ts @@ -20,6 +20,7 @@ const diagnostic: Diagnostic | undefined = checked.diagnostics[0]; formatted.formattedSource?.toUpperCase(); rendered.svg?.startsWith(" { } }); +test("browser diagnostics preserve actionable compiler guidance", () => { + const actionable = wasmOutputs().find( + ({ name }) => name === "actionable-error-string", + ); + assert.ok(actionable); + assert.equal(actionable.render.svg, null); + assert.equal(actionable.check.metadata.engineVersion, "0.2.0"); + assert.deepEqual(actionable.check.diagnostics[0], { + code: "STK2002", + severity: "error", + message: "Unknown layout direction 'hoo'.", + range: { + start: { byteOffset: 68, line: 4, column: 22 }, + end: { byteOffset: 71, line: 4, column: 25 }, + }, + expected: ["right", "down"], + help: "Use 'right' for horizontal flow or 'down' for vertical flow.", + related: [], + }); +}); + test("the JavaScript boundary rejects unsupported source values consistently", () => { for (const operation of [format, check, render]) { assert.throws(