diff --git a/Cargo.lock b/Cargo.lock index 0385644..1ec1e80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,7 +195,7 @@ source = "git+https://github.com/stack-sh/compiler.git?rev=4a18fac42afc2256a1bb3 [[package]] name = "stack-engine" -version = "0.5.0" +version = "0.6.0" dependencies = [ "roxmltree", "serde", @@ -208,7 +208,7 @@ dependencies = [ [[package]] name = "stack-engine-wasm" -version = "0.5.0" +version = "0.6.0" dependencies = [ "js-sys", "serde", @@ -228,8 +228,8 @@ dependencies = [ [[package]] name = "stack-theme" -version = "0.4.0" -source = "git+https://github.com/stack-sh/theme.git?rev=2347315e6e86ab9d2708e05fd3f9b5f3d87e1241#2347315e6e86ab9d2708e05fd3f9b5f3d87e1241" +version = "0.5.0" +source = "git+https://github.com/stack-sh/theme.git?rev=7e208d6a3c90d255799f390a4e8b86248c73caee#7e208d6a3c90d255799f390a4e8b86248c73caee" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index d3b1e96..fa1a08a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,4 @@ serde_json = "=1.0.151" sha2 = "=0.11.0" stack-compiler = { git = "https://github.com/stack-sh/compiler.git", rev = "4a18fac42afc2256a1bb3a6ff13d12d732a391e7" } stack-formatter = { path = "crates/stack-formatter" } -stack-theme = { git = "https://github.com/stack-sh/theme.git", rev = "2347315e6e86ab9d2708e05fd3f9b5f3d87e1241" } +stack-theme = { git = "https://github.com/stack-sh/theme.git", rev = "7e208d6a3c90d255799f390a4e8b86248c73caee" } diff --git a/crates/stack-engine-wasm/Cargo.toml b/crates/stack-engine-wasm/Cargo.toml index 3aec4b3..87b471d 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.5.0" +version = "0.6.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 d15d8bc..a322254 100644 --- a/crates/stack-engine-wasm/src/lib.rs +++ b/crates/stack-engine-wasm/src/lib.rs @@ -70,6 +70,8 @@ pub struct ProviderNotice { pub archive_sha256: String, /// Provider terms URL. pub terms_url: String, + /// Every audited archive that contributed to this pack. + pub sources: Vec, /// User-visible attribution. pub attribution: String, /// User-visible terms summary. @@ -88,6 +90,30 @@ pub struct ProviderNoticeIcon { pub id: String, /// Official provider product name. pub product_name: String, + /// Rights-owner source for this brand icon, when the archive is multi-brand. + #[serde(skip_serializing_if = "Option::is_none")] + pub brand_source_url: Option, + /// Rights-owner usage guidelines for this brand icon, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub brand_guidelines_url: Option, + /// Pack-local source ID, or `primary` for the primary source. + pub source_id: String, +} + +/// JavaScript-facing audited provider source. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderNoticeSource { + /// Pack-local source ID. + pub id: String, + /// Official source page. + pub page_url: String, + /// Audited upstream release identifier. + pub release: String, + /// Complete official source archive SHA-256. + pub archive_sha256: String, + /// Terms reviewed for this source. + pub terms_url: String, } #[derive(Deserialize)] @@ -305,6 +331,11 @@ impl From for ProviderNotice { source_release: notice.source_release, archive_sha256: notice.archive_sha256, terms_url: notice.terms_url, + sources: notice + .sources + .into_iter() + .map(ProviderNoticeSource::from) + .collect(), attribution: notice.attribution, terms_summary: notice.terms_summary, non_endorsement: notice.non_endorsement, @@ -322,6 +353,21 @@ impl From for ProviderNoticeIcon { Self { id: icon.id, product_name: icon.product_name, + brand_source_url: icon.brand_source_url, + brand_guidelines_url: icon.brand_guidelines_url, + source_id: icon.source_id, + } + } +} + +impl From for ProviderNoticeSource { + fn from(source: stack_engine::ProviderNoticeSource) -> Self { + Self { + id: source.id, + page_url: source.page_url, + release: source.release, + archive_sha256: source.archive_sha256, + terms_url: source.terms_url, } } } @@ -466,6 +512,17 @@ export interface RenderResult { export interface ProviderNoticeIcon { readonly id: string; readonly productName: string; + readonly brandSourceUrl?: string; + readonly brandGuidelinesUrl?: string; + readonly sourceId: string; +} + +export interface ProviderNoticeSource { + readonly id: string; + readonly pageUrl: string; + readonly release: string; + readonly archiveSha256: string; + readonly termsUrl: string; } export interface ProviderNotice { @@ -476,6 +533,7 @@ export interface ProviderNotice { readonly sourceRelease: string; readonly archiveSha256: string; readonly termsUrl: string; + readonly sources: readonly ProviderNoticeSource[]; readonly attribution: string; readonly termsSummary: string; readonly nonEndorsement: string; @@ -631,6 +689,17 @@ fn provider_notices_to_js(notices: Vec) -> Result) -> Result, /// User-visible attribution text. pub attribution: String, /// User-visible terms summary. @@ -465,6 +467,27 @@ pub struct ProviderNoticeIcon { pub id: String, /// Official provider product name. pub product_name: String, + /// Rights-owner source for this brand icon, when the archive is multi-brand. + pub brand_source_url: Option, + /// Rights-owner usage guidelines for this brand icon, when available. + pub brand_guidelines_url: Option, + /// Pack-local source ID, or `primary` for the primary source. + pub source_id: String, +} + +/// One audited archive listed in a rendered-artifact notice. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProviderNoticeSource { + /// Pack-local source ID. The primary source always uses `primary`. + pub id: String, + /// Official source page. + pub page_url: String, + /// Audited upstream release identifier. + pub release: String, + /// Complete official source archive SHA-256. + pub archive_sha256: String, + /// Terms reviewed for this source. + pub terms_url: String, } /// Engine-owned portable diagnostic shared by native and future WASM outputs. @@ -614,7 +637,7 @@ mod tests { output.metadata.language_version, Some(LanguageVersion { major: 1, minor: 0 }) ); - assert_eq!(output.metadata.theme_catalog_version, "0.4.0"); + assert_eq!(output.metadata.theme_catalog_version, "0.5.0"); assert_eq!( output.metadata.theme_catalog_revision, stack_theme::CATALOG_REVISION @@ -658,10 +681,10 @@ mod tests { ("ai", "Artificial intelligence system"), ]; let catalog = stack_theme::catalog(); - assert_eq!(catalog.catalog_version, "0.4.0"); + assert_eq!(catalog.catalog_version, "0.5.0"); assert_eq!( stack_theme::CATALOG_REVISION, - "sha256:9cb3de8b504acbf22c93cea5fbea66be50f38734dc1dee18b9cab7084082cc1f" + "sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53" ); for theme in &catalog.themes { for (identifier, subject) in expected_icons { @@ -680,7 +703,7 @@ mod tests { let rendered = Engine::bundled().render(source)?; assert!(checked.diagnostics.is_empty()); assert!(rendered.diagnostics.is_empty()); - assert_eq!(rendered.metadata.theme_catalog_version, "0.4.0"); + assert_eq!(rendered.metadata.theme_catalog_version, "0.5.0"); assert_eq!( rendered.metadata.theme_catalog_revision, stack_theme::CATALOG_REVISION diff --git a/crates/stack-engine/src/provider.rs b/crates/stack-engine/src/provider.rs index 86369f9..4cf6cf7 100644 --- a/crates/stack-engine/src/provider.rs +++ b/crates/stack-engine/src/provider.rs @@ -7,6 +7,7 @@ use roxmltree::{Document, NodeType}; use sha2::{Digest, Sha256}; use stack_theme::{ ProviderIcon, ProviderPack as ProviderPackManifest, ProviderPackPermittedOutput, + ProviderPackSource, }; use crate::{OperationResult, OperationalError}; @@ -16,11 +17,13 @@ const MAX_PROVIDER_PACK_BYTES: usize = 32 * 1024 * 1024; const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; const ALLOWED_ELEMENTS: &[&str] = &[ "circle", + "clipPath", "defs", "ellipse", "g", "line", "linearGradient", + "mask", "path", "polygon", "polyline", @@ -31,16 +34,24 @@ const ALLOWED_ELEMENTS: &[&str] = &[ ]; const ALLOWED_ATTRIBUTES: &[&str] = &[ "aria-hidden", + "clip-path", "clip-rule", "cx", "cy", "d", "fill", + "fill-opacity", "fill-rule", + "fx", + "fy", "gradientTransform", "gradientUnits", "height", + "href", "id", + "isolation", + "mask", + "maskUnits", "opacity", "offset", "points", @@ -53,6 +64,7 @@ const ALLOWED_ATTRIBUTES: &[&str] = &[ "stroke", "stroke-linecap", "stroke-linejoin", + "stroke-miterlimit", "stroke-width", "transform", "viewBox", @@ -153,9 +165,16 @@ impl ProviderPack { fn validate_manifest_boundary(manifest: &ProviderPackManifest) -> OperationResult<()> { let redistribution = &manifest.rights.redistribution; let processing = &manifest.rights.processing; - if manifest.schema_version != "1.0" + if !matches!(manifest.schema_version.as_str(), "1.0" | "1.1") || manifest.icons.is_empty() || manifest.icons.len() > 10_000 + || manifest.additional_sources.len() > 8 + || (manifest.schema_version == "1.0" + && (!manifest.additional_sources.is_empty() + || manifest + .icons + .iter() + .any(|icon| icon.asset.source_id.is_some()))) || !manifest.rights.terms_acceptance_required || !manifest .rights @@ -178,8 +197,7 @@ fn validate_manifest_boundary(manifest: &ProviderPackManifest) -> OperationResul )); } if !valid_provider_id(&manifest.provider.id) - || !valid_sha256(&manifest.source.archive_sha256) - || manifest.source.terms_url.is_empty() + || !valid_source(&manifest.source) || manifest.notice.attribution.is_empty() || manifest.notice.terms_summary.is_empty() || manifest.notice.non_endorsement.is_empty() @@ -187,6 +205,19 @@ fn validate_manifest_boundary(manifest: &ProviderPackManifest) -> OperationResul return Err(invalid_pack("provider pack identity or notice is invalid")); } + let mut source_ids = BTreeSet::new(); + for additional in &manifest.additional_sources { + if additional.id == "primary" + || !valid_provider_id(&additional.id) + || !source_ids.insert(additional.id.as_str()) + || !valid_source(&additional.source) + { + return Err(invalid_pack( + "provider pack contains an invalid or duplicate source", + )); + } + } + let prefix = format!("{}:", manifest.provider.id); let mut identifiers = BTreeSet::new(); let mut paths = BTreeSet::new(); @@ -198,6 +229,19 @@ fn validate_manifest_boundary(manifest: &ProviderPackManifest) -> OperationResul || !paths.insert(icon.asset.path.as_str()) || !valid_sha256(&icon.asset.original_sha256) || !valid_sha256(&icon.asset.processed_sha256) + || icon + .brand_source_url + .as_deref() + .is_some_and(|url| !valid_https_url(url)) + || icon + .brand_guidelines_url + .as_deref() + .is_some_and(|url| !valid_https_url(url)) + || icon + .asset + .source_id + .as_deref() + .is_some_and(|source_id| !source_ids.contains(source_id)) || icon.asset.view_box[2] <= 0 || icon.asset.view_box[3] <= 0 { @@ -209,6 +253,17 @@ fn validate_manifest_boundary(manifest: &ProviderPackManifest) -> OperationResul Ok(()) } +fn valid_source(source: &ProviderPackSource) -> bool { + valid_sha256(&source.archive_sha256) + && !source.page_url.is_empty() + && !source.terms_url.is_empty() + && !source.release.is_empty() +} + +fn valid_https_url(value: &str) -> bool { + value.starts_with("https://") && !value.bytes().any(|byte| byte.is_ascii_whitespace()) +} + fn validate_assets( manifest: &ProviderPackManifest, assets: &[ProviderAsset], @@ -285,9 +340,16 @@ fn validate_svg(svg: &str, expected_view_box: [i32; 4]) -> OperationResult<()> { || node.tag_name().namespace() != Some(SVG_NAMESPACE) || (name == "svg" && node != root) || (name == "defs" && parent_name != Some("svg")) - || (matches!(name, "linearGradient" | "radialGradient") && parent_name != Some("defs")) + || (matches!( + name, + "linearGradient" | "radialGradient" | "clipPath" | "mask" + ) && parent_name != Some("defs")) || (name == "stop" && !matches!(parent_name, Some("linearGradient" | "radialGradient"))) - || (parent_name == Some("defs") && !matches!(name, "linearGradient" | "radialGradient")) + || (parent_name == Some("defs") + && !matches!( + name, + "linearGradient" | "radialGradient" | "clipPath" | "mask" + )) { return Err(unsafe_svg()); } @@ -300,14 +362,24 @@ fn validate_svg(svg: &str, expected_view_box: [i32; 4]) -> OperationResult<()> { return Err(unsafe_svg()); } if attribute_name == "id" - && (!matches!(name, "linearGradient" | "radialGradient") - || !attribute.value().starts_with("stack-") + && (!matches!( + name, + "linearGradient" | "radialGradient" | "clipPath" | "mask" + ) || !attribute.value().starts_with("stack-") || !declared.insert(attribute.value())) { return Err(unsafe_svg()); } - if let Some(identifier) = local_reference(attribute.value()) { - if !matches!(attribute_name, "fill" | "stroke") { + if let Some(identifier) = local_url_reference(attribute.value()) { + if !matches!(attribute_name, "fill" | "stroke" | "clip-path" | "mask") { + return Err(unsafe_svg()); + } + referenced.insert(identifier); + } else if attribute_name == "href" { + let Some(identifier) = fragment_reference(attribute.value()) else { + return Err(unsafe_svg()); + }; + if !matches!(name, "linearGradient" | "radialGradient") { return Err(unsafe_svg()); } referenced.insert(identifier); @@ -369,13 +441,17 @@ fn parse_view_box(value: Option<&str>) -> Option<[i32; 4]> { .then(|| [values[0], values[1], values[2], values[3]]) } -fn local_reference(value: &str) -> Option<&str> { +fn local_url_reference(value: &str) -> Option<&str> { value .strip_prefix("url(#") .and_then(|value| value.strip_suffix(')')) .filter(|value| !value.is_empty()) } +fn fragment_reference(value: &str) -> Option<&str> { + value.strip_prefix('#').filter(|value| !value.is_empty()) +} + fn contains_unsafe_reference(value: &str) -> bool { let lowercase = value.to_ascii_lowercase(); lowercase.contains("url(") @@ -437,6 +513,7 @@ mod tests { use std::error::Error; use serde::Deserialize; + use stack_theme::ProviderPackAdditionalSource; use super::*; use crate::{Engine, Severity}; @@ -516,7 +593,10 @@ mod tests { assert!(svg.contains(packs[0].revision())); assert_eq!(first.provider_notices.len(), 1); assert_eq!(first.provider_notices[0].provider_id, "example"); + assert_eq!(first.provider_notices[0].sources.len(), 1); + assert_eq!(first.provider_notices[0].sources[0].id, "primary"); assert_eq!(first.provider_notices[0].icons[0].id, "example:storage"); + assert_eq!(first.provider_notices[0].icons[0].source_id, "primary"); assert_eq!( first.provider_notices[0].icons[0].product_name, "Example Storage" @@ -541,6 +621,87 @@ mod tests { Ok(()) } + #[test] + fn multi_source_pack_validates_and_reports_exact_icon_provenance() -> Result<(), Box> + { + let mut input = fixture_input()?; + input.manifest.schema_version = "1.1".to_owned(); + input + .manifest + .additional_sources + .push(ProviderPackAdditionalSource { + id: "categories".to_owned(), + source: input.manifest.source.clone(), + }); + input.manifest.icons[0].asset.source_id = Some("categories".to_owned()); + input.manifest.icons[0].brand_source_url = Some("https://example.com/brand".to_owned()); + input.manifest.icons[0].brand_guidelines_url = + Some("https://example.com/guidelines".to_owned()); + let pack = ProviderPack::new( + input.manifest, + input + .assets + .into_iter() + .map(|asset| ProviderAsset::new(asset.path, asset.svg)) + .collect(), + )?; + let packs = [pack]; + let output = Engine::with_provider_packs(&packs)?.render( + b"stack 1.0 diagram \"Provider\" { node item \"Storage\" { icon \"example:storage\" } }", + )?; + + assert_eq!(output.provider_notices[0].sources.len(), 2); + assert_eq!(output.provider_notices[0].sources[1].id, "categories"); + assert_eq!(output.provider_notices[0].icons[0].source_id, "categories"); + assert_eq!( + output.provider_notices[0].icons[0] + .brand_guidelines_url + .as_deref(), + Some("https://example.com/guidelines") + ); + Ok(()) + } + + #[test] + fn multi_source_pack_rejects_duplicate_unknown_and_version_mismatched_sources() + -> Result<(), Box> { + let input = fixture_input()?; + let assets = input + .assets + .iter() + .map(|asset| ProviderAsset::new(&asset.path, &asset.svg)) + .collect::>(); + + let mut duplicate = input.manifest.clone(); + duplicate.schema_version = "1.1".to_owned(); + duplicate.additional_sources = vec![ + ProviderPackAdditionalSource { + id: "categories".to_owned(), + source: duplicate.source.clone(), + }, + ProviderPackAdditionalSource { + id: "categories".to_owned(), + source: duplicate.source.clone(), + }, + ]; + assert!(ProviderPack::new(duplicate, assets.clone()).is_err()); + + let mut unknown = input.manifest.clone(); + unknown.schema_version = "1.1".to_owned(); + unknown.icons[0].asset.source_id = Some("categories".to_owned()); + assert!(ProviderPack::new(unknown, assets.clone()).is_err()); + + let mut version_mismatch = input.manifest; + version_mismatch + .additional_sources + .push(ProviderPackAdditionalSource { + id: "categories".to_owned(), + source: version_mismatch.source.clone(), + }); + assert!(ProviderPack::new(version_mismatch, assets).is_err()); + Ok(()) + } + #[test] fn manifest_boundary_rejects_invalid_rights_identity_and_records() -> Result<(), Box> { @@ -686,6 +847,12 @@ mod tests { assert!(pack_with_svg(svg).is_ok()); } + #[test] + fn namespaced_local_clip_paths_masks_and_gradient_inheritance_are_accepted() { + let svg = ""; + assert!(pack_with_svg(svg).is_ok()); + } + #[test] fn duplicate_provider_namespaces_and_excessive_pack_counts_are_rejected() -> Result<(), Box> { diff --git a/crates/stack-engine/src/resources.rs b/crates/stack-engine/src/resources.rs index a783443..ea0fb3c 100644 --- a/crates/stack-engine/src/resources.rs +++ b/crates/stack-engine/src/resources.rs @@ -3,7 +3,7 @@ use stack_compiler::ir::{Diagram, NodeKind}; use stack_theme::{Catalog, FontMetrics, NodeVisual, ProviderIcon, Theme}; -use crate::{ProviderNotice, ProviderNoticeIcon, ProviderPack}; +use crate::{ProviderNotice, ProviderNoticeIcon, ProviderNoticeSource, ProviderPack}; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum ResourceWarning { @@ -151,6 +151,22 @@ impl<'catalog> Resources<'catalog> { }) .unwrap_or_else(|| { let manifest = resolved.pack.manifest(); + let mut sources = vec![ProviderNoticeSource { + id: "primary".to_owned(), + page_url: manifest.source.page_url.clone(), + release: manifest.source.release.clone(), + archive_sha256: manifest.source.archive_sha256.clone(), + terms_url: manifest.source.terms_url.clone(), + }]; + sources.extend(manifest.additional_sources.iter().map(|additional| { + ProviderNoticeSource { + id: additional.id.clone(), + page_url: additional.source.page_url.clone(), + release: additional.source.release.clone(), + archive_sha256: additional.source.archive_sha256.clone(), + terms_url: additional.source.terms_url.clone(), + } + })); notices.push(ProviderNotice { provider_id: manifest.provider.id.clone(), provider_name: manifest.provider.name.clone(), @@ -159,6 +175,7 @@ impl<'catalog> Resources<'catalog> { source_release: manifest.source.release.clone(), archive_sha256: manifest.source.archive_sha256.clone(), terms_url: manifest.source.terms_url.clone(), + sources, attribution: manifest.notice.attribution.clone(), terms_summary: manifest.notice.terms_summary.clone(), non_endorsement: manifest.notice.non_endorsement.clone(), @@ -174,6 +191,14 @@ impl<'catalog> Resources<'catalog> { notices[notice_index].icons.push(ProviderNoticeIcon { id: resolved.icon.id.clone(), product_name: resolved.icon.product_name.clone(), + brand_source_url: resolved.icon.brand_source_url.clone(), + brand_guidelines_url: resolved.icon.brand_guidelines_url.clone(), + source_id: resolved + .icon + .asset + .source_id + .clone() + .unwrap_or_else(|| "primary".to_owned()), }); } } diff --git a/crates/stack-engine/tests/render_snapshots.rs b/crates/stack-engine/tests/render_snapshots.rs index d4973fa..8e5a776 100644 --- a/crates/stack-engine/tests/render_snapshots.rs +++ b/crates/stack-engine/tests/render_snapshots.rs @@ -53,7 +53,7 @@ fn explicit_core_icon_matches_standalone_svg_snapshot() -> Result<(), Box - + Complete semantics Architecture diagram with 10 nodes, 3 groups, and 8 relationships. - stack-engine 0.5.0; language 1.0; theme 0.4.0 at sha256:9cb3de8b504acbf22c93cea5fbea66be50f38734dc1dee18b9cab7084082cc1f + stack-engine 0.6.0; language 1.0; theme 0.5.0 at sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53 diff --git a/crates/stack-engine/tests/snapshots/render/default-normalization.svg b/crates/stack-engine/tests/snapshots/render/default-normalization.svg index c8d7383..9fc4e47 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.5.0; language 1.0; theme 0.4.0 at sha256:9cb3de8b504acbf22c93cea5fbea66be50f38734dc1dee18b9cab7084082cc1f + stack-engine 0.6.0; language 1.0; theme 0.5.0 at sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53 diff --git a/crates/stack-engine/tests/snapshots/render/explicit-core-icon.svg b/crates/stack-engine/tests/snapshots/render/explicit-core-icon.svg index c817a2c..d466b37 100644 --- a/crates/stack-engine/tests/snapshots/render/explicit-core-icon.svg +++ b/crates/stack-engine/tests/snapshots/render/explicit-core-icon.svg @@ -1,8 +1,8 @@ - + Core icon Architecture diagram with 1 node, 0 groups, and 0 relationships. - stack-engine 0.5.0; language 1.0; theme 0.4.0 at sha256:9cb3de8b504acbf22c93cea5fbea66be50f38734dc1dee18b9cab7084082cc1f + stack-engine 0.6.0; language 1.0; theme 0.5.0 at sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53 diff --git a/package-lock.json b/package-lock.json index 329c72b..6166268 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stack-engine-workspace", - "version": "0.5.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stack-engine-workspace", - "version": "0.5.0", + "version": "0.6.0", "workspaces": [ "packages/engine" ], @@ -395,7 +395,7 @@ }, "packages/engine": { "name": "@stack-sh/engine", - "version": "0.5.0", + "version": "0.6.0", "license": "Apache-2.0" } } diff --git a/package.json b/package.json index f08c0f5..973f49e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "stack-engine-workspace", "private": true, - "version": "0.5.0", + "version": "0.6.0", "workspaces": [ "packages/engine" ], diff --git a/packages/engine/package.json b/packages/engine/package.json index 24ff5ef..2558d70 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -1,6 +1,6 @@ { "name": "@stack-sh/engine", - "version": "0.5.0", + "version": "0.6.0", "description": "Browser WebAssembly adapter for Stack diagram operations", "type": "module", "license": "Apache-2.0", diff --git a/scripts/validate-svg.py b/scripts/validate-svg.py index ab0528a..6789a9b 100644 --- a/scripts/validate-svg.py +++ b/scripts/validate-svg.py @@ -88,10 +88,10 @@ def main() -> None: "association", } assert values(explicit_icon, "data-icon-id") == {"gateway"} - assert explicit_icon.attrib.get("data-theme-version") == "0.4.0" + assert explicit_icon.attrib.get("data-theme-version") == "0.5.0" assert ( explicit_icon.attrib.get("data-theme-revision") - == "sha256:9cb3de8b504acbf22c93cea5fbea66be50f38734dc1dee18b9cab7084082cc1f" + == "sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53" ) diff --git a/tests/wasm.test.mjs b/tests/wasm.test.mjs index 7799ee5..018f256 100644 --- a/tests/wasm.test.mjs +++ b/tests/wasm.test.mjs @@ -81,7 +81,7 @@ test("browser diagnostics preserve actionable compiler guidance", () => { ); assert.ok(actionable); assert.equal(actionable.render.svg, null); - assert.equal(actionable.check.metadata.engineVersion, "0.5.0"); + assert.equal(actionable.check.metadata.engineVersion, "0.6.0"); assert.deepEqual(actionable.check.diagnostics[0], { code: "STK2002", severity: "error", @@ -103,11 +103,11 @@ test("browser rendering resolves the bundled explicit core icon", () => { assert.ok(explicitIcon); assert.deepEqual(explicitIcon.check.diagnostics, []); assert.deepEqual(explicitIcon.render.diagnostics, []); - assert.equal(explicitIcon.render.metadata.engineVersion, "0.5.0"); - assert.equal(explicitIcon.render.metadata.themeCatalogVersion, "0.4.0"); + assert.equal(explicitIcon.render.metadata.engineVersion, "0.6.0"); + assert.equal(explicitIcon.render.metadata.themeCatalogVersion, "0.5.0"); assert.equal( explicitIcon.render.metadata.themeCatalogRevision, - "sha256:9cb3de8b504acbf22c93cea5fbea66be50f38734dc1dee18b9cab7084082cc1f", + "sha256:3bfd66e1a96628b29b95b7273b54373bcce952f7285aefa506b4255a629eaf53", ); assert.match(explicitIcon.render.svg, /data-icon-id="gateway"/); assert.doesNotMatch(explicitIcon.render.svg, /data-icon-id="kind-external"/); @@ -125,8 +125,18 @@ test("browser rendering resolves local provider packs with native provenance", ( assert.match(rendered.svg, /fill="#4285f4"/); assert.equal(rendered.providerNotices.length, 1); assert.equal(rendered.providerNotices[0].providerId, "example"); + assert.deepEqual(rendered.providerNotices[0].sources, [ + { + id: "primary", + pageUrl: "https://example.com/icons", + release: "fixture-1", + archiveSha256: + "sha256:0000000000000000000000000000000000000000000000000000000000000000", + termsUrl: "https://example.com/terms", + }, + ]); assert.deepEqual(rendered.providerNotices[0].icons, [ - { id: "example:storage", productName: "Example Storage" }, + { id: "example:storage", productName: "Example Storage", sourceId: "primary" }, ]); assert.match(rendered.providerNotices[0].packRevision, /^sha256:[0-9a-f]{64}$/); });