22 KiB
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:
cargo metadata --no-deps
Expected:
-
FAIL or output only one package instead of the three planned workspace members
-
Step 2: Replace the root
Cargo.tomlwith workspace metadata
[workspace]
members = [
"crates/plc_platform_core",
"crates/app_feeder_distributor",
"crates/app_operation_system",
]
resolver = "2"
- Step 3: Add the shared crate manifest
[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
[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
[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:
cargo metadata --no-deps
Expected:
-
PASS
-
Output includes
plc_platform_core,app_feeder_distributor, andapp_operation_system -
Step 7: Commit
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
use plc_platform_core::platform_context::PlatformContext;
#[test]
fn platform_context_type_is_public() {
fn assert_send_sync_clone<T: Send + Sync + Clone>() {}
assert_send_sync_clone::<PlatformContext>();
}
- Step 2: Run the test to verify it fails
Run:
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
pub mod bootstrap;
pub mod platform_context;
- Step 4: Add the initial
PlatformContexttype
use std::sync::Arc;
#[derive(Clone)]
pub struct PlatformContext {
pub config_name: Arc<str>,
}
impl PlatformContext {
pub fn new(config_name: impl Into<Arc<str>>) -> Self {
Self {
config_name: config_name.into(),
}
}
}
- Step 5: Add a minimal bootstrap module
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:
cargo test -p plc_platform_core platform_context_type_is_public -- --exact
Expected:
-
PASS
-
Step 7: Commit
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::modelorcrate::util -
Step 1: Write a failing model import test
use plc_platform_core::model::Equipment;
#[test]
fn equipment_model_is_exposed_from_shared_core() {
let _ = std::mem::size_of::<Equipment>();
}
- Step 2: Run the test to verify it fails
Run:
cargo test -p plc_platform_core equipment_model_is_exposed_from_shared_core -- --exact
Expected:
-
FAIL with missing
modelmodule orEquipmenttype -
Step 3: Move
src/model.rsandsrc/util*intoplc_platform_coreand export them
pub mod bootstrap;
pub mod model;
pub mod platform_context;
pub mod util;
- Step 4: Update downstream imports to the shared path
use plc_platform_core::model::Equipment;
use plc_platform_core::util::response::ApiResponse;
- Step 5: Run focused tests and a type check
Run:
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 checksucceeds or reports only the next modules that still need moving -
Step 6: Commit
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
use plc_platform_core::service::EquipmentRolePoint;
#[test]
fn service_types_are_public_from_shared_core() {
let _ = std::mem::size_of::<EquipmentRolePoint>();
}
- Step 2: Run the test to verify it fails
Run:
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
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
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:
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
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
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
#[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<String>, 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
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:
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
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
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:
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
controlmodule surface
pub mod command;
pub mod runtime;
- Step 4: Export
controlfrom the shared crate root
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
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:
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
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, feedercontrol, feederweb -
Test:
crates/app_feeder_distributor/tests/router_smoke.rs -
Step 1: Write the failing feeder router smoke test
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:
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
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<PlatformContext>,
pub runtime: Arc<ControlRuntimeStore>,
}
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
pub mod control;
pub mod handler;
pub mod router;
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
#[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:
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
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
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:
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
use std::sync::Arc;
use plc_platform_core::platform_context::PlatformContext;
#[derive(Clone)]
pub struct OperationAppState {
pub platform: Arc<PlatformContext>,
}
use axum::Router;
pub fn build_router_for_tests() -> Router {
Router::new()
}
- Step 4: Add a distinct operation-system binary
#[tokio::main]
async fn main() {
dotenv::dotenv().ok();
let _ = app_operation_system::app::run().await;
}
- Step 5: Add a dedicated web placeholder
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>运转系统专用版</title>
</head>
<body>
<main>Operation system app scaffold</main>
</body>
</html>
- Step 6: Run the operation-system test and type-check
Run:
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
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:
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
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
## 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
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
#[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:
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:
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
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