openpencil/tools/check-jian-boundaries.sh

117 lines
5 KiB
Bash
Raw Normal View History

feat(shell-native): Step 1a Task 4 — basic_window demo + acceptance + Phase C Gate Phase C Task 4 closes Step 1a (G1 shared Skia context) on v0.8.0: - crates/openpencil-shell-native/examples/basic_window.rs: winit + SharedSkiaContext::new_desktop + NativeBackend (Jian DrawOp) + JianPointerMapper integration. Paints chrome rect + "Hello 你好" + box outline; close → idempotent teardown. Demonstrates Phase B Task 3 winit → Jian PointerTranslator → JianPointerMapper → ShellEvent pipeline end-to-end. - crates/openpencil-shell-native/notes/step-1a-{macos,linux,windows}-manual-smoke.md: manual GPU smoke runbooks for spec §1.2 acceptance #1 (macOS PASS recorded; Linux/Windows pending real-hardware run, deferred per CONCERN-R5-1 + WINDOWS_GPU_DEFERRED_NO_RUNNER). - tools/check-jian-boundaries.sh: spec §11 + §12.3 invariants. Verifies that openpencil-app has no direct jian-* dep, mobile (aarch64-linux-android, aarch64-apple-ios) and wasm32 closures exclude jian-host-desktop / jian-skia, and openpencil-shell-web declares no jian-host-desktop dep at the manifest level. - .github/workflows/rust-check.yml: wires bash tools/check-jian-boundaries.sh on Linux runner with mobile + wasm32 targets installed. - README.md: roadmap entry for the Step 1a milestone. Verified locally on macOS aarch64: - cargo fmt --all -- --check - cargo clippy --workspace --all-targets -- -D warnings - cargo build --examples --workspace - cargo test --workspace (38 PASS, 0 FAIL, 0 IGNORED) - cargo check -p openpencil-shell-native --target {aarch64-linux-android, aarch64-apple-ios} - bash tools/check-jian-boundaries.sh (4 invariants PASS) - spec §11 invariants 1–4 grep checks PASS Spec v19.3 FROZEN (openpencil-docs 651090d); Plan v7 FROZEN. vendor/jian pinned at c4a794dc.
2026-05-05 14:39:00 +00:00
#!/usr/bin/env bash
# Step 1a Phase C Task 4 / spec v19 §11 + §12.3 boundary invariants.
#
# Verifies the following Jian crate boundary invariants from outside the
# Rust build system. Run from the repo root.
#
# Invariant 1 (§12.3): openpencil-app must NOT depend directly on any
# `jian-*` crate — Jian is a shell-native implementation detail; the
# app only sees OP's `RenderBackend` / `ShellEvent` facade.
#
feat(shell-native): extend widget stack to iOS + Android cargo check Per 2026-05-10 user directive ("extend, jian 最后也会需要 ios 和 android"): lift the desktop-only cfg gate so the widget render stack (skia-safe + jian-skia + NativeBackend + widget_host) compiles for iOS (`aarch64-apple-ios`) AND Android (`aarch64-linux-android`) cargo check too. Mobile shells now have a real widget-rendering surface to target in Step 1f, and the "shell-core widgets are platform-agnostic" claim from spec §1.4 is now compile-verified across desktop trio + mobile pair + wasm. Cargo.toml restructure (`crates/openpencil-shell-native/Cargo.toml`): - New `[target.'cfg(any(macos, linux, windows, ios, android))']` block for the cross-platform widget stack: `skia-safe = "0.97"` (default-features = false; binary-cache + textlayout) and `jian-skia` (textlayout). Both pull on every desktop trio + mobile pair target. - Existing desktop-only block kept for the GUI host stack: adds `gl` to skia-safe's features (iOS deprecated GL — Metal goes in Step 1f; Android GL/Vulkan via the platform provider not via skia-safe's bundled bindings here), plus glutin / glutin- winit / winit / scopeguard / jian-host-desktop. Cargo deduplicates: skia-safe resolves to one crate-version with feature-union (binary-cache + textlayout from the wider block + gl from the desktop block on desktop-only). src/lib.rs gate lift: - `pub mod backend;` and `pub mod widget_host;` cfg now includes `target_os = "ios"` and `target_os = "android"`. `pub use` re-exports follow. - `canvas_view_stub` stays desktop-only (uses glow GL-isolation probe with no mobile equivalent). - Comment block at the cfg site cites the user directive + Step 1f handoff (real EaglProvider / AndroidEglProvider impls + Metal / Vulkan / event integration). Boundary script revision (`tools/check-jian-boundaries.sh`): - Invariant 2 was: mobile targets must NOT pull jian-host-desktop OR jian-skia. Per the user directive, jian-skia is now ALLOWED on mobile (the widget render stack uses it). jian-host-desktop remains forbidden — it carries winit / glutin / desktop GLContextProvider impls that have no mobile equivalent. - Header comment block + active grep narrowed accordingly. The Step 1f path through EaglProvider / AndroidEglProvider is the spec-blessed mobile host plugin point (no IPC / CLI needed). Verification: - `cargo check -p openpencil-shell-native --target aarch64-apple-ios` — green (skia-bindings + jian-skia + shell-native all compile) - `cargo check -p openpencil-shell-native --target aarch64-linux-android` — green (same) - `cargo check -p openpencil-shell-native` — green (no desktop regression) - `cargo check -p openpencil-shell-core --target wasm32-unknown-unknown` — green (shell-core stays wasm32-clean) - `cargo test -p openpencil-shell-core --test widgets_static` — 21/21 passing (widget logic untouched) - `bash tools/check-wasm-bundle.sh` — PASS: - 0 env.* imports - 622 156 bytes gzip = 59% of 1 MiB ceiling (no web regression) - `bash tools/check-widget-boundary.sh` — PASS - `bash tools/check-jian-boundaries.sh` — 4/4 invariants pass (Invariant 2 revised to allow jian-skia on mobile) What's still mobile-pending (Step 1f scope): - `EaglProvider` (iOS) — Metal-backed `GlContextProvider` impl (skia-safe `metal` feature when iOS host actually runs) - `AndroidEglProvider` (Android) — GL/Vulkan-backed impl - Mobile host runners (UIKit AppDelegate / Activity wrappers) - Mobile event translation (jian-host-ios / jian-host-android — siblings of jian-host-desktop) - `inspector_window` example is desktop-only by design (winit + SharedSkiaContext::new_desktop); mobile shells will land their own UIKit / Activity runners that consume the SAME `WidgetHostNative::paint(&mut frame, width)` surface The widget glue itself (NativeFrameBackend + WidgetHostNative) is platform-agnostic in shape — no winit / glutin / EGL types leak in. Step 1f mobile work plugs in providers, not widgets.
2026-05-10 02:16:36 +00:00
# Invariant 2 (§11.1, §12.3 — REVISED 2026-05-10): mobile targets
# (`aarch64-linux-android`, `aarch64-apple-ios`) must NOT pull
# `jian-host-desktop` (desktop GUI host glue — winit / glutin /
# GLContextProvider impl that has no mobile equivalent). Per the
# 2026-05-10 user directive ("extend, jian 最后也会需要 ios 和
# android"), `jian-skia` is now ALLOWED on mobile targets — the
# widget render stack (skia-safe + jian-skia + NativeBackend +
# widget_host) compiles for iOS / Android cargo check, leaving
# only the host-specific (Metal / Vulkan / event) integration to
# Step 1f via the existing `EaglProvider` / `AndroidEglProvider`
# plugin point.
feat(shell-native): Step 1a Task 4 — basic_window demo + acceptance + Phase C Gate Phase C Task 4 closes Step 1a (G1 shared Skia context) on v0.8.0: - crates/openpencil-shell-native/examples/basic_window.rs: winit + SharedSkiaContext::new_desktop + NativeBackend (Jian DrawOp) + JianPointerMapper integration. Paints chrome rect + "Hello 你好" + box outline; close → idempotent teardown. Demonstrates Phase B Task 3 winit → Jian PointerTranslator → JianPointerMapper → ShellEvent pipeline end-to-end. - crates/openpencil-shell-native/notes/step-1a-{macos,linux,windows}-manual-smoke.md: manual GPU smoke runbooks for spec §1.2 acceptance #1 (macOS PASS recorded; Linux/Windows pending real-hardware run, deferred per CONCERN-R5-1 + WINDOWS_GPU_DEFERRED_NO_RUNNER). - tools/check-jian-boundaries.sh: spec §11 + §12.3 invariants. Verifies that openpencil-app has no direct jian-* dep, mobile (aarch64-linux-android, aarch64-apple-ios) and wasm32 closures exclude jian-host-desktop / jian-skia, and openpencil-shell-web declares no jian-host-desktop dep at the manifest level. - .github/workflows/rust-check.yml: wires bash tools/check-jian-boundaries.sh on Linux runner with mobile + wasm32 targets installed. - README.md: roadmap entry for the Step 1a milestone. Verified locally on macOS aarch64: - cargo fmt --all -- --check - cargo clippy --workspace --all-targets -- -D warnings - cargo build --examples --workspace - cargo test --workspace (38 PASS, 0 FAIL, 0 IGNORED) - cargo check -p openpencil-shell-native --target {aarch64-linux-android, aarch64-apple-ios} - bash tools/check-jian-boundaries.sh (4 invariants PASS) - spec §11 invariants 1–4 grep checks PASS Spec v19.3 FROZEN (openpencil-docs 651090d); Plan v7 FROZEN. vendor/jian pinned at c4a794dc.
2026-05-05 14:39:00 +00:00
#
# Invariant 3 (§11.1, §1.2): wasm32 builds of `openpencil-shell-web`
# must NOT pull `jian-host-desktop` or `jian-skia` (skia-safe build.rs
# fails on wasm32; Jian-core is wasm32-clean per P0.5 and is the only
# Jian crate allowed in the bundle).
#
# Invariant 4 (§1.2): `openpencil-shell-web` must NOT depend on
# `jian-host-desktop` at all — even as a non-default optional dep.
#
# Exit codes:
# 0 — all invariants pass.
# 1+ — one or more invariants fail; the failing crate names are
# echoed before the script exits.
#
# Dependencies: `cargo`, `jq` (for cargo metadata JSON parsing).
set -euo pipefail
if ! command -v jq >/dev/null 2>&1; then
echo "check-jian-boundaries.sh: \`jq\` is required but not installed." >&2
echo " apt: sudo apt-get install -y jq" >&2
echo " brew: brew install jq" >&2
exit 2
fi
# ── Invariant 1: openpencil-app has no direct jian-* dependency. ──────
# `cargo metadata` returns a workspace-wide resolve graph; we filter
# the `resolve.nodes[]` entry whose name matches `openpencil-app` and
# inspect its direct `deps[]`. A direct dep on any `jian-*` crate
# fails the invariant.
metadata_full="$(cargo metadata --format-version 1)"
forbidden_app="$(echo "$metadata_full" | jq -r '
[.packages[] | select(.name == "openpencil-app") | .id] as $app_ids
| .resolve.nodes[]
| select(.id as $id | $app_ids | index($id))
| .deps[].name
' | grep -E '^jian-' || true)"
if [ -n "$forbidden_app" ]; then
echo "INVARIANT 1 FAILED: openpencil-app directly depends on jian-* crate(s):" >&2
echo "$forbidden_app" >&2
exit 1
fi
feat(shell-native): extend widget stack to iOS + Android cargo check Per 2026-05-10 user directive ("extend, jian 最后也会需要 ios 和 android"): lift the desktop-only cfg gate so the widget render stack (skia-safe + jian-skia + NativeBackend + widget_host) compiles for iOS (`aarch64-apple-ios`) AND Android (`aarch64-linux-android`) cargo check too. Mobile shells now have a real widget-rendering surface to target in Step 1f, and the "shell-core widgets are platform-agnostic" claim from spec §1.4 is now compile-verified across desktop trio + mobile pair + wasm. Cargo.toml restructure (`crates/openpencil-shell-native/Cargo.toml`): - New `[target.'cfg(any(macos, linux, windows, ios, android))']` block for the cross-platform widget stack: `skia-safe = "0.97"` (default-features = false; binary-cache + textlayout) and `jian-skia` (textlayout). Both pull on every desktop trio + mobile pair target. - Existing desktop-only block kept for the GUI host stack: adds `gl` to skia-safe's features (iOS deprecated GL — Metal goes in Step 1f; Android GL/Vulkan via the platform provider not via skia-safe's bundled bindings here), plus glutin / glutin- winit / winit / scopeguard / jian-host-desktop. Cargo deduplicates: skia-safe resolves to one crate-version with feature-union (binary-cache + textlayout from the wider block + gl from the desktop block on desktop-only). src/lib.rs gate lift: - `pub mod backend;` and `pub mod widget_host;` cfg now includes `target_os = "ios"` and `target_os = "android"`. `pub use` re-exports follow. - `canvas_view_stub` stays desktop-only (uses glow GL-isolation probe with no mobile equivalent). - Comment block at the cfg site cites the user directive + Step 1f handoff (real EaglProvider / AndroidEglProvider impls + Metal / Vulkan / event integration). Boundary script revision (`tools/check-jian-boundaries.sh`): - Invariant 2 was: mobile targets must NOT pull jian-host-desktop OR jian-skia. Per the user directive, jian-skia is now ALLOWED on mobile (the widget render stack uses it). jian-host-desktop remains forbidden — it carries winit / glutin / desktop GLContextProvider impls that have no mobile equivalent. - Header comment block + active grep narrowed accordingly. The Step 1f path through EaglProvider / AndroidEglProvider is the spec-blessed mobile host plugin point (no IPC / CLI needed). Verification: - `cargo check -p openpencil-shell-native --target aarch64-apple-ios` — green (skia-bindings + jian-skia + shell-native all compile) - `cargo check -p openpencil-shell-native --target aarch64-linux-android` — green (same) - `cargo check -p openpencil-shell-native` — green (no desktop regression) - `cargo check -p openpencil-shell-core --target wasm32-unknown-unknown` — green (shell-core stays wasm32-clean) - `cargo test -p openpencil-shell-core --test widgets_static` — 21/21 passing (widget logic untouched) - `bash tools/check-wasm-bundle.sh` — PASS: - 0 env.* imports - 622 156 bytes gzip = 59% of 1 MiB ceiling (no web regression) - `bash tools/check-widget-boundary.sh` — PASS - `bash tools/check-jian-boundaries.sh` — 4/4 invariants pass (Invariant 2 revised to allow jian-skia on mobile) What's still mobile-pending (Step 1f scope): - `EaglProvider` (iOS) — Metal-backed `GlContextProvider` impl (skia-safe `metal` feature when iOS host actually runs) - `AndroidEglProvider` (Android) — GL/Vulkan-backed impl - Mobile host runners (UIKit AppDelegate / Activity wrappers) - Mobile event translation (jian-host-ios / jian-host-android — siblings of jian-host-desktop) - `inspector_window` example is desktop-only by design (winit + SharedSkiaContext::new_desktop); mobile shells will land their own UIKit / Activity runners that consume the SAME `WidgetHostNative::paint(&mut frame, width)` surface The widget glue itself (NativeFrameBackend + WidgetHostNative) is platform-agnostic in shape — no winit / glutin / EGL types leak in. Step 1f mobile work plugs in providers, not widgets.
2026-05-10 02:16:36 +00:00
# ── Invariant 2: mobile targets don't pull jian-host-desktop. ─────────
# `cargo tree` honours `--target` cfg-gates so only the deps that
# actually compile under the mobile target are listed. `jian-skia` IS
# allowed on mobile (2026-05-10 user directive — widget render stack
# extends to iOS / Android); we still forbid `jian-host-desktop`
# because it pulls winit / glutin / desktop GLContextProvider impls
# that have no mobile equivalent.
feat(shell-native): Step 1a Task 4 — basic_window demo + acceptance + Phase C Gate Phase C Task 4 closes Step 1a (G1 shared Skia context) on v0.8.0: - crates/openpencil-shell-native/examples/basic_window.rs: winit + SharedSkiaContext::new_desktop + NativeBackend (Jian DrawOp) + JianPointerMapper integration. Paints chrome rect + "Hello 你好" + box outline; close → idempotent teardown. Demonstrates Phase B Task 3 winit → Jian PointerTranslator → JianPointerMapper → ShellEvent pipeline end-to-end. - crates/openpencil-shell-native/notes/step-1a-{macos,linux,windows}-manual-smoke.md: manual GPU smoke runbooks for spec §1.2 acceptance #1 (macOS PASS recorded; Linux/Windows pending real-hardware run, deferred per CONCERN-R5-1 + WINDOWS_GPU_DEFERRED_NO_RUNNER). - tools/check-jian-boundaries.sh: spec §11 + §12.3 invariants. Verifies that openpencil-app has no direct jian-* dep, mobile (aarch64-linux-android, aarch64-apple-ios) and wasm32 closures exclude jian-host-desktop / jian-skia, and openpencil-shell-web declares no jian-host-desktop dep at the manifest level. - .github/workflows/rust-check.yml: wires bash tools/check-jian-boundaries.sh on Linux runner with mobile + wasm32 targets installed. - README.md: roadmap entry for the Step 1a milestone. Verified locally on macOS aarch64: - cargo fmt --all -- --check - cargo clippy --workspace --all-targets -- -D warnings - cargo build --examples --workspace - cargo test --workspace (38 PASS, 0 FAIL, 0 IGNORED) - cargo check -p openpencil-shell-native --target {aarch64-linux-android, aarch64-apple-ios} - bash tools/check-jian-boundaries.sh (4 invariants PASS) - spec §11 invariants 1–4 grep checks PASS Spec v19.3 FROZEN (openpencil-docs 651090d); Plan v7 FROZEN. vendor/jian pinned at c4a794dc.
2026-05-05 14:39:00 +00:00
for target in aarch64-linux-android aarch64-apple-ios; do
tree_mobile="$(cargo tree -p openpencil-shell-native \
--target "$target" \
--prefix none \
--edges normal,build 2>/dev/null || true)"
forbidden_mobile="$(echo "$tree_mobile" \
feat(shell-native): extend widget stack to iOS + Android cargo check Per 2026-05-10 user directive ("extend, jian 最后也会需要 ios 和 android"): lift the desktop-only cfg gate so the widget render stack (skia-safe + jian-skia + NativeBackend + widget_host) compiles for iOS (`aarch64-apple-ios`) AND Android (`aarch64-linux-android`) cargo check too. Mobile shells now have a real widget-rendering surface to target in Step 1f, and the "shell-core widgets are platform-agnostic" claim from spec §1.4 is now compile-verified across desktop trio + mobile pair + wasm. Cargo.toml restructure (`crates/openpencil-shell-native/Cargo.toml`): - New `[target.'cfg(any(macos, linux, windows, ios, android))']` block for the cross-platform widget stack: `skia-safe = "0.97"` (default-features = false; binary-cache + textlayout) and `jian-skia` (textlayout). Both pull on every desktop trio + mobile pair target. - Existing desktop-only block kept for the GUI host stack: adds `gl` to skia-safe's features (iOS deprecated GL — Metal goes in Step 1f; Android GL/Vulkan via the platform provider not via skia-safe's bundled bindings here), plus glutin / glutin- winit / winit / scopeguard / jian-host-desktop. Cargo deduplicates: skia-safe resolves to one crate-version with feature-union (binary-cache + textlayout from the wider block + gl from the desktop block on desktop-only). src/lib.rs gate lift: - `pub mod backend;` and `pub mod widget_host;` cfg now includes `target_os = "ios"` and `target_os = "android"`. `pub use` re-exports follow. - `canvas_view_stub` stays desktop-only (uses glow GL-isolation probe with no mobile equivalent). - Comment block at the cfg site cites the user directive + Step 1f handoff (real EaglProvider / AndroidEglProvider impls + Metal / Vulkan / event integration). Boundary script revision (`tools/check-jian-boundaries.sh`): - Invariant 2 was: mobile targets must NOT pull jian-host-desktop OR jian-skia. Per the user directive, jian-skia is now ALLOWED on mobile (the widget render stack uses it). jian-host-desktop remains forbidden — it carries winit / glutin / desktop GLContextProvider impls that have no mobile equivalent. - Header comment block + active grep narrowed accordingly. The Step 1f path through EaglProvider / AndroidEglProvider is the spec-blessed mobile host plugin point (no IPC / CLI needed). Verification: - `cargo check -p openpencil-shell-native --target aarch64-apple-ios` — green (skia-bindings + jian-skia + shell-native all compile) - `cargo check -p openpencil-shell-native --target aarch64-linux-android` — green (same) - `cargo check -p openpencil-shell-native` — green (no desktop regression) - `cargo check -p openpencil-shell-core --target wasm32-unknown-unknown` — green (shell-core stays wasm32-clean) - `cargo test -p openpencil-shell-core --test widgets_static` — 21/21 passing (widget logic untouched) - `bash tools/check-wasm-bundle.sh` — PASS: - 0 env.* imports - 622 156 bytes gzip = 59% of 1 MiB ceiling (no web regression) - `bash tools/check-widget-boundary.sh` — PASS - `bash tools/check-jian-boundaries.sh` — 4/4 invariants pass (Invariant 2 revised to allow jian-skia on mobile) What's still mobile-pending (Step 1f scope): - `EaglProvider` (iOS) — Metal-backed `GlContextProvider` impl (skia-safe `metal` feature when iOS host actually runs) - `AndroidEglProvider` (Android) — GL/Vulkan-backed impl - Mobile host runners (UIKit AppDelegate / Activity wrappers) - Mobile event translation (jian-host-ios / jian-host-android — siblings of jian-host-desktop) - `inspector_window` example is desktop-only by design (winit + SharedSkiaContext::new_desktop); mobile shells will land their own UIKit / Activity runners that consume the SAME `WidgetHostNative::paint(&mut frame, width)` surface The widget glue itself (NativeFrameBackend + WidgetHostNative) is platform-agnostic in shape — no winit / glutin / EGL types leak in. Step 1f mobile work plugs in providers, not widgets.
2026-05-10 02:16:36 +00:00
| grep -oE '\bjian-host-desktop\b' \
feat(shell-native): Step 1a Task 4 — basic_window demo + acceptance + Phase C Gate Phase C Task 4 closes Step 1a (G1 shared Skia context) on v0.8.0: - crates/openpencil-shell-native/examples/basic_window.rs: winit + SharedSkiaContext::new_desktop + NativeBackend (Jian DrawOp) + JianPointerMapper integration. Paints chrome rect + "Hello 你好" + box outline; close → idempotent teardown. Demonstrates Phase B Task 3 winit → Jian PointerTranslator → JianPointerMapper → ShellEvent pipeline end-to-end. - crates/openpencil-shell-native/notes/step-1a-{macos,linux,windows}-manual-smoke.md: manual GPU smoke runbooks for spec §1.2 acceptance #1 (macOS PASS recorded; Linux/Windows pending real-hardware run, deferred per CONCERN-R5-1 + WINDOWS_GPU_DEFERRED_NO_RUNNER). - tools/check-jian-boundaries.sh: spec §11 + §12.3 invariants. Verifies that openpencil-app has no direct jian-* dep, mobile (aarch64-linux-android, aarch64-apple-ios) and wasm32 closures exclude jian-host-desktop / jian-skia, and openpencil-shell-web declares no jian-host-desktop dep at the manifest level. - .github/workflows/rust-check.yml: wires bash tools/check-jian-boundaries.sh on Linux runner with mobile + wasm32 targets installed. - README.md: roadmap entry for the Step 1a milestone. Verified locally on macOS aarch64: - cargo fmt --all -- --check - cargo clippy --workspace --all-targets -- -D warnings - cargo build --examples --workspace - cargo test --workspace (38 PASS, 0 FAIL, 0 IGNORED) - cargo check -p openpencil-shell-native --target {aarch64-linux-android, aarch64-apple-ios} - bash tools/check-jian-boundaries.sh (4 invariants PASS) - spec §11 invariants 1–4 grep checks PASS Spec v19.3 FROZEN (openpencil-docs 651090d); Plan v7 FROZEN. vendor/jian pinned at c4a794dc.
2026-05-05 14:39:00 +00:00
| sort -u || true)"
if [ -n "$forbidden_mobile" ]; then
echo "INVARIANT 2 FAILED ($target): forbidden Jian crates in closure:" >&2
echo "$forbidden_mobile" >&2
exit 1
fi
done
# ── Invariant 3: wasm32 has no jian-host-desktop / jian-skia. ──────────
# `jian-core` IS allowed (P0.5 wasm32-clean).
tree_wasm="$(cargo tree -p openpencil-shell-web \
--target wasm32-unknown-unknown \
--prefix none \
--edges normal,build 2>/dev/null || true)"
forbidden_wasm="$(echo "$tree_wasm" \
| grep -oE '\bjian-(host-desktop|skia)\b' \
| sort -u || true)"
if [ -n "$forbidden_wasm" ]; then
echo "INVARIANT 3 FAILED: wasm32 openpencil-shell-web pulls forbidden Jian crates:" >&2
echo "$forbidden_wasm" >&2
exit 1
fi
# ── Invariant 4: openpencil-shell-web has no jian-host-desktop dep. ───
# Distinct from invariant 3 (which checks the resolved closure on the
# wasm32 target): this checks the manifest itself across all targets.
# `cargo tree --all-targets` would include dev-deps; we explicitly
# filter `--edges normal,build` for the manifest-level invariant.
shell_web_deps="$(cargo tree -p openpencil-shell-web \
--prefix none \
--edges normal,build 2>/dev/null \
| grep -E '\bjian-host-desktop\b' || true)"
if [ -n "$shell_web_deps" ]; then
echo "INVARIANT 4 FAILED: openpencil-shell-web depends on jian-host-desktop:" >&2
echo "$shell_web_deps" >&2
exit 1
fi
echo "check-jian-boundaries.sh: all 4 Jian boundary invariants pass."