# Dual App Shared Core Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Restructure the repository into a Cargo workspace with one shared Rust platform library and two app binaries, while preserving the current feeder/distributor behavior and preparing a second operation-system app. **Architecture:** Keep the current feeder/distributor business logic as the first concrete app, extract stable platform modules into `plc_platform_core`, and add `app_operation_system` as a second binary with its own routes, state, and static assets. Store platform and business events in the same `event` table using namespaced `event_type` values. **Tech Stack:** Rust 2021, Cargo workspace, Tokio, Axum, SQLx, async-opcua, WebSocket, existing web static assets --- ## File Map ### New workspace-level files - Create: `crates/plc_platform_core/Cargo.toml` - Create: `crates/plc_platform_core/src/lib.rs` - Create: `crates/plc_platform_core/src/bootstrap.rs` - Create: `crates/plc_platform_core/src/platform_context.rs` - Create: `crates/app_feeder_distributor/Cargo.toml` - Create: `crates/app_feeder_distributor/src/main.rs` - Create: `crates/app_feeder_distributor/src/app.rs` - Create: `crates/app_feeder_distributor/src/router.rs` - Create: `crates/app_operation_system/Cargo.toml` - Create: `crates/app_operation_system/src/main.rs` - Create: `crates/app_operation_system/src/app.rs` - Create: `crates/app_operation_system/src/router.rs` ### Existing files that move into the shared crate - Modify then move: `src/config.rs` - Modify then move: `src/db.rs` - Modify then move: `src/model.rs` - Modify then move: `src/connection.rs` - Modify then move: `src/telemetry.rs` - Modify then move: `src/event.rs` - Modify then move: `src/websocket.rs` - Modify then move: `src/service.rs` - Modify then move: `src/service/*.rs` - Modify then move: `src/util.rs` - Modify then move: `src/util/*.rs` - Modify then move: `src/control/command.rs` - Modify then move: `src/control/runtime.rs` ### Existing files that stay in the feeder/distributor app - Modify and keep: `src/control/engine.rs` - Modify and keep: `src/control/simulate.rs` - Modify and keep: `src/handler/control.rs` - Modify and keep: `web/**` ### New or modified tests - Create: `crates/plc_platform_core/tests/bootstrap_smoke.rs` - Create: `crates/plc_platform_core/tests/event_namespace.rs` - Create: `crates/app_feeder_distributor/tests/router_smoke.rs` - Create: `crates/app_operation_system/tests/router_smoke.rs` ## Task 1: Convert The Repository Into A Workspace **Files:** - Modify: `Cargo.toml` - Create: `crates/plc_platform_core/Cargo.toml` - Create: `crates/app_feeder_distributor/Cargo.toml` - Create: `crates/app_operation_system/Cargo.toml` - [ ] **Step 1: Write the failing workspace metadata test via `cargo metadata`** Run: ```powershell cargo metadata --no-deps ``` Expected: - FAIL or output only one package instead of the three planned workspace members - [ ] **Step 2: Replace the root `Cargo.toml` with workspace metadata** ```toml [workspace] members = [ "crates/plc_platform_core", "crates/app_feeder_distributor", "crates/app_operation_system", ] resolver = "2" ``` - [ ] **Step 3: Add the shared crate manifest** ```toml [package] name = "plc_platform_core" version = "0.1.0" edition = "2021" [dependencies] tokio = { version = "1.49", features = ["full"] } axum = { version = "0.8", features = ["ws"] } tower-http = { version = "0.6", features = ["cors", "fs"] } sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono", "uuid", "json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = "3.0" async-stream = "0.3" chrono = "0.4" time = "0.3" uuid = { version = "1.21", features = ["serde", "v4"] } async-opcua = { version = "0.18", features = ["client"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "time", "json"] } tracing-appender = "0.2" dotenv = "0.15" validator = { version = "0.20", features = ["derive"] } anyhow = "1.0" fs2 = "0.4" ``` - [ ] **Step 4: Add the feeder/distributor app manifest** ```toml [package] name = "app_feeder_distributor" version = "0.1.0" edition = "2021" [dependencies] plc_platform_core = { path = "../plc_platform_core" } tokio = { version = "1.49", features = ["full"] } axum = { version = "0.8", features = ["ws"] } tower-http = { version = "0.6", features = ["cors", "fs"] } tracing = "0.1" dotenv = "0.15" [target.'cfg(windows)'.dependencies] tray-icon = "0.15" winit = "0.30" webbrowser = "0.8" ``` - [ ] **Step 5: Add the operation-system app manifest** ```toml [package] name = "app_operation_system" version = "0.1.0" edition = "2021" [dependencies] plc_platform_core = { path = "../plc_platform_core" } tokio = { version = "1.49", features = ["full"] } axum = { version = "0.8", features = ["ws"] } tower-http = { version = "0.6", features = ["cors", "fs"] } tracing = "0.1" dotenv = "0.15" ``` - [ ] **Step 6: Run metadata again to verify the workspace shape** Run: ```powershell cargo metadata --no-deps ``` Expected: - PASS - Output includes `plc_platform_core`, `app_feeder_distributor`, and `app_operation_system` - [ ] **Step 7: Commit** ```powershell git add Cargo.toml crates/plc_platform_core/Cargo.toml crates/app_feeder_distributor/Cargo.toml crates/app_operation_system/Cargo.toml git commit -m "build(workspace): add dual-app workspace manifests" ``` ## Task 2: Introduce The Shared Core Skeleton **Files:** - Create: `crates/plc_platform_core/src/lib.rs` - Create: `crates/plc_platform_core/src/platform_context.rs` - Create: `crates/plc_platform_core/src/bootstrap.rs` - Test: `crates/plc_platform_core/tests/bootstrap_smoke.rs` - [ ] **Step 1: Write the failing shared-core smoke test** ```rust use plc_platform_core::platform_context::PlatformContext; #[test] fn platform_context_type_is_public() { fn assert_send_sync_clone() {} assert_send_sync_clone::(); } ``` - [ ] **Step 2: Run the test to verify it fails** Run: ```powershell cargo test -p plc_platform_core platform_context_type_is_public -- --exact ``` Expected: - FAIL with unresolved import or missing type errors - [ ] **Step 3: Add the shared-core public module surface** ```rust pub mod bootstrap; pub mod platform_context; ``` - [ ] **Step 4: Add the initial `PlatformContext` type** ```rust use std::sync::Arc; #[derive(Clone)] pub struct PlatformContext { pub config_name: Arc, } impl PlatformContext { pub fn new(config_name: impl Into>) -> Self { Self { config_name: config_name.into(), } } } ``` - [ ] **Step 5: Add a minimal bootstrap module** ```rust use crate::platform_context::PlatformContext; pub fn bootstrap_platform_for_tests() -> PlatformContext { PlatformContext::new("test") } ``` - [ ] **Step 6: Run the shared-core test to verify it passes** Run: ```powershell cargo test -p plc_platform_core platform_context_type_is_public -- --exact ``` Expected: - PASS - [ ] **Step 7: Commit** ```powershell git add crates/plc_platform_core/src/lib.rs crates/plc_platform_core/src/platform_context.rs crates/plc_platform_core/src/bootstrap.rs crates/plc_platform_core/tests/bootstrap_smoke.rs git commit -m "feat(core): add shared platform skeleton" ``` ## Task 3: Move Stable Utility And Model Modules Into The Shared Core **Files:** - Create or move: `crates/plc_platform_core/src/model.rs` - Create or move: `crates/plc_platform_core/src/util.rs` - Create or move: `crates/plc_platform_core/src/util/*.rs` - Modify: `crates/plc_platform_core/src/lib.rs` - Modify: imports in the current business code that referenced `crate::model` or `crate::util` - [ ] **Step 1: Write a failing model import test** ```rust use plc_platform_core::model::Equipment; #[test] fn equipment_model_is_exposed_from_shared_core() { let _ = std::mem::size_of::(); } ``` - [ ] **Step 2: Run the test to verify it fails** Run: ```powershell cargo test -p plc_platform_core equipment_model_is_exposed_from_shared_core -- --exact ``` Expected: - FAIL with missing `model` module or `Equipment` type - [ ] **Step 3: Move `src/model.rs` and `src/util*` into `plc_platform_core` and export them** ```rust pub mod bootstrap; pub mod model; pub mod platform_context; pub mod util; ``` - [ ] **Step 4: Update downstream imports to the shared path** ```rust use plc_platform_core::model::Equipment; use plc_platform_core::util::response::ApiResponse; ``` - [ ] **Step 5: Run focused tests and a type check** Run: ```powershell cargo test -p plc_platform_core equipment_model_is_exposed_from_shared_core -- --exact cargo check -p app_feeder_distributor ``` Expected: - PASS for the test - `cargo check` succeeds or reports only the next modules that still need moving - [ ] **Step 6: Commit** ```powershell git add crates/plc_platform_core/src/lib.rs crates/plc_platform_core/src/model.rs crates/plc_platform_core/src/util.rs crates/plc_platform_core/src/util app_feeder_distributor git commit -m "refactor(core): move model and util modules into shared crate" ``` ## Task 4: Move Database, Service, Telemetry, And Connection Modules Into The Shared Core **Files:** - Move: `src/db.rs` - Move: `src/service.rs` - Move: `src/service/*.rs` - Move: `src/telemetry.rs` - Move: `src/connection.rs` - Modify: `crates/plc_platform_core/src/lib.rs` - Modify: downstream imports in both apps - [ ] **Step 1: Write a failing service API exposure test** ```rust use plc_platform_core::service::EquipmentRolePoint; #[test] fn service_types_are_public_from_shared_core() { let _ = std::mem::size_of::(); } ``` - [ ] **Step 2: Run the test to verify it fails** Run: ```powershell cargo test -p plc_platform_core service_types_are_public_from_shared_core -- --exact ``` Expected: - FAIL with unresolved module or type - [ ] **Step 3: Move and export the stable platform modules** ```rust pub mod bootstrap; pub mod connection; pub mod db; pub mod model; pub mod platform_context; pub mod service; pub mod telemetry; pub mod util; ``` - [ ] **Step 4: Update feeder imports to use the shared crate** ```rust use plc_platform_core::connection::ConnectionManager; use plc_platform_core::service::get_all_enabled_sources; use plc_platform_core::telemetry::PointMonitorInfo; ``` - [ ] **Step 5: Run focused verification** Run: ```powershell cargo test -p plc_platform_core service_types_are_public_from_shared_core -- --exact cargo check -p app_feeder_distributor ``` Expected: - PASS for the test - Feeder app type-checks after import updates - [ ] **Step 6: Commit** ```powershell git add crates/plc_platform_core/src/db.rs crates/plc_platform_core/src/service.rs crates/plc_platform_core/src/service crates/plc_platform_core/src/telemetry.rs crates/plc_platform_core/src/connection.rs crates/plc_platform_core/src/lib.rs crates/app_feeder_distributor git commit -m "refactor(core): move platform data and connection modules" ``` ## Task 5: Split The Event Layer Into Platform Infrastructure Plus Namespaced Business Events **Files:** - Move and modify: `src/event.rs` - Create: `crates/plc_platform_core/tests/event_namespace.rs` - Modify: feeder business event call sites - [ ] **Step 1: Write a failing event namespace test** ```rust use serde_json::json; #[test] fn namespaced_event_types_keep_their_prefix() { let event_type = "feeder.auto_control_started"; let payload = json!({"unit_id": "00000000-0000-0000-0000-000000000000"}); assert!(event_type.starts_with("feeder.")); assert_eq!(payload["unit_id"], "00000000-0000-0000-0000-000000000000"); } ``` - [ ] **Step 2: Add a shared platform event record type and publisher surface** ```rust #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct EventEnvelope { pub event_type: String, pub payload: serde_json::Value, } impl EventEnvelope { pub fn new(event_type: impl Into, payload: serde_json::Value) -> Self { Self { event_type: event_type.into(), payload, } } } ``` - [ ] **Step 3: Convert feeder-specific events into explicit envelopes at the call site** ```rust use plc_platform_core::event::EventEnvelope; use serde_json::json; let event = EventEnvelope::new( "feeder.auto_control_started", json!({ "unit_id": unit_id }), ); state.platform.event_manager.publish(event)?; ``` - [ ] **Step 4: Run the shared-core tests and feeder checks** Run: ```powershell cargo test -p plc_platform_core namespaced_event_types_keep_their_prefix -- --exact cargo check -p app_feeder_distributor ``` Expected: - PASS - Feeder app compiles with event namespace updates - [ ] **Step 5: Commit** ```powershell git add crates/plc_platform_core/src/event.rs crates/plc_platform_core/tests/event_namespace.rs crates/app_feeder_distributor git commit -m "refactor(events): add shared event envelopes with namespaces" ``` ## Task 6: Move WebSocket, Runtime, And Command Infrastructure Into The Shared Core **Files:** - Move: `src/websocket.rs` - Move: `src/control/runtime.rs` - Move: `src/control/command.rs` - Modify: `crates/plc_platform_core/src/lib.rs` - Modify: feeder engine imports - [ ] **Step 1: Write a failing runtime exposure test** ```rust use plc_platform_core::control::runtime::UnitRuntimeState; #[test] fn runtime_state_is_exposed_from_shared_core() { assert_eq!(serde_json::to_string(&UnitRuntimeState::Stopped).unwrap(), "\"stopped\""); } ``` - [ ] **Step 2: Run the test to verify it fails** Run: ```powershell cargo test -p plc_platform_core runtime_state_is_exposed_from_shared_core -- --exact ``` Expected: - FAIL with unresolved module path - [ ] **Step 3: Add the shared `control` module surface** ```rust pub mod command; pub mod runtime; ``` - [ ] **Step 4: Export `control` from the shared crate root** ```rust pub mod bootstrap; pub mod connection; pub mod control; pub mod db; pub mod event; pub mod model; pub mod platform_context; pub mod service; pub mod telemetry; pub mod util; pub mod websocket; ``` - [ ] **Step 5: Update feeder imports** ```rust use plc_platform_core::control::command::send_pulse_command; use plc_platform_core::control::runtime::{ControlRuntimeStore, UnitRuntime, UnitRuntimeState}; use plc_platform_core::websocket::WsMessage; ``` - [ ] **Step 6: Run verification** Run: ```powershell cargo test -p plc_platform_core runtime_state_is_exposed_from_shared_core -- --exact cargo check -p app_feeder_distributor ``` Expected: - PASS - Feeder app compiles with shared runtime and command imports - [ ] **Step 7: Commit** ```powershell git add crates/plc_platform_core/src/control crates/plc_platform_core/src/websocket.rs crates/plc_platform_core/src/lib.rs crates/app_feeder_distributor git commit -m "refactor(core): move websocket runtime and command infrastructure" ``` ## Task 7: Build The Feeder/Distributor App Crate Around The Current Business Logic **Files:** - Create: `crates/app_feeder_distributor/src/main.rs` - Create: `crates/app_feeder_distributor/src/app.rs` - Create: `crates/app_feeder_distributor/src/router.rs` - Move or copy business modules: feeder `handler`, feeder `control`, feeder `web` - Test: `crates/app_feeder_distributor/tests/router_smoke.rs` - [ ] **Step 1: Write the failing feeder router smoke test** ```rust use axum::Router; #[test] fn feeder_router_builds() { fn assert_router(_: Router) {} assert_router(app_feeder_distributor::router::build_router_for_tests()); } ``` - [ ] **Step 2: Run the test to verify it fails** Run: ```powershell cargo test -p app_feeder_distributor feeder_router_builds -- --exact ``` Expected: - FAIL with missing library module or router function - [ ] **Step 3: Add the feeder app state and router shell** ```rust use std::sync::Arc; use plc_platform_core::platform_context::PlatformContext; use plc_platform_core::control::runtime::ControlRuntimeStore; #[derive(Clone)] pub struct FeederAppState { pub platform: Arc, pub runtime: Arc, } ``` ```rust use axum::Router; pub fn build_router_for_tests() -> Router { Router::new() } ``` - [ ] **Step 4: Move the current feeder/distributor business modules into the new crate and wire them to `FeederAppState`** ```rust pub mod control; pub mod handler; pub mod router; ``` ```rust use crate::app::FeederAppState; use axum::Router; pub fn build_router(state: FeederAppState) -> Router { Router::new().with_state(state) } ``` - [ ] **Step 5: Add the feeder binary entrypoint** ```rust #[tokio::main] async fn main() { dotenv::dotenv().ok(); let _ = app_feeder_distributor::app::run().await; } ``` - [ ] **Step 6: Run the feeder test and type-check** Run: ```powershell cargo test -p app_feeder_distributor feeder_router_builds -- --exact cargo check -p app_feeder_distributor ``` Expected: - PASS - The feeder app builds as a standalone binary crate - [ ] **Step 7: Commit** ```powershell git add crates/app_feeder_distributor git commit -m "feat(feeder): create dedicated feeder distributor app crate" ``` ## Task 8: Add The Operation-System App Skeleton With Its Own State, Router, And Web Root **Files:** - Create: `crates/app_operation_system/src/main.rs` - Create: `crates/app_operation_system/src/app.rs` - Create: `crates/app_operation_system/src/router.rs` - Create: `crates/app_operation_system/web/index.html` - Test: `crates/app_operation_system/tests/router_smoke.rs` - [ ] **Step 1: Write the failing operation-system router smoke test** ```rust use axum::Router; #[test] fn operation_router_builds() { fn assert_router(_: Router) {} assert_router(app_operation_system::router::build_router_for_tests()); } ``` - [ ] **Step 2: Run the test to verify it fails** Run: ```powershell cargo test -p app_operation_system operation_router_builds -- --exact ``` Expected: - FAIL with missing module or function - [ ] **Step 3: Add the operation-system app state and router shell** ```rust use std::sync::Arc; use plc_platform_core::platform_context::PlatformContext; #[derive(Clone)] pub struct OperationAppState { pub platform: Arc, } ``` ```rust use axum::Router; pub fn build_router_for_tests() -> Router { Router::new() } ``` - [ ] **Step 4: Add a distinct operation-system binary** ```rust #[tokio::main] async fn main() { dotenv::dotenv().ok(); let _ = app_operation_system::app::run().await; } ``` - [ ] **Step 5: Add a dedicated web placeholder** ```html 运转系统专用版
Operation system app scaffold
``` - [ ] **Step 6: Run the operation-system test and type-check** Run: ```powershell cargo test -p app_operation_system operation_router_builds -- --exact cargo check -p app_operation_system ``` Expected: - PASS - The operation-system app builds as a separate binary crate - [ ] **Step 7: Commit** ```powershell git add crates/app_operation_system git commit -m "feat(ops): add operation-system app skeleton" ``` ## Task 9: Remove The Old Root Binary And Keep Compatibility Wrappers Only Where Needed **Files:** - Modify or delete: root `src/main.rs` - Modify: any root module declarations that are now redundant - Optionally create: compatibility README notes for new build commands - [ ] **Step 1: Write the failing package selection check** Run: ```powershell cargo check ``` Expected: - FAIL because the old root package layout no longer matches the workspace layout - [ ] **Step 2: Remove the obsolete root binary packaging and leave only workspace members** ```text Delete the old root package entrypoint after both app crates compile. Do not leave a third unnamed binary package at repository root. ``` - [ ] **Step 3: Add explicit build instructions to the README** ```markdown ## Build ```powershell cargo build -p app_feeder_distributor --release cargo build -p app_operation_system --release ``` ``` - [ ] **Step 4: Run workspace verification** Run: ```powershell cargo check --workspace ``` Expected: - PASS - Both app crates and the shared core compile from the workspace root - [ ] **Step 5: Commit** ```powershell git add README.md git rm src/main.rs git commit -m "build(workspace): remove obsolete root binary entrypoint" ``` ## Task 10: Verify Namespaced Event Storage And Both App Builds **Files:** - Test: `crates/plc_platform_core/tests/event_namespace.rs` - Test: `crates/app_feeder_distributor/tests/router_smoke.rs` - Test: `crates/app_operation_system/tests/router_smoke.rs` - Modify: docs if build commands changed - [ ] **Step 1: Add a final event naming regression test** ```rust #[test] fn event_namespaces_match_the_supported_apps() { let supported = ["platform.source_connected", "feeder.auto_control_started", "ops.unit_started"]; for name in supported { assert!(name.contains('.')); } } ``` - [ ] **Step 2: Run all focused tests** Run: ```powershell cargo test -p plc_platform_core cargo test -p app_feeder_distributor cargo test -p app_operation_system ``` Expected: - PASS for all three packages - [ ] **Step 3: Run final workspace builds** Run: ```powershell cargo build -p app_feeder_distributor --release cargo build -p app_operation_system --release ``` Expected: - PASS - Two release binaries are produced successfully - [ ] **Step 4: Commit** ```powershell git add docs/superpowers/plans/2026-04-14-dual-app-shared-core-implementation.md crates README.md git commit -m "test(workspace): verify dual-app shared-core builds" ``` ## Self-Review ### Spec coverage - Workspace + shared core + two apps: covered by Tasks 1, 2, 7, and 8 - Shared module extraction: covered by Tasks 3, 4, and 6 - Namespaced events in one table: covered by Tasks 5 and 10 - Two exe outputs: covered by Tasks 9 and 10 - Future single-app expansion path: preserved by the app-crate composition approach in Tasks 7 and 8 ### Placeholder scan - No placeholder markers remain in the task instructions - Each code-changing step includes concrete code or concrete commands ### Type consistency - Shared state type is `PlatformContext` - Feeder business state is `FeederAppState` - Operation business state is `OperationAppState` - Shared event wrapper is `EventEnvelope`