docs(plan): add dual-app shared-core implementation plan

This commit is contained in:
caoqianming 2026-04-14 15:49:08 +08:00
parent e2a2d4a55e
commit 87450af171
1 changed files with 887 additions and 0 deletions

View File

@ -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<T: Send + Sync + Clone>() {}
assert_send_sync_clone::<PlatformContext>();
}
```
- [ ] **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<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**
```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::<Equipment>();
}
```
- [ ] **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::<EquipmentRolePoint>();
}
```
- [ ] **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<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**
```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<PlatformContext>,
pub runtime: Arc<ControlRuntimeStore>,
}
```
```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<PlatformContext>,
}
```
```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
<!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:
```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`