Commit graph

8 commits

Author SHA1 Message Date
Kayshen-X 6f4ab59313 fix: clear clippy -D warnings across the workspace
The op-* crate reorg never ran `cargo clippy -- -D warnings`, so the CI
lint gate failed. Fix every violation surgically: real fixes for
mechanical lints (needless_range_loop, derivable_impls, ptr_arg,
needless_borrow, doc_lazy_continuation, unused_imports, manual_find,
collapsible_match, never_loop, dead_code, complex types via type
aliases) and scoped `#[allow]` for intrusive ones (too_many_arguments
on paint helpers, result_large_err where ToolOutcome / PenNode payloads
are deliberately the Err type).
2026-05-17 01:26:29 +08:00
Kayshen-X c5d408d6be style(shell): cargo fmt --all (rustfmt-clean)
Stop-hook fix: codex flagged Rust files as not rustfmt-clean.
Run cargo fmt --all across openpencil-shell-{core,native,web}
+ wasm-libc-shim. 67 lib tests still pass, native + web cargo
check clean.
2026-05-10 18:47:33 +08:00
Kayshen-X 01c5baf8f5 refactor(wasm-libc-shim): split imp.rs to honor 800-line ceiling
Codex Step 3 stop-hook flagged: `wasm-libc-shim` violates the
repo file-size gate. After the Step 3 additions
(`bd6f4836` + `12b9af92`) `imp.rs` grew to 845 lines, over
the 800-line ceiling documented in `openpencil/CLAUDE.md`
("Single files must not exceed 800 lines").

# Layout

```
crates/wasm-libc-shim/src/imp/
  mod.rs   (521 lines) — original C-hard.2 surface:
                          allocator + libm + libc string +
                          libc misc + C++ ABI + operator new/
                          delete + threads + libcxx panic stubs
  step3.rs (349 lines) — Skia font-path additions: extra
                          string ops (strncmp/strncpy/strstr/
                          strrchr/strcat/strtol/tolower/qsort),
                          file I/O sentinel returns, mmap,
                          setjmp/longjmp, _ZnwmRKSt9nothrow_t
```

Both files emit `#[no_mangle]` symbols at the same flat global
namespace — wasm-ld resolves them identically regardless of
the parent Rust module. Module structure is purely a source-
organisation concern, no ABI implications.

# Plumbing

- `git mv imp.rs imp/mod.rs` to preserve history.
- `imp/mod.rs` declares `mod step3;` at the top + makes
  `is_space` `pub(super)` so `step3::strtol` can reuse it
  without re-implementing the whitespace check.
- `c_long` import dropped from `mod.rs` (only `step3.rs` uses
  it now).

# Verification

- `cargo build -p openpencil-shell-web --target
  wasm32-unknown-unknown --features skia --release` — green
- `wasm-bindgen --target web` — produces ../pkg/*
- `bash tools/check-wasm-bundle.sh` — PASS, 0 env.*, 906 988
  bytes gzip = 86% of 1 MiB ceiling (no growth — the split is
  source-only)
- File sizes:
  - imp/mod.rs: 521 lines (under 800 ✓)
  - imp/step3.rs: 349 lines (under 800 ✓)
2026-05-10 13:02:47 +08:00
Kayshen-X 7b29943946 fix(wasm-libc-shim): Step 3 R1 — c_long for strtol/ftell/fseek
Codex Step 3 R1 CONCERN: wasm32-unknown-unknown sizes `long` as
32-bit, but the new libc shim returned `i64` from `strtol` /
`ftell` and accepted `i64 offset` in `fseek`. The wasm-ld
linker resolved the mismatch by inserting `signature_mismatch:
strtol` / `signature_mismatch:ftell` / `signature_mismatch:
fseek` trap stubs into the bundle — calling any of them at
runtime would have crashed even though the shim crate
"compiled".

Fix: use `core::ffi::c_long` (= i32 on wasm32, i64 on
desktop) for return + offset types. Verified with
`wasm-objdump -x | grep signature_mismatch:` — no entries
remain for strtol / ftell / fseek / setjmp / longjmp / fopen /
fread / fclose / fprintf. (The remaining `signature_mismatch:
_ZNSt3__2…` entries are our libcxx_stub! macros, which return
`!` and are correct to trap-on-call.)

Two NIT fixes folded in:

- `strtol` now accepts an explicit `0x` prefix when the
  caller passes `base=16` (codex Step 3 R1 NIT-2 — strtoull
  already had the same handling; mirrored for parity).
- `qsort` no longer silently no-ops on element size > 256;
  panics loudly so a real call site gets a usable diagnostic
  (codex Step 3 R1 NIT-3). Tiny font-feature / glyph-run
  arrays stay under the threshold.

WebBackend typeface caching tightened (codex Step 3 R1 NIT-1):
- New sticky `typeface_tried: bool` flag flips on first
  attempt regardless of outcome. Subsequent draw_text calls
  skip the FontMgr / from_data round-trip if `typeface` is
  still None — a one-time failure no longer re-parses the
  TTF on every frame.

Verification:
- `cargo build -p openpencil-shell-web --target
  wasm32-unknown-unknown --features skia --release` — green
- `wasm-bindgen --target web` — produces ../pkg/*
- `wasm-objdump -x | grep "signature_mismatch:" | grep -E
  "strtol|ftell|fseek|..."` — empty (all shim signatures
  resolve cleanly)
- `bash tools/check-wasm-bundle.sh` — PASS:
  - 0 env.* imports
  - 906 964 bytes gzip = 86% of 1 MiB ceiling (unchanged)
2026-05-10 12:51:17 +08:00
Kayshen-X fe5249567f fix(shell-web): Step 3 stop-hook — actually render canvas text
Codex stop-hook flagged: "web Step 3 cannot render the claimed
canvas text". Root cause: WebBackend::draw_text was a Phase A
no-op stub. CanvasViewport calls draw_text for "Hello
OpenPencil" / "Click me" / Layer panel labels / etc — none of
those text strings actually rendered in the browser.

# Fix

`crates/openpencil-shell-web/src/backend/mod.rs::WebBackend`:
- Embeds `assets/Roboto-Regular.ttf` (Apache 2.0, 35 KB, copied
  from rust-skia test resources) via `include_bytes!`. The
  C-hard wasm32-unknown-unknown skia build uses
  `skia_enable_fontmgr_custom_empty=yes` (see
  `vendor/skia-safe-op/skia-bindings/build_support/platform/
  wasm_unknown.rs`), so there are no system fonts and we have
  to bake the bytes in.
- New `typeface: Option<Typeface>` field, lazy-init on first
  draw_text via `FontMgr::custom_empty().and_then(|m|
  m.new_from_data(ROBOTO_TTF, None))`. Build failure → None
  silently no-ops subsequent draws (no panic — text just
  doesn't render).
- `draw_text` now iterates `layout.runs()` and calls
  `Canvas::draw_str` per run with a `Font::new(typeface,
  font_size)` + `Paint` from the run color.

`crates/openpencil-shell-web/Cargo.toml`:
- Drops `textlayout` skia-safe feature. We use raw `draw_str`
  not paragraph builder; textlayout pulled ICU + Harfbuzz +
  ~400 KB gzip + a swarm of font-lookup imports we don't
  exercise.

# 24 new env.* imports — wasm-libc-shim expansion

Even without textlayout, Skia's font path imports 24 libc
symbols our prior C-hard.2 shim didn't cover. All resolved:

`crates/wasm-libc-shim/src/imp.rs` — Rust extern "C" shims:
- string ops (real impls): strncmp, strncpy, strstr, strrchr,
  strcat, strtol, tolower, qsort (insertion sort, fits Skia's
  small-array call sites; debug_assert on element size > 256)
- file I/O (sentinel error returns, no filesystem on wasm):
  fopen → null, fread → 0, fclose → 0, fputc → c, fileno → -1,
  fstat → -1, pread → -1, ftell → -1, fseek → -1
- env: getenv → null
- mmap: returns MAP_FAILED ((void*)-1); munmap → -1
- setjmp/longjmp: setjmp returns 0 (treat as initial call);
  longjmp panics — happy text path through in-memory TTF parse
  should never trigger it
- C++ nothrow new: `_ZnwmRKSt9nothrow_t` forwards to malloc,
  returns nullptr on OOM (the nothrow contract)

`crates/wasm-libc-shim/src/stdio_stub.c`:
- fprintf C-side variadic stub (Skia diagnostic path) that
  routes into the same panic helper as snprintf / vsnprintf /
  vfprintf — same fail-fast policy.

# Verification

- `cargo build -p openpencil-shell-web --target
  wasm32-unknown-unknown --features skia --release` — green
- `wasm-bindgen --target web` produces ../pkg/openpencil_shell
  _web.{js,_bg.wasm}
- `bash tools/check-wasm-bundle.sh` — PASS:
  - 0 env.* imports preserved (24 new ones absorbed by shim)
  - 906 935 bytes gzip = 86% of 1 MiB ceiling (+286 KB vs
    pre-text — Skia font/freetype subsystem is substantial.
    Headroom: 14% of ceiling)
- `cargo test -p openpencil-shell-core --lib` — 39 tests
- `cargo build -p openpencil-shell-native --example
  inspector_window` — green (desktop demo unchanged — uses
  jian-skia textlayout via NativeBackend, not the web font
  path)
- `cargo check -p openpencil-shell-native --target
  aarch64-apple-ios` — green
- `cargo check -p openpencil-shell-native --target
  aarch64-linux-android` — green

# Re-run the demo

```
EMSDK="$HOME/.emsdk" cargo build -p openpencil-shell-web \
    --target wasm32-unknown-unknown --features skia --release
wasm-bindgen --target web --out-dir crates/openpencil-shell-web/pkg \
    target/wasm32-unknown-unknown/release/openpencil_shell_web.wasm
cd crates/openpencil-shell-web/smoke
python3 -m http.server 8000
# Browser: http://localhost:8000/step-1b.html
```

Now the canvas viewport renders "Hello OpenPencil" + "Click me"
text in addition to the rect/stroke geometry.
2026-05-10 12:38:33 +08:00
Kayshen-X 8ac685ff84 feat(shell): add wasm-libc-shim crate for wasm32-unknown-unknown skia bundle
Provides the libc / libcxx / libm symbols that the wasm32-unknown-
unknown skia static archive (built via vendor/skia-safe-op) imports
at link time but wasm-bindgen does not synthesize. With this crate
linked in, the post-bindgen bundle has 0 env.* imports and is
runtime-loadable as a vanilla browser ES module.

Categories implemented (~83 symbols, dedup against the actual
import list):
  - allocator: malloc / free / calloc / realloc / malloc_usable_size
    via dlmalloc-rs + a 16-byte size header per allocation so the
    GlobalAlloc::dealloc layout contract is preserved on free and
    realloc copies min(old_size, new_size) on grow
  - libm: asinh / acosh / atanh / nextafterf / remainder via libm
  - libc string: memchr / wmemchr / strcmp / strcpy / strtoull
    hand-rolled byte-wise
  - libc stdio: snprintf / vsnprintf / vfprintf as C-side variadic
    stubs (stdio_stub.c, compiled by cc) that route into a Rust
    extern wasm_libc_shim_stdio_panic before returning, so any
    actual invocation surfaces a named panic via console_error_
    panic_hook instead of a silent empty success
  - libc misc: abort (panics with diagnostic) + __errno_location
    (single-mut-static — single-threaded wasm only)
  - C++ ABI: __cxa_atexit (no-op), __cxa_guard_acquire / release,
    __cxa_pure_virtual (panics)
  - operator new / delete: _Znwm / _Znam / _ZdlPv* / _ZdaPv*
    forwarding to malloc / free; _Znwm(0) routes through
    malloc(1) per C++ standard (operator new must return a
    non-null pointer)
  - threads: sem_init / sem_destroy / sem_post / sem_wait no-op
  - libcxx string / locale / iostream / shared_weak_count /
    to_string: ~23 panic stubs via the libcxx_stub! macro that
    panic with the symbol name; these are linker-pulled by
    templated code that the skia raster + custom_empty fontmgr
    pipeline does not exercise at runtime, so a panic = regression
    signal

Build-time gating:
  - active only on wasm32-unknown-unknown via cfg(all(target_arch
    = "wasm32", target_os = "unknown")); native builds link an
    empty crate so the symbols do not collide with the host libc
  - compile_error! on target_feature = "atomics" because the
    static-mut errno + non-atomic __cxa guard impls would race
    under wasm threads — the path forward is real TLS errno +
    atomic guard variables in a follow-up sub-phase

Step 1b §3.2 P0.5B Run path, sub-phase C-hard.2.
2026-05-09 21:07:00 +08:00
Fini af9292d8f5 fix(ai): classify Type 0 components as non-mobile to skip phone chrome
Why: "Design a profile card" through MiniMax-M2.7 produced a 375×803 mobile
screen with auto-injected status bar, because the planner skill listed
"profiles" as a Type 2 single-task screen and the orchestrator's
isMobileScreen heuristic ran on width≤480 alone.

What: design-type.md + decomposition.md add Type 0 (single component:
card / badge / chip / modal) with width=400 height=0 1 subtask no chrome.
isMobileFullScreen helper extracted to orchestrator-plan-classify.ts and
required by both orchestrator.ts and orchestrator-sub-agent.ts so the
two paths can't drift on what "mobile" means (Codex review caught this
when only orchestrator.ts had the new check).

Verified with same MiniMax + same prompt: 400×320 component, 8 nodes,
firstChildRole=card, no status-bar.
2026-05-09 21:00:00 +08:00
Kayshen-X 40b03c9075 feat(shell): add wasm-libc-shim crate for wasm32-unknown-unknown skia bundle
Provides the libc / libcxx / libm symbols that the wasm32-unknown-
unknown skia static archive (built via vendor/skia-safe-op) imports
at link time but wasm-bindgen does not synthesize. With this crate
linked in, the post-bindgen bundle has 0 env.* imports and is
runtime-loadable as a vanilla browser ES module.

Categories implemented (~83 symbols, dedup against the actual
import list):
  - allocator: malloc / free / calloc / realloc / malloc_usable_size
    via dlmalloc-rs + a 16-byte size header per allocation so the
    GlobalAlloc::dealloc layout contract is preserved on free and
    realloc copies min(old_size, new_size) on grow
  - libm: asinh / acosh / atanh / nextafterf / remainder via libm
  - libc string: memchr / wmemchr / strcmp / strcpy / strtoull
    hand-rolled byte-wise
  - libc stdio: snprintf / vsnprintf / vfprintf as C-side variadic
    stubs (stdio_stub.c, compiled by cc) that route into a Rust
    extern wasm_libc_shim_stdio_panic before returning, so any
    actual invocation surfaces a named panic via console_error_
    panic_hook instead of a silent empty success
  - libc misc: abort (panics with diagnostic) + __errno_location
    (single-mut-static — single-threaded wasm only)
  - C++ ABI: __cxa_atexit (no-op), __cxa_guard_acquire / release,
    __cxa_pure_virtual (panics)
  - operator new / delete: _Znwm / _Znam / _ZdlPv* / _ZdaPv*
    forwarding to malloc / free; _Znwm(0) routes through
    malloc(1) per C++ standard (operator new must return a
    non-null pointer)
  - threads: sem_init / sem_destroy / sem_post / sem_wait no-op
  - libcxx string / locale / iostream / shared_weak_count /
    to_string: ~23 panic stubs via the libcxx_stub! macro that
    panic with the symbol name; these are linker-pulled by
    templated code that the skia raster + custom_empty fontmgr
    pipeline does not exercise at runtime, so a panic = regression
    signal

Build-time gating:
  - active only on wasm32-unknown-unknown via cfg(all(target_arch
    = "wasm32", target_os = "unknown")); native builds link an
    empty crate so the symbols do not collide with the host libc
  - compile_error! on target_feature = "atomics" because the
    static-mut errno + non-atomic __cxa guard impls would race
    under wasm threads — the path forward is real TLS errno +
    atomic guard variables in a follow-up sub-phase

Step 1b §3.2 P0.5B Run path, sub-phase C-hard.2.
2026-05-09 20:59:21 +08:00