Commit graph

21 commits

Author SHA1 Message Date
Kayshen-X 5f68ab3c1a refactor: extract op-editor-ui crate
Relocate the widget facade (widgets/, including the Widget trait,
render primitives, the editor-UI compositions, the CanvasViewport
center canvas, the lucide icon drawer, and editor_state_ext), the
theme tokens, the layout-resolved render scene (layout_scene +
layout_scene_hit), and the design-variable aggregation (scene_vars)
out of openpencil-shell-core into a dedicated op-editor-ui crate.

The canvas widgets (canvas_viewport*) stay inside op-editor-ui rather
than splitting into a separate op-canvas crate: they depend on the
widgets/ siblings editor_state_ext + icons and on the Widget trait, so
a clean mechanical split is not possible — per the task's explicit
allowance not to force a fragile split.

op-editor-ui's lib.rs mirrors the old shell-core crate-root re-exports
(render_backend facade types + jian gesture types + the i18n alias) so
every intra-module `crate::Color` / `crate::theme` / `crate::widgets`
path resolves unchanged — a pure relocation with no path rewrites
inside the moved modules. openpencil-shell-core becomes a thin
re-export shim (`pub use op_editor_ui::{widgets, theme, ...}`) so the
hosts keep resolving `openpencil_shell_core::*` until the Task 7.3
host rename dissolves the crate. The widgets_static integration test
moves to op-editor-ui/tests with its imports rewritten. The widget
boundary script's reverse-check path is updated to the new crate.
No behaviour change; all tests move with their code.
2026-05-16 23:33:47 +08:00
Kayshen-X 0ab38e1ca0 fix(i18n): point convert-locales.py output at the op-i18n crate 2026-05-16 14:25:58 +08:00
Kayshen-X e3a163dc2d fix(ci): scrub remaining refs to the deleted stub crates
Codex review of Task 1.2 found four dangling references to the deleted
stub crates (pen-types/core/engine/codegen/figma + openpencil-app):

- package.json: drop the five -p <crate> args from cargo:wasm-check
- rust-release.yml: restore the build job, re-pointed at the real
  openpencil-desktop crate so release-draft has artifacts to publish
- check-jian-boundaries.sh: 4 -> 3 invariants in the success message
- README.md: remove the deleted crate rows from the crate-list table
2026-05-16 12:56:39 +08:00
Kayshen-X 0a90f13aae chore: delete dead pen-* stub crates + openpencil-app 2026-05-16 12:45:07 +08:00
Kayshen-X 2cda18318d feat(shell): selection handles + drag-create + per-node flags + LayerPanel polish
Re-apply 4 reset commits (1854dfa6 → b94274c6) bundled with session
follow-ons. Native + web hosts share the new behavior end-to-end.

Selection + canvas interaction:
- bounded Frame drag now translates descendants too
- 8 selection handles with hover-cursor feedback
- thinner selection outline + smaller AA handles
- handle-drag resize for rect/ellipse/polygon/line/frame/text
- drag-to-create shapes / frames / text from the active tool
- per-NodeKind hit-test (oval / triangle / line slack / point-in-poly)
- rotation pivot is kind-aware (handles negative-size Lines)

Per-node flags (TS parity):
- Node.hidden / locked / collapsed / fill_type (moved off Document.ui)
- mutators gated by is_editable / is_subtree_editable so locked /
  hidden subtrees can't be translated, resized, rotated, recolored,
  or deleted as collateral

Multi-select + marquee + clipboard + keyboard shortcuts:
- selected_set + anchor; shift+click toggles set membership
- marquee rect-select with screen-px threshold + ADD-only shift
- copy / cut / paste / duplicate / nudge / reorder / select-all
- escape one-layer-per-press priority cascade (property-focus →
  locale picker → shape picker → fill-type picker → chat → selection)
- Cmd-letter chord guards (!shift) so Cmd-Shift-letter doesn't fall
  through to text input; !modifier guards on named keys

LayerPanel polish:
- hover-reveal eye/lock affordances (TS parity)
- Eye → EyeOff icon when hidden; Lock → LockOpen when unlocked
- locked Lock renders in warm orange
- chevron expand/collapse for container rows; collapsed subtree
  hides from tree (paint/hit-test unaffected)
- `+` add-page button wired end-to-end (mints fresh id past
  max_node_id + 1, names "Page N", overflow-safe)
- smaller, refined trailing icons (12 px @ 1.2 stroke)
- 18 px chevron-to-kind-icon gap

RenderBackend trait grew fill_oval / stroke_oval / fill_polygon /
stroke_polygon / rotate so both native and web backends can paint
the new node shapes.

Refactor:
- split native widget_host.rs (1799 lines) into spine + 7 sibling
  submodules under widget_host/ to stay under the 800-line ceiling
- split web widget_host.rs into spine + paint + keyboard siblings
- amend tools/check-widget-boundary.sh + spec § 1.4 to allow
  widget_host/* sibling files; tighten `// glue:` marker rule to
  the immediately-preceding line (rustfmt-stable)

Stop-hook iterations addressed:
- allocator overflow guards (checked_add) on duplicate / paste /
  add_page paths
- subtree-size precheck before any id mint in deep_clone
- hidden subtree skipped in paint AND selection overlay
- nested protected delete leak closed via is_subtree_editable
- per-FocusKind hex/numeric input gating; sticky `#` prefix on hex
- ScaleFactorChanged refreshes viewport from window.inner_size()

122 shell-core tests pass; cargo fmt --all --check clean;
cargo check --workspace clean; widget boundary check clean.
2026-05-11 21:30:06 +08:00
Kayshen-X ba6f28f673 chore: gitignore tools/__pycache__/ + remove the .pyc that snuck in 2026-05-10 19:04:55 +08:00
Kayshen-X 2a3a6e96d6 fix(shell-core): convert-locales handles multi-line + double-quoted values
Earlier convert-locales.py was line-based + single-quote-only, missing
~16 keys per locale where:
  - the value spans onto the next line ('long.key.name':\n    'value')
  - the value uses double quotes for English contractions ('topbar.dontSave': "Don't Save")

Switch to a regex.finditer over the whole file with multi-line +
double-quote alternation. All 15 locales now report 706 keys each,
matching the TS source (apps/web/src/i18n/locales/*.ts).

Stop-hook: 'locale import is incomplete'.
2026-05-10 19:04:38 +08:00
Kayshen-X 544a307fd9 feat(shell-core): import all 15 TS locale tables verbatim
Replaces the hand-rolled 25-key i18n.rs with 15 generated locale
modules (~700 keys each) mirrored from
apps/web/src/i18n/locales/*.ts via tools/convert-locales.py.

Locale enum expanded to match the TS dropdown:
EnUs / ZhCn / ZhTw / Ja / Ko / Fr / Es / De / Pt / Ru / Hi / Tr /
Th / Vi / Id (15 total). Each carries its native-script
display_name() (English / 简体中文 / 繁體中文 / 日本語 / 한국어 /
Français / Español / Deutsch / Português / Русский / हिन्दी /
Türkçe / ไทย / Tiếng Việt / Bahasa Indonesia).

Globe icon click cycles all 15 via Locale::next() (round-robin
through Locale::ALL).

Chrome key references updated to TS dot.case naming so the same
key resolves on both sides:
- topbar.untitled → common.untitled
- layer_panel.pages → pages.title
- layer_panel.layers → layers.title
- chat.new_chat → ai.newChat
- chat.start_with_ai → ai.tryExample
- chat.input_placeholder → ai.designWithAgent

Generator script lives at tools/convert-locales.py (re-run when
TS strings update). Each locale .rs file is ≤ 710 lines (under
the 800-line ceiling). Cross-locale fallback: missing keys try
EN before falling through to the key itself.

68 lib tests pass (+1 i18n fallback test).
2026-05-10 18:57:20 +08:00
Kayshen-X 97ef49d76f 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 10:16:36 +08:00
Kayshen-X 8ad5fa81b0 build(step-1b): tools/check-widget-boundary.sh — Phase B4 spec §1.4 guard
Enforces the Step 1b §1.4 widget boundary invariant: widget logic
(Widget impls + layout/paint/access_node methods) lives in
crates/openpencil-shell-core/src/widgets/; shell-web's only
widget-touching file is `widget_host.rs` and even there the only
widget-method signature allowed is the `// glue:` marked paint
dispatcher.

Forward checks (no widget logic in shell-web/src/):
- F1: `impl(<...>)?[[:space:]]+(ns::)*Widget[[:space:]]+for[[:space:]]`
  anywhere under shell-web/src/. Allows generic params + arbitrary
  namespace depth so `impl<T> shell_core::widgets::Widget for X` is
  caught. No `// glue:` exemption — Widget impls have no place in
  shell-web period.
- F2: `fn[[:space:]]+(layout|access_node)\(` anywhere under
  shell-web/src/. No exemption.
- F3: `fn[[:space:]]+paint\(` under shell-web/src/, EXCEPT lines in
  widget_host.rs that ALSO carry `// glue:`. Tight exemption — the
  marker only blesses one specific signature, not arbitrary tagged
  lines.
- F4: any line under shell-web/src/ mentioning both
  `openpencil_shell_core` AND `widgets`, except widget_host.rs.
  Catches direct + grouped `use` forms (e.g. `use
  openpencil_shell_core::{widgets::TreeWidget};`) plus path
  expressions. Multi-line braced `use` is out of scope (single-line
  policy in this crate).

Reverse check (shell-core/src/widgets/ has all four impls):
- R1: For each of {tree, prop_row, dropdown, text_input}, the file
  must exist AND, after stripping `//` line comments, must contain
  a live `impl Widget for X`. Block comments out of scope (line
  comments only in this directory).

CI integration:
- New "Verify Step 1b widget boundary (spec §1.4)" step in
  .github/workflows/rust-check.yml right after the existing
  "Verify Jian boundary invariants" step, gated to Linux runner
  (matches the jian-boundaries pattern).
- Added `tools/check-jian-boundaries.sh` and
  `tools/check-widget-boundary.sh` to the rust-check.yml push +
  pull_request path filters so PRs editing only the checker still
  trigger CI.

7-test regression matrix (positive + 6 negative cases):
- positive (real codebase) → PASS
- generic `impl<T> Widget for X` injected → FAIL F1
- direct `use openpencil_shell_core::widgets` outside host → FAIL F4
- `// glue:` tag on `impl Widget for X` line → FAIL F1 (exemption
  doesn't save it; only `fn paint` lines are exempted)
- shell-core file replaced with `// stub` → FAIL R1
- grouped `use openpencil_shell_core::{widgets::TreeWidget};` → FAIL F4
- grouped `use openpencil_shell_core::{widgets};` → FAIL F4
- shell-core file body replaced with `// impl Widget for X { ... }` → FAIL R1

Codex iterate review: 5 rounds → GO. Round 1 BLOCK (greedy
WidgetHost match), R2 BLOCK + 3 CONCERN (calls/imports unchecked,
generic impls, broad exemption, filename-only count), R3 BLOCK +
CONCERN (grouped imports, commented-out impls), R4 2 NITs
(documentation parity), R5 GO clean.
2026-05-09 21:48:00 +08:00
Kayshen-X d20e1acf0f build(step-1b): tools/check-wasm-bundle.sh — local A3 bundle gate
Implements the Phase A A4 NIT-1 fix from codex Phase A gate review
(verdict GO, 2 NITs flagged): the local equivalent of the Step 1b §6
+ §7.1 CI gate, runnable by developers before submitting Phase A-E
PRs. The full CI workflow remains DEFERRED in
`.github/workflows/rust-release.yml` until brew emscripten install +
EMSDK + .wasm.a → .a symlink + wasm-bindgen + wasm-opt are
automated; until then this script is the authoritative gate.

What it enforces (matches spec §6 + §7.1):
  - EMSDK env var present (build-time-only emsdk libcxx headers +
    wasm-aware clang)
  - cargo build → wasm-bindgen → wasm-opt -Oz pipeline succeeds
  - 0 env.* imports in the post-bindgen bundle (any leak = LinkError
    at load time = regression)
  - gzip size ≤ STEP1B_SHELL_WASM_GZIP_LIMIT_BYTES (default 1 MiB)

Verified on the C-hard.2 bundle: 613 006 bytes gzip (58% of 1 MiB
ceiling), 0 env.* imports, exit 0.

Step 1b Phase A — completes A3 (local) + A4 (codex verdict GO).
2026-05-09 21:44:00 +08:00
Kayshen-X 92f7898329 build(step-1b): add tools/fetch-skia-artifact.sh (Variant A release path)
Codex P0 mini-gate Round 3 BLOCK fix: P0.2 plan calls for
tools/fetch-skia-artifact.sh, and Phase A CI step invokes it before
measuring bundle size — but the script did not exist in the OP repo.
Phase A cannot start without it.

Variant A (GitHub release asset) selected per probe notes §3.1
"Artifact Distribution Strategy": <1 MB gzip, free hosting, no extra
git-lfs setup overhead.

Behavior:
  exit 0  artifact present and SHA matches OR no release pinned yet
          (Phase E will publish; dev loop uses skia-bindings from-source)
  exit 1  artifact present but SHA mismatched, or fetch failed after
          a download attempt

Phase E will overwrite RELEASE_TAG / RELEASE_FILE / EXPECTED_SHA256
constants with the published release asset metadata. Until then dev
builds run skia-bindings from source (no fetch needed).

Smoke run: `bash tools/fetch-skia-artifact.sh` →
"no release tag pinned yet — Phase E will publish; dev builds use
skia-bindings from-source path" + exit 0.
2026-05-09 21:04: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 05aa7b7d1b build(step-1b): add tools/fetch-skia-artifact.sh (Variant A release path)
Codex P0 mini-gate Round 3 BLOCK fix: P0.2 plan calls for
tools/fetch-skia-artifact.sh, and Phase A CI step invokes it before
measuring bundle size — but the script did not exist in the OP repo.
Phase A cannot start without it.

Variant A (GitHub release asset) selected per probe notes §3.1
"Artifact Distribution Strategy": <1 MB gzip, free hosting, no extra
git-lfs setup overhead.

Behavior:
  exit 0  artifact present and SHA matches OR no release pinned yet
          (Phase E will publish; dev loop uses skia-bindings from-source)
  exit 1  artifact present but SHA mismatched, or fetch failed after
          a download attempt

Phase E will overwrite RELEASE_TAG / RELEASE_FILE / EXPECTED_SHA256
constants with the published release asset metadata. Until then dev
builds run skia-bindings from source (no fetch needed).

Smoke run: `bash tools/fetch-skia-artifact.sh` →
"no release tag pinned yet — Phase E will publish; dev builds use
skia-bindings from-source path" + exit 0.
2026-05-09 09:29:53 +08:00
Kayshen-X 543f556cdf 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 22:39:00 +08:00
Kayshen-X c55807e432 ci: remove TS/Electron workflows (build-electron / ci / docker / publish-cli)
Rust-ification 阶段,CI 只保留 Rust 相关:
- rust-check.yml: cargo fmt + build + test (with STEP1A_REQUIRE_GPU=1 on Linux) + clippy + cargo-deny
- wasm-bundle-check.yml: wasm32 target check

删除:
- build-electron.yml: Electron desktop build (Rust 化后用 openpencil-shell-native)
- ci.yml: TS type-check + Vitest + web build (Rust 化后已废)
- docker.yml: TS Docker image (Rust 化后重做)
- publish-cli.yml: npm packages (Rust 化后改 cargo publish)
2026-05-05 22:09:00 +08:00
Kayshen-X c9aaa0efc4 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 21:51:00 +08:00
Kayshen-X 22003f9b0c chore(shell-native): add transient P0 probe gate (Step 1a)
Drives the three-OS CI matrix verification of the skia-safe + glutin +
glow + winit dep stack per Step 1a spec §7.

- examples/p0_probe.rs: stencil_visibility + readback chain runner (must
  own a real OS main thread because winit on macOS rejects
  EventLoop::new() from cargo test worker threads).
- tests/p0_probe.rs: subprocess-invoke wrapper, gated
  #[ignore = "P0_PROBE_GATE"] so default cargo test stays untouched.
- Cargo.toml: add transient [target.'cfg(not(target_arch = "wasm32"))'.
  dev-dependencies] block (skia-safe 0.97 + glutin 0.32.3 + glutin-winit
  0.5.0 + glow 0.17.0 + raw-window-handle 0.6.2 + scopeguard 1.2.0 +
  winit defaults). Pinned to versions resolved in /tmp/skia-glow-probe.
- .github/workflows/rust-check.yml: install Linux GL prereqs (xvfb,
  mesa, libxkbcommon, libwayland) and add a P0-probe-gate step running
  cargo test --ignored on each OS (Linux through xvfb-run; Windows
  early-returns per spec §8.2 WINDOWS_GPU_DEFERRED_NO_RUNNER).

All three artefacts are TRANSIENT — reverted in a follow-up cleanup
commit after CI is green and the loader-compat notes commit lands.
Task 1 owns the permanent integration.
2026-05-05 12:23:49 +08:00
Kayshen-X e4dd3d7b37 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 12:23:43 +08:00
Kayshen-X d5011547f1 ci: remove TS/Electron workflows (build-electron / ci / docker / publish-cli)
Rust-ification 阶段,CI 只保留 Rust 相关:
- rust-check.yml: cargo fmt + build + test (with STEP1A_REQUIRE_GPU=1 on Linux) + clippy + cargo-deny
- wasm-bundle-check.yml: wasm32 target check

删除:
- build-electron.yml: Electron desktop build (Rust 化后用 openpencil-shell-native)
- ci.yml: TS type-check + Vitest + web build (Rust 化后已废)
- docker.yml: TS Docker image (Rust 化后重做)
- publish-cli.yml: npm packages (Rust 化后改 cargo publish)
2026-05-05 12:23:33 +08:00
Kayshen-X e66e8aefcc 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 12:23:27 +08:00