From b76058d54995e51bf155b56157c5d6822cfabdcd Mon Sep 17 00:00:00 2001 From: Sean Dolbec Date: Mon, 26 Oct 2026 11:45:03 -0400 Subject: [PATCH 01/80] fix(msdk): preserve literal names in generated starters --- mseries/OS9-HANDOFF.md | 21 +++++++++++++++++++++ mseries/ROADMAP.md | 21 +++++++++++++++++++++ rust/msdk/src/templates.rs | 23 ++++++++++++++++++++++- rust/msdk/tests/templates.rs | 22 ++++++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index ed01210e4..c67ef510d 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,3 +1,24 @@ +## SDK starter literal-name repair — 2026-10-26 + +`msdk new` now JSON-escapes supplied identity/name strings and substitutes original +placeholders once. Quotes, backslashes, Unicode, newlines and names resembling +placeholder text no longer corrupt or recursively rename generated descriptions. +Five template tests pass; actual CLI settings generation in JSON and `.msdk` +formats passes `check` and `gate` with a quoted/backslashed Unicode name. + +Verification used an isolated copy of the unchanged msdk crate plus its external +fixture files, with cached Cargo dependencies on macOS. Workspace offline testing +cannot resolve the uncached pinned Smithay Git dependency. The full isolated core +suite has two failures: inspect expects the old T3 Chat default/usage heading, +and intents expects absent tool `claudepantheonrs_send`. These are outside template +rendering and remain open; no green whole-suite or GTK/runtime claim is made. + +Next SDK completion work: reconcile those T3 contract tests; provide a complete +standalone app/build/package path; qualify versioned service errors and optional +capability lifecycle; exercise native Dock conformance (#605), and retain actual +visual/accessibility/installation and controlled model-authoring evidence (#535). +Signal/Messages remain deferred. No desktop installation or panel restart. + ## Unified native T3 panel integration — 2026-10-26 Merged Fleet PR #607 at `04fe68e6`. Owner does not require particular remote diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index 24ab7645d..c3edb49e2 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -1,3 +1,24 @@ +## SDK starter literal-name repair — 2026-10-26 + +`msdk new` now JSON-escapes supplied identity/name strings and substitutes original +placeholders once. Quotes, backslashes, Unicode, newlines and names resembling +placeholder text no longer corrupt or recursively rename generated descriptions. +Five template tests pass; actual CLI settings generation in JSON and `.msdk` +formats passes `check` and `gate` with a quoted/backslashed Unicode name. + +Verification used an isolated copy of the unchanged msdk crate plus its external +fixture files, with cached Cargo dependencies on macOS. Workspace offline testing +cannot resolve the uncached pinned Smithay Git dependency. The full isolated core +suite has two failures: inspect expects the old T3 Chat default/usage heading, +and intents expects absent tool `claudepantheonrs_send`. These are outside template +rendering and remain open; no green whole-suite or GTK/runtime claim is made. + +Next SDK completion work: reconcile those T3 contract tests; provide a complete +standalone app/build/package path; qualify versioned service errors and optional +capability lifecycle; exercise native Dock conformance (#605), and retain actual +visual/accessibility/installation and controlled model-authoring evidence (#535). +Signal/Messages remain deferred. No desktop installation or panel restart. + ## Unified native T3 panel integration — 2026-10-26 Merged Fleet PR #607 at `04fe68e6`. Owner does not require particular remote diff --git a/rust/msdk/src/templates.rs b/rust/msdk/src/templates.rs index 5e8d07650..4f5bf00ac 100644 --- a/rust/msdk/src/templates.rs +++ b/rust/msdk/src/templates.rs @@ -77,7 +77,28 @@ pub fn find(name: &str) -> Option<&'static Template> { impl Template { /// The description with the owner's id and display name in place of the template's. pub fn render(&self, id: &str, display: &str) -> String { - self.ui.replace(self.id, id).replace(self.display, display) + // Escape each replacement as JSON string contents. Substitute the original + // template in one pass so user input resembling another placeholder + // remains literal rather than being expanded a second time. + let escaped = |value: &str| { + let json = serde_json::to_string(value).expect("strings serialize to JSON"); + json[1..json.len() - 1].to_owned() + }; + let (id, display) = (escaped(id), escaped(display)); + let mut result = String::new(); + let mut rest = self.ui; + while let Some((offset, placeholder, replacement)) = + [(rest.find(self.id), self.id, id.as_str()), (rest.find(self.display), self.display, display.as_str())] + .into_iter() + .filter_map(|(offset, key, value)| offset.map(|offset| (offset, key, value))) + .min_by_key(|(offset, _, _)| *offset) + { + result.push_str(&rest[..offset]); + result.push_str(replacement); + rest = &rest[offset + placeholder.len()..]; + } + result.push_str(rest); + result } pub fn readme(&self, display: &str) -> String { diff --git a/rust/msdk/tests/templates.rs b/rust/msdk/tests/templates.rs index 46873f1b1..c068ae901 100644 --- a/rust/msdk/tests/templates.rs +++ b/rust/msdk/tests/templates.rs @@ -35,3 +35,25 @@ fn template_names_are_unique_and_documented() { } assert!(templates::find("settings").is_some() && templates::find("nope").is_none()); } + +#[test] +fn custom_names_are_literal_json_strings_in_every_starter() { + for template in templates::TEMPLATES { + let name = format!("{} \"Notes\" \\ café\nsecond line", template.id); + let rendered = template.render("com.example.Notes", &name); + let doc: serde_json::Value = serde_json::from_str(&rendered).unwrap(); + assert_eq!(doc["app"]["name"], name, "{}", template.name); + assert_eq!(doc["app"]["id"], "com.example.Notes"); + let text = msdk::text::print(&doc).unwrap(); + assert!(!text.is_empty()); + } +} + +#[test] +fn replacement_values_are_not_expanded_as_template_placeholders() { + for template in templates::TEMPLATES { + let doc: serde_json::Value = serde_json::from_str(&template.render(template.display, template.id)).unwrap(); + assert_eq!(doc["app"]["id"], template.display); + assert_eq!(doc["app"]["name"], template.id); + } +} From 1a0d616af0d0e2e2f012c44ce9ad6091ed96dfaf Mon Sep 17 00:00:00 2001 From: Sean Dolbec Date: Mon, 26 Oct 2026 11:56:11 -0400 Subject: [PATCH 02/80] feat(msdk): generate standalone typed GTK applications and qualify the developer path --- .github/workflows/msdk.yml | 79 +++++++++++++ mseries/APP-BUILDING-GUIDE.md | 6 +- mseries/OS9-HANDOFF.md | 17 +++ mseries/ROADMAP.md | 17 +++ mseries/RUST-PANTHEON-SDK.md | 15 ++- mseries/SDK-COMPLETION.md | 42 +++++++ mseries/sdk-tests/prepare.py | 30 +++++ rust/msdk/README.md | 64 ++++++++-- rust/msdk/src/generate.rs | 175 ++++++++++++++++++++++++++++ rust/msdk/src/lib.rs | 1 + rust/msdk/src/main.rs | 53 ++++++++- rust/msdk/templates/project/main.rs | 50 ++++++++ rust/msdk/tests/generate.rs | 98 ++++++++++++++++ rust/msdk/tests/inspect.rs | 8 +- rust/msdk/tests/intents.rs | 6 +- 15 files changed, 636 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/msdk.yml create mode 100644 mseries/SDK-COMPLETION.md create mode 100644 mseries/sdk-tests/prepare.py create mode 100644 rust/msdk/src/generate.rs create mode 100644 rust/msdk/templates/project/main.rs create mode 100644 rust/msdk/tests/generate.rs diff --git a/.github/workflows/msdk.yml b/.github/workflows/msdk.yml new file mode 100644 index 000000000..703504264 --- /dev/null +++ b/.github/workflows/msdk.yml @@ -0,0 +1,79 @@ +name: M-series SDK developer path + +on: + workflow_dispatch: + pull_request: + paths: + - "rust/msdk/**" + - "rust/msdk-gtk/**" + - "rust/claude-pantheon-rs/sdk/**" + - "rust/mseries-greeter/data/greeter.ui.json" + - "mseries/sdk-tests/**" + - ".github/workflows/msdk.yml" + push: + branches: [dev] + paths: + - "rust/msdk/**" + - "rust/msdk-gtk/**" + - "mseries/sdk-tests/**" + - ".github/workflows/msdk.yml" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + qualify: + runs-on: ubuntu-26.04-arm + timeout-minutes: 25 + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/work/sdk-target + steps: + - uses: actions/checkout@v7 + - name: Provision native SDK test environment + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends cargo rustc rustfmt rust-clippy \ + build-essential pkg-config libgtk-4-dev libwayland-dev libwayland-bin \ + xvfb xauth dbus-x11 at-spi2-core + - uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry + work/sdk-target + key: msdk-native-${{ runner.arch }}-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: msdk-native-${{ runner.arch }}- + - name: Prepare isolated SDK workspace + run: python3 mseries/sdk-tests/prepare.py "$RUNNER_TEMP/sdk" + - name: Core tests and strict lint + run: | + cargo fmt --manifest-path "$RUNNER_TEMP/sdk/Cargo.toml" -p msdk --check + cargo test --manifest-path "$RUNNER_TEMP/sdk/Cargo.toml" -p msdk + cargo clippy --manifest-path "$RUNNER_TEMP/sdk/Cargo.toml" -p msdk --all-targets -- -D warnings + - name: Build and test generated native application + run: | + mkdir -p "$RUNNER_TEMP/sdk-evidence" + "$CARGO_TARGET_DIR/debug/msdk" new settings "$RUNNER_TEMP/starter" --name 'SDK Settings' --id io.github.techmore.SdkSettings + "$CARGO_TARGET_DIR/debug/msdk" gen "$RUNNER_TEMP/starter/app.ui.json" "$RUNNER_TEMP/generated" --sdk-root "$RUNNER_TEMP/sdk" + cargo fmt --manifest-path "$RUNNER_TEMP/generated/Cargo.toml" + cargo test --manifest-path "$RUNNER_TEMP/generated/Cargo.toml" --all-targets + cargo clippy --manifest-path "$RUNNER_TEMP/generated/Cargo.toml" --all-targets -- -D warnings + for mode in light dark; do + theme=Adwaita + if [ "$mode" = dark ]; then theme=Adwaita:dark; fi + GTK_THEME="$theme" GSK_RENDERER=cairo xvfb-run -a dbus-run-session -- \ + "$CARGO_TARGET_DIR/debug/msdk-sdksettings" --screenshot "$RUNNER_TEMP/sdk-evidence/settings-$mode.png" + done + cp "$RUNNER_TEMP/sdk/Cargo.lock" "$RUNNER_TEMP/sdk-evidence/sdk-Cargo.lock" + cp "$RUNNER_TEMP/generated/Cargo.lock" "$RUNNER_TEMP/sdk-evidence/generated-Cargo.lock" + git rev-parse HEAD > "$RUNNER_TEMP/sdk-evidence/source.txt" + sha256sum "$CARGO_TARGET_DIR/debug/msdk-sdksettings" > "$RUNNER_TEMP/sdk-evidence/binary.sha256" + - uses: actions/upload-artifact@v7 + if: always() + with: + name: msdk-native-${{ github.run_id }} + path: ${{ runner.temp }}/sdk-evidence/ + if-no-files-found: warn diff --git a/mseries/APP-BUILDING-GUIDE.md b/mseries/APP-BUILDING-GUIDE.md index a8d62558c..1cab8f2cd 100644 --- a/mseries/APP-BUILDING-GUIDE.md +++ b/mseries/APP-BUILDING-GUIDE.md @@ -51,8 +51,10 @@ transport, account mappings and GitHub services remain app-specific adapters. Use `cargo run -p msdk -- templates` from `rust/` to discover actual starters, then the CLI's `describe`, `check` and `gate` commands. Consult -[RUST-PANTHEON-SDK.md](RUST-PANTHEON-SDK.md) for boundaries. Do not present -proposed `msdk gen`, `msdk pack`, `msdk parity` or live introspection commands +[RUST-PANTHEON-SDK.md](RUST-PANTHEON-SDK.md) for boundaries. Use `msdk gen --sdk-root /rust` +when a standalone Cargo executable and typed models are needed. It preserves +rules and the GTK host; custom service backends are app-owned extension points. +Do not present proposed `msdk pack`, `msdk parity` or live introspection commands as available. Keep native chrome, scaling, accessibility, recoverable states, diagnostics and install/remove behavior in the delivered outcome contract. diff --git a/mseries/OS9-HANDOFF.md b/mseries/OS9-HANDOFF.md index c67ef510d..e3b6f45ce 100644 --- a/mseries/OS9-HANDOFF.md +++ b/mseries/OS9-HANDOFF.md @@ -1,3 +1,20 @@ +## SDK standalone app generation and core reconciliation — 2026-10-26 + +Added `msdk gen` for checked descriptions: independent Cargo project, inspectable +Rust records/State/Message/event bridges, embedded document/service manifests, +real GTK executable and initial-state test. No custom backend or Rust logic is +silently invented. All six generated models compile; keyword/collision cases +and >2^53 integers round-trip exactly. Starter escaping remains covered. +Reconciled two stale core tests with the current T3 SDK identity and Usage +start page while preserving permission/page-visibility assertions. + +Core tests and strict clippy pass in the isolated macOS core workspace. New +Linux native generation/build/launch screenshot CI is pending; target desktop, +package/install, lifecycle, model usability and owner acceptance remain open. +`SDK-COMPLETION.md` preserves the full requested completion contract and next +gates; mandatory SDK/authoring and CLI docs describe the new command accurately. +No desktop installation, live account reads, kernel or panel changes. + ## SDK starter literal-name repair — 2026-10-26 `msdk new` now JSON-escapes supplied identity/name strings and substitutes original diff --git a/mseries/ROADMAP.md b/mseries/ROADMAP.md index c3edb49e2..570a11d08 100644 --- a/mseries/ROADMAP.md +++ b/mseries/ROADMAP.md @@ -1,3 +1,20 @@ +## SDK standalone app generation and core reconciliation — 2026-10-26 + +Added `msdk gen` for checked descriptions: independent Cargo project, inspectable +Rust records/State/Message/event bridges, embedded document/service manifests, +real GTK executable and initial-state test. No custom backend or Rust logic is +silently invented. All six generated models compile; keyword/collision cases +and >2^53 integers round-trip exactly. Starter escaping remains covered. +Reconciled two stale core tests with the current T3 SDK identity and Usage +start page while preserving permission/page-visibility assertions. + +Core tests and strict clippy pass in the isolated macOS core workspace. New +Linux native generation/build/launch screenshot CI is pending; target desktop, +package/install, lifecycle, model usability and owner acceptance remain open. +`SDK-COMPLETION.md` preserves the full requested completion contract and next +gates; mandatory SDK/authoring and CLI docs describe the new command accurately. +No desktop installation, live account reads, kernel or panel changes. + ## SDK starter literal-name repair — 2026-10-26 `msdk new` now JSON-escapes supplied identity/name strings and substitutes original diff --git a/mseries/RUST-PANTHEON-SDK.md b/mseries/RUST-PANTHEON-SDK.md index 1bca51592..561c5e6d5 100644 --- a/mseries/RUST-PANTHEON-SDK.md +++ b/mseries/RUST-PANTHEON-SDK.md @@ -30,10 +30,17 @@ SDK parity. Source inventory: [SDK audit](SDK-AUDIT-2026-10-23.md#2026-10-25-t3- | `FixtureServices` | Isolated UI/data scenarios, not proof of live integration. | | Native kit | Separately consumed GTK/Granite, scaling, accessibility, polling/badge helpers; lifecycle must still be qualified by the app. | -`msdk gen`, `msdk pack`, `msdk parity` and a live introspection registry remain -proposals, not commands to give an LLM as existing tools. The implemented -description generator is real; complete standalone app/package generation is -not established. Follow the [T3 priority-slice gates](APP-BUILDING-GUIDE.md#t3-priority-slice-and-sdk-qualification-gates): +`msdk gen --sdk-root /rust` now +creates a standalone Cargo project with typed records, State/Message bridges, +the embedded description and a real GTK Host executable. Neighboring +`*.services.json` manifests are embedded, but application-specific service/feed +implementations still require an app-local adapter. Custom Rust logic is rejected +with a LogicFactory instruction rather than replaced with a no-op. Generation +never installs or executes application services. See the [CLI guide](../rust/msdk/README.md). + +`msdk pack`, `msdk parity` and a live introspection registry remain proposals. +Native build/run, packaging and full SDK readiness are tracked separately in +[SDK-COMPLETION.md](SDK-COMPLETION.md). Follow the [T3 priority-slice gates](APP-BUILDING-GUIDE.md#t3-priority-slice-and-sdk-qualification-gates): token monitoring and RepoBar-derived GitHub overview first (#596), with Shiplog (#595) building on that data. Whole-chat parity is a separate contract. diff --git a/mseries/SDK-COMPLETION.md b/mseries/SDK-COMPLETION.md new file mode 100644 index 000000000..523dd77b6 --- /dev/null +++ b/mseries/SDK-COMPLETION.md @@ -0,0 +1,42 @@ +# SDK completion and qualification ledger + +The requested outcome is a complete, tested and validated Rust Pantheon app SDK. +The product contracts are issue #535, the current APP-BUILDING-GUIDE and +RUST-PANTHEON-SDK, the sdk-bench specs/rubric, and the Dock standard proposed in +PR #605. Existing Signal/Messages deferral remains in force: use synthetic +messaging data for SDK checks; do not resume or replace those clients. + +A green compiler/test run is scoped evidence. SDK completion requires every +applicable row below to have retained source-matched evidence; missing evidence +is an open gate. No completion percentage is assigned. + +| Requirement | Authoritative evidence needed | Current state / next work | +| --- | --- | --- | +| Ownership/runtime/API support | Published graphics, shell, toolkit and app API versions/owners; stable/provisional/unsupported boundaries; compatibility and deprecation policy | Existing documents are partial; reconcile a v1 contract and supported-host matrix | +| Description authoring/discovery | Checker, schema, discovery and rules agree; all six starters pass; valid user strings survive generation | Starter escaping repaired; core suite includes current T3 identity/page assertions | +| Typed Rust and standalone app | `msdk gen` emits compiling records/State/Message plus a real GTK executable; original description/rules retained; custom backends explicit | Implementation under qualification; generated model crate compiles for all six starters and keyword/large-integer cases; native CI pending | +| Clean developer path | Clean checkout to useful app, recoverable service error, typed diagnostic/source context, optional capability, build/package/run guide; coding <=30 min and provisioning/wall time separately measured | Generation removes manual Cargo setup; complete golden-path sample and measured novice/model trial still needed | +| Services and errors | Version mismatch/unavailable/denied/malformed data through actual adapters; bounded async/cancel/retry behavior; privacy-safe structured diagnostics | Audit Host/Services, implement versioned error and cancellation contracts, then exercise adapters | +| Optional lifecycle | Disabled/closed causes no extra reads, stale completions ignored, sources released; exact scope/account remains visible | Existing window watcher and Host ownership require lifecycle qualification | +| Native toolkit/UI | Source-matched light/dark, regular/compact, default/larger text and supported scales; keyboard-only primary journey and live accessible names/roles | New isolated Linux generated-app checks pending; target Pantheon visual/interaction acceptance remains open | +| Shell/status module | Discoverable reusable CodexBar-derived provider/account module; Windows bottom-right and elementary status hosts; all semantics and exact interaction gates from #605 | Native T3 integration is separate from reusable SDK capability; no parity claim | +| Installation/distribution | Correct executable/desktop ID/icon/metainfo; package validation, fresh install/reinstall/remove and source-installed-loaded identity | `msdk pack` remains unimplemented; generated Cargo app is not an installable package | +| Reference consumers | T3 monitoring/GitHub actions through actual SDK adapters; comparable native/SDK semantics; read-only authority and account freshness | T3 core fixtures exist; full reference/host/real-account qualification open; Messages remains deferred | +| Performance | Same hardware/workload startup/RSS/idle CPU/wakeups/render metrics; explicit build/provision/queue timings | No new performance result claimed | +| Model usability | Exact licensed model/runtime/weights/prompt/spec; clean isolated no-network trials, same rubric, failures/repairs/interventions retained; independent visual/behavior acceptance | Six fixed benchmark specs exist; controlled repeated runs and baseline comparisons remain open | +| Release/API readiness | All above gates audited against final immutable artifacts, docs match shipped behavior, unsupported scope explicitly recorded | SDK v1 completion remains unproven | + +## Current verification environment + +This chat's Mac has Rust but no GTK development package. Core checks use an +isolated copy of msdk and its external fixture files to avoid resolving the +unrelated pinned Smithay Git dependency in the whole-OS workspace. Generated +model compilation uses cached Cargo dependencies. This is not Linux GTK, target +Pantheon, installation, hardware or owner visual acceptance evidence. + +`mseries/sdk-tests/prepare.py NEW_DIRECTORY` creates the reproducible SDK-only +workspace, keeping the checked-in dependency lock as its starting point. The +M-series SDK developer path workflow provisions Linux separately, tests/lints +core, builds/tests/lints a generated GTK app and captures its actual executable +in light/dark isolated desktops. Its resulting locks/source/hash/screenshots are +retained. Runtime and target-host gates remain open until those are exercised. diff --git a/mseries/sdk-tests/prepare.py b/mseries/sdk-tests/prepare.py new file mode 100644 index 000000000..312000595 --- /dev/null +++ b/mseries/sdk-tests/prepare.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Copy only the SDK dependency closure; never resolve the OS/compositor workspace.""" +import argparse +from pathlib import Path +import shutil + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('destination', type=Path) +args = parser.parse_args() +root = Path(__file__).resolve().parents[2] +dest = args.destination.resolve() +dest.mkdir(parents=True, exist_ok=False) +for name in ('msdk', 'msdk-gtk'): + shutil.copytree(root / 'rust' / name, dest / name, + ignore=shutil.ignore_patterns('target')) +for relative in ('mseries-greeter/data/greeter.ui.json', + 'claude-pantheon-rs/sdk', + 'pantheon-compositor/protocol/pantheon-desktop-shell-v1.xml'): + source = root / 'rust' / relative + target = dest / relative + target.parent.mkdir(parents=True, exist_ok=True) + if source.is_dir(): + shutil.copytree(source, target) + else: + shutil.copy2(source, target) +(dest / 'Cargo.toml').write_text('[workspace]\nmembers = ["msdk", "msdk-gtk"]\nresolver = "3"\n') +# Retain the checked-in versions as the starting lock. Cargo may prune unrelated +# OS packages; keep the resulting SDK-only lock as qualification evidence. +shutil.copy2(root / 'rust/Cargo.lock', dest / 'Cargo.lock') +print(dest) diff --git a/rust/msdk/README.md b/rust/msdk/README.md index 6d66df60d..8cf121b3a 100644 --- a/rust/msdk/README.md +++ b/rust/msdk/README.md @@ -1,17 +1,57 @@ -# msdk (Phase 1 core) +# M-series app SDK core and CLI -The closed UI vocabulary of the M-series app SDK and `msdk check`. No GUI dependencies; builds in seconds. -Design and rationale: [mseries/PLATFORM-PLAN.md](../../mseries/PLATFORM-PLAN.md). Benchmark: [mseries/sdk-bench](../../mseries/sdk-bench/). +The core provides descriptions (`.msdk` and JSON), checker/discovery/schema, +typed rules, semantic inspection, acceptance scenarios and standalone Rust app +generation. GTK hosting is in `msdk-gtk`. API/release status and remaining +qualification gates: [SDK completion ledger](../../mseries/SDK-COMPLETION.md). -```bash -cargo run -p msdk -- check msdk/examples/quick-notes.ui.json # one line per problem, with a JSON path, a code and a fix -cargo run -p msdk -- components # the vocabulary as JSON (prompts, grammar generation) +From `rust/`: + +```sh +cargo run -p msdk -- templates +cargo run -p msdk -- new settings /tmp/my-settings --id com.example.MySettings --name 'My Settings' +cargo run -p msdk -- check /tmp/my-settings/app.ui.json +cargo run -p msdk -- gate /tmp/my-settings/app.ui.json /tmp/my-settings/app.gates.json +cargo run -p msdk -- gen /tmp/my-settings/app.ui.json /tmp/my-settings-app --sdk-root "$PWD" +cargo run --manifest-path /tmp/my-settings-app/Cargo.toml ``` -Format v0 is JSON so a small model can be constrained to it with a grammar. An app description has `app`, -`capabilities`, `state` (text, bool, number, list), `messages` and a `window` built only from the components -listed by `msdk components`. `Custom` is the escape hatch and always raises a warning; each use is a vocabulary -gap to record in `mseries/sdk-bench/GAPS.md`. +Use new output directories: `new` and `gen` refuse existing destinations. Keep +the SDK checkout available; generated Cargo dependencies use its canonical local +paths. Generation creates an independent workspace so app builds do not resolve +the compositor/whole OS. The full OS workspace can still resolve unrelated Git +dependencies when building the CLI; use `mseries/sdk-tests/prepare.py` for the +isolated SDK workspace when those sources are unavailable. + +The supported native build path requires Linux, Rust 2024, GTK 4.20+ development +headers, pkg-config, Wayland headers and wayland-scanner. Generation/core checks +also run without GTK. Actual supported runtime and target-host acceptance remain +qualification gates; the generator is not a native macOS GTK port. + +`gen` emits a Cargo manifest, description, `src/lib.rs` (record structs, `State`, +`Message::into_event`, embedded document), `src/main.rs` (real GTK Host), initial +state round-trip test and app README. Number fields use `serde_json::Number` so +large integer measurements remain exact. Source names are preserved in serde +fields; Rust keywords/non-identifiers get collision-free Rust identifiers. +The executable supports `--screenshot output.png` for isolated render checks and +exits unsuccessfully if opening/rendering fails. Ordinary launches use the +stable application ID and reactivate the existing window. + +Custom service manifests next to the source description are embedded and +registered by `app_model::document()`. They declare types, not implementations: +implement `Services` and switch to `Host::open_with` for those backends. For +custom Rust logic, use generated models plus an explicit `LogicFactory`; +project generation rejects missing custom logic instead of inventing behavior. +Generation does not call services, provision credentials or install artifacts. + +After generation, `cargo fmt`, `cargo test --all-targets` and +`cargo clippy --all-targets -- -D warnings` check the standalone source. Changes +to the description's types require regenerating models in a new directory and +reviewing app-local changes. Views/rules stay inspectable in `app.ui.json`. -Not here yet: the `logic.rs` side (State/Msg/update), the runtime that renders the tree (Relm4 spike or own -runtime on gtk4-rs + granite-rs), `msdk shot`, `msdk pack`. +`msdk-run description --watch` is the separate interpreter preview loop. +`describe`, `complete`, `explain`, `schema` and `components` derive from the +checker vocabulary. `render` produces a semantic tree; `gate` runs headless +state/effect scenarios. Neither executes a real backend or proves live GTK +accessibility. `msdk pack`, `parity`, Studio and a live introspection registry +are not implemented commands. diff --git a/rust/msdk/src/generate.rs b/rust/msdk/src/generate.rs new file mode 100644 index 000000000..9192a59b6 --- /dev/null +++ b/rust/msdk/src/generate.rs @@ -0,0 +1,175 @@ +//! Inspectable Rust models and standalone GTK projects from checked descriptions. +//! Generation never executes app services or installs anything. + +use std::collections::BTreeMap; +use std::path::Path; + +use serde_json::Value; + +fn literal(value: &str) -> String { + format!("{value:?}") +} + +// Preserve ordinary names, including Rust keywords, and encode the few names +// Rust cannot represent as raw identifiers. The prefix is reserved by encoding +// user names beginning with it too, so distinct source names cannot collide. +fn ident(name: &str) -> String { + if !name.is_empty() + && name != "_" + && !matches!(name, "self" | "Self" | "super" | "crate") + && !name.starts_with("msdk_encoded_") + && !name.as_bytes()[0].is_ascii_digit() + && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') + { + format!("r#{name}") + } else { + format!("msdk_encoded_{}", name.as_bytes().iter().map(|b| format!("{b:02x}")).collect::()) + } +} + +fn rust_type(ty: &str) -> Result { + Ok(match ty { + "text" => "String".into(), + "bool" => "bool".into(), + "number" => "serde_json::Number".into(), + t if t.starts_with("list<") && t.ends_with('>') => { + format!("Vec", ident(&t[5..t.len() - 1])) + } + other => return Err(format!("gen: unsupported type {other:?}")), + }) +} + +fn checked(doc: &Value) -> Result<(), String> { + let errors: Vec<_> = + crate::check(doc).into_iter().filter(|d| d.severity == crate::Severity::Error).map(|d| d.to_string()).collect(); + if errors.is_empty() { Ok(()) } else { Err(errors.join("\n")) } +} + +/// Generate typed records, State and Message, with lossless JSON/event bridges. +/// Number uses serde_json::Number rather than rounding integer observations to f64. +pub fn models(doc: &Value) -> Result { + checked(doc)?; + let mut out = String::from( + "// Generated by msdk gen. Edit the description and regenerate in a new directory.\n\ + #![allow(non_snake_case, non_camel_case_types)]\n\ + use serde::{Deserialize, Serialize};\n\ + pub mod records {\n use serde::{Deserialize, Serialize};\n", + ); + for (name, fields) in doc["types"].as_object().into_iter().flatten() { + out.push_str(&format!( + " #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\n pub struct {} {{\n", + ident(name) + )); + for (name, ty) in fields.as_object().into_iter().flatten() { + let ty = rust_type(ty.as_str().ok_or("gen: field type must be a string")?)?; + out.push_str(&format!( + " #[serde(rename = {})]\n pub {}: {ty},\n", + literal(name), + ident(name) + )); + } + out.push_str(" }\n"); + } + out.push_str("}\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub struct State {\n"); + for (name, ty) in doc["state"].as_object().into_iter().flatten() { + let ty = rust_type(ty.as_str().ok_or("gen: state type must be a string")?)?; + out.push_str(&format!(" #[serde(rename = {})]\n pub {}: {ty},\n", literal(name), ident(name))); + } + out.push_str("}\nimpl State {\n pub fn initial() -> Result {\n Self::from_store(msdk::logic::initial_store(&document()))\n }\n pub fn from_store(store: msdk::logic::Store) -> Result {\n serde_json::from_value(serde_json::Value::Object(store))\n }\n pub fn into_store(self) -> Result {\n match serde_json::to_value(self)? {\n serde_json::Value::Object(store) => Ok(store),\n _ => unreachable!(\"State serializes as an object\"),\n }\n }\n}\n#[derive(Debug, Clone, PartialEq)]\npub enum Message {\n"); + for (name, ty) in doc["messages"].as_object().into_iter().flatten() { + let payload = if ty == "none" { + String::new() + } else { + format!("({})", rust_type(ty.as_str().ok_or("gen: message type must be a string")?)?) + }; + out.push_str(&format!(" {}{payload},\n", ident(name))); + } + out.push_str("}\nimpl Message {\n pub fn into_event(self) -> (String, Option) {\n match self {\n"); + for (name, ty) in doc["messages"].as_object().into_iter().flatten() { + let (pat, payload): (String, String) = if ty == "none" { + (String::new(), "None".into()) + } else { + ("(value)".into(), "Some(serde_json::json!(value))".into()) + }; + out.push_str(&format!(" Self::{}{pat} => ({}.into(), {payload}),\n", ident(name), literal(name))); + } + out.push_str(" }\n }\n}\n\n/// The checked description embedded in this application.\npub fn document() -> serde_json::Value {\n serde_json::from_str(include_str!(\"../app.ui.json\")).expect(\"generated checked description\")\n}\n"); + Ok(out) +} + +/// Return a complete standalone Cargo project without mutating the filesystem. +/// Custom service manifests are not silently invented or replaced by fixtures. +pub fn project(doc: &Value, sdk_root: &Path) -> Result, String> { + project_with_manifests(doc, sdk_root, &[]) +} + +pub fn project_with_manifests( + doc: &Value, + sdk_root: &Path, + manifests: &[Value], +) -> Result, String> { + for manifest in manifests { + crate::vocab::load_manifest(manifest)?; + } + checked(doc)?; + if doc["logic"] == "rust" { + return Err( + "gen: Rust logic must be supplied by the app; use models() and Host::open_with with a LogicFactory".into(), + ); + } + let root = sdk_root.canonicalize().map_err(|e| format!("gen: SDK root: {e}"))?; + for name in ["msdk", "msdk-gtk"] { + if !root.join(name).join("Cargo.toml").is_file() { + return Err(format!("gen: SDK root must contain {name}/Cargo.toml")); + } + } + let surfaces = doc["surfaces"].as_object().ok_or("gen: missing surfaces")?; + let surface = + if surfaces.contains_key("main") { "main" } else { surfaces.keys().next().ok_or("gen: no surface")? }; + let id = doc["app"]["id"].as_str().ok_or("gen: missing app id")?; + let package = format!("msdk-{}", id.rsplit('.').next().unwrap_or("app").replace('_', "-").to_ascii_lowercase()); + let root_text = root.to_str().ok_or("gen: SDK root must be UTF-8")?; + let mut files = BTreeMap::new(); + files.insert("Cargo.toml".into(), format!( + "[package]\nname = {package:?}\nversion = \"0.1.0\"\nedition = \"2024\"\npublish = false\n\n[workspace]\n\n[lib]\nname = \"app_model\"\n\n[dependencies]\nmsdk = {{ path = {} }}\nmsdk-gtk = {{ path = {} }}\ngtk = {{ package = \"gtk4\", version = \"0.11\", features = [\"v4_20\"] }}\nserde = {{ version = \"1\", features = [\"derive\"] }}\nserde_json = \"1\"\n", + serde_json::to_string(&format!("{root_text}/msdk")).unwrap(), + serde_json::to_string(&format!("{root_text}/msdk-gtk")).unwrap(), + )); + files.insert("app.ui.json".into(), serde_json::to_string_pretty(doc).map_err(|e| e.to_string())? + "\n"); + let mut model = models(doc)?; + let mut registration = String::new(); + for manifest in manifests { + let manifest = serde_json::to_string(manifest).map_err(|e| e.to_string())?; + registration.push_str(&format!(" msdk::vocab::load_manifest(&serde_json::from_str({}).expect(\"embedded service manifest\")).expect(\"checked service manifest\");\n", literal(&manifest))); + } + model = model.replace( + "pub fn document() -> serde_json::Value {\n", + &format!("pub fn document() -> serde_json::Value {{\n{registration}"), + ); + files.insert("src/lib.rs".into(), model); + files.insert( + "src/main.rs".into(), + include_str!("../templates/project/main.rs") + .replace("@APP_ID@", &literal(id)) + .replace("@SURFACE@", &literal(surface)), + ); + files.insert("README.md".into(), format!( + "# {}\n\nGenerated by `msdk gen`; SDK format {}.\n\nOn a supported Linux GTK 4.20+ development host, with Rust 2024, pkg-config,\nGTK development headers, Wayland development headers and wayland-scanner installed:\n\n```sh\ncargo build\ncargo test\ncargo run\n```\n\n`src/lib.rs` exposes typed records, `State`, `Message::into_event` and the embedded\ndescription. Edit `app.ui.json` for views/rules, or use a new output directory to\nregenerate models after changing types. Keep the SDK checkout at `{root_text}`;\nthe generated project uses explicit local dependencies and its own Cargo workspace.\n\nThe executable runs the real GTK Host with the description's update rules and\nbuilt-in services. Custom services/feeds require an app-local `Services` adapter\nand `Host::open_with`; generation does not implement those backends. Panel\nplacement requires the documented Pantheon shell protocol and target-host tests.\nBuild/launch success is separate from visual, accessibility, service, packaging\nand installed desktop acceptance. No install or background service is performed\nby generation.\n", + doc["app"]["name"].as_str().unwrap_or("SDK app"), crate::FORMAT_VERSION, + )); + files.insert("tests/model.rs".into(), "#[test]\nfn initial_state_round_trips() {\n let state = app_model::State::initial().expect(\"typed initial state\");\n let store = state.clone().into_store().unwrap();\n assert_eq!(app_model::State::from_store(store).unwrap(), state);\n}\n".into()); + Ok(files) +} + +/// Creates a new directory exclusively; existing projects are never overwritten. +pub fn write_project(dir: &Path, files: &BTreeMap) -> Result<(), String> { + std::fs::create_dir(dir).map_err(|e| format!("gen: {}: {e}", dir.display()))?; + for (name, body) in files { + let path = dir.join(name); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + std::fs::write(path, body).map_err(|e| e.to_string())?; + } + Ok(()) +} diff --git a/rust/msdk/src/lib.rs b/rust/msdk/src/lib.rs index 536b3fb7d..ce0c9be04 100644 --- a/rust/msdk/src/lib.rs +++ b/rust/msdk/src/lib.rs @@ -10,6 +10,7 @@ pub mod agent; mod check; pub mod contrast; pub mod gate; +pub mod generate; pub mod inspect; pub mod intents; pub mod logic; diff --git a/rust/msdk/src/main.rs b/rust/msdk/src/main.rs index 7fe06ee39..c15110120 100644 --- a/rust/msdk/src/main.rs +++ b/rust/msdk/src/main.rs @@ -80,9 +80,10 @@ fn main() -> ExitCode { ExitCode::SUCCESS } Some("new") if args.len() > 2 => run_new(&args[1..]), + Some("gen") if args.len() > 2 => run_gen(&args[1..]), _ => { eprintln!( - "usage: msdk check [--json] ...\n msdk describe [Name] | explain | complete \n msdk gate [gates.json]\n msdk agent-manifest [--mcp] [--services f.json]\n msdk render [--surface NAME] [--state JSON] [--json]\n msdk fmt [--check|--stdout] ... | text | json \n msdk templates | new