diff --git a/docs/superpowers/plans/2026-04-14-dual-app-shared-core-implementation.md b/docs/superpowers/plans/2026-04-14-dual-app-shared-core-implementation.md new file mode 100644 index 0000000..366999c --- /dev/null +++ b/docs/superpowers/plans/2026-04-14-dual-app-shared-core-implementation.md @@ -0,0 +1,887 @@ +# 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`