docs(plan): add dual-app shared-core implementation plan
This commit is contained in:
parent
e2a2d4a55e
commit
87450af171
|
|
@ -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`
|
||||
Loading…
Reference in New Issue