From e08cf2af3459add59aac798cd3940e149788bc3f Mon Sep 17 00:00:00 2001 From: Noel Saw <56978803+noelsaw1@users.noreply.github.com> Date: Tue, 26 Aug 2025 08:26:24 -0700 Subject: [PATCH] Add FSM-based NHK PoC plugin starter --- assets/dist/fsm.js | 41 ++++++++++++++++++++ assets/ts/fsm.ts | 49 +++++++++++++++++++++++ changelog.md | 4 ++ composer.json | 12 ++++++ nhk-poc.php | 37 ++++++++++++++++++ package.json | 10 +++++ src/Admin/Menu.php | 73 +++++++++++++++++++++++++++++++++++ src/Api/AjaxHandler.php | 33 ++++++++++++++++ src/Core/FSM/StateMachine.php | 61 +++++++++++++++++++++++++++++ src/Core/Plugin.php | 70 +++++++++++++++++++++++++++++++++ 10 files changed, 390 insertions(+) create mode 100644 assets/dist/fsm.js create mode 100644 assets/ts/fsm.ts create mode 100644 changelog.md create mode 100644 composer.json create mode 100644 nhk-poc.php create mode 100644 package.json create mode 100644 src/Admin/Menu.php create mode 100644 src/Api/AjaxHandler.php create mode 100644 src/Core/FSM/StateMachine.php create mode 100644 src/Core/Plugin.php diff --git a/assets/dist/fsm.js b/assets/dist/fsm.js new file mode 100644 index 0000000..cc9c2bc --- /dev/null +++ b/assets/dist/fsm.js @@ -0,0 +1,41 @@ +"use strict"; +/** + * Shared FSM implementation in TypeScript. + * Mirrors PHP backend to maintain SSoT across contexts. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.StateMachine = void 0; +class StateMachine { + constructor(transitions, initial) { + this.transitions = transitions; + this.history = []; + this.state = initial; + this.history.push(initial); + } + transition(to) { + const allowed = this.transitions[this.state] || []; + if (allowed.includes(to)) { + this.state = to; + this.history.push(to); + return true; + } + return false; + } + getState() { + return this.state; + } + getHistory() { + return this.history; + } +} +exports.StateMachine = StateMachine; +// Example FSM instance for dashboard visualiser. +const definition = { + init: ['ready'], + ready: ['running', 'error'], + running: ['ready', 'error'], + error: ['ready'], +}; +const machine = new StateMachine(definition, 'init'); +// Expose machine for debugging in admin screens. +window.nhkPocMachine = machine; diff --git a/assets/ts/fsm.ts b/assets/ts/fsm.ts new file mode 100644 index 0000000..38b486a --- /dev/null +++ b/assets/ts/fsm.ts @@ -0,0 +1,49 @@ +/** + * Shared FSM implementation in TypeScript. + * Mirrors PHP backend to maintain SSoT across contexts. + */ + +export interface MachineDefinition { + [state: string]: string[]; +} + +export class StateMachine { + private state: string; + private history: string[] = []; + + constructor(private transitions: MachineDefinition, initial: string) { + this.state = initial; + this.history.push(initial); + } + + public transition(to: string): boolean { + const allowed = this.transitions[this.state] || []; + if (allowed.includes(to)) { + this.state = to; + this.history.push(to); + return true; + } + return false; + } + + public getState(): string { + return this.state; + } + + public getHistory(): string[] { + return this.history; + } +} + +// Example FSM instance for dashboard visualiser. +const definition: MachineDefinition = { + init: ['ready'], + ready: ['running', 'error'], + running: ['ready', 'error'], + error: ['ready'], +}; + +const machine = new StateMachine(definition, 'init'); + +// Expose machine for debugging in admin screens. +(window as any).nhkPocMachine = machine; diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..e2b96db --- /dev/null +++ b/changelog.md @@ -0,0 +1,4 @@ +# Changelog + +## 1.0.0 +- Initial proof-of-concept release with FSM-based architecture. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..48d3ed0 --- /dev/null +++ b/composer.json @@ -0,0 +1,12 @@ +{ + "name": "nhk/poc", + "description": "NHK Proof of Concept WordPress plugin framework", + "type": "wordpress-plugin", + "license": "GPL-2.0-or-later", + "autoload": { + "psr-4": { + "NHK\\Poc\\": "src/" + } + }, + "require": {} +} diff --git a/nhk-poc.php b/nhk-poc.php new file mode 100644 index 0000000..429f267 --- /dev/null +++ b/nhk-poc.php @@ -0,0 +1,37 @@ +boot(); +} ); + +// Add settings link on Plugins listing page. +add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), function ( $links ) { + $url = admin_url( 'admin.php?page=nhk-poc-settings' ); + $links[] = '' . esc_html__( 'Settings', 'nhk-poc' ) . ''; + return $links; +} ); diff --git a/package.json b/package.json new file mode 100644 index 0000000..46243bd --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "nhk-poc", + "version": "1.0.0", + "devDependencies": { + "typescript": "^5.0.0" + }, + "scripts": { + "build": "tsc assets/ts/fsm.ts --target ES2017 --module commonjs --outDir assets/dist" + } +} diff --git a/src/Admin/Menu.php b/src/Admin/Menu.php new file mode 100644 index 0000000..5ddc45a --- /dev/null +++ b/src/Admin/Menu.php @@ -0,0 +1,73 @@ +serialize() ); + } ); + } + + public static function render_dashboard(): void { + echo '

NHK PoC Dashboard

'; + } + + public static function render_settings(): void { + echo '

Settings

FSM-driven settings page.

'; + } + + public static function render_changelog(): void { + $file = NHK_POC_PATH . 'changelog.md'; + $content = file_exists( $file ) ? wp_kses_post( nl2br( file_get_contents( $file ) ) ) : __( 'No changelog available', 'nhk-poc' ); + echo '

Changelog

' . $content . '
'; + } +} diff --git a/src/Api/AjaxHandler.php b/src/Api/AjaxHandler.php new file mode 100644 index 0000000..7dacc18 --- /dev/null +++ b/src/Api/AjaxHandler.php @@ -0,0 +1,33 @@ +transition( $next ) ) { + throw new \Exception( 'Invalid transition to ' . $next ); + } + + wp_send_json_success( [ + 'state' => $sm->state(), + 'history' => $sm->history(), + ] ); + } catch ( \Throwable $e ) { + error_log( 'NHK PoC AJAX error: ' . $e->getMessage() ); + wp_send_json_error( [ + 'message' => $e->getMessage(), + 'history' => $sm->history(), + ] ); + } + } ); + } +} diff --git a/src/Core/FSM/StateMachine.php b/src/Core/FSM/StateMachine.php new file mode 100644 index 0000000..548e60b --- /dev/null +++ b/src/Core/FSM/StateMachine.php @@ -0,0 +1,61 @@ +> */ + private $transitions; + + /** @var string */ + private $state; + + /** @var array */ + private $history = []; + + public function __construct( array $transitions, string $initial ) { + $this->transitions = $transitions; + $this->state = $initial; + $this->history[] = $initial; + } + + /** + * Move to a new state if the transition is allowed. + */ + public function transition( string $to ): bool { + $allowed = $this->transitions[ $this->state ] ?? []; + if ( in_array( $to, $allowed, true ) ) { + $this->state = $to; + $this->history[] = $to; + return true; + } + return false; + } + + /** + * Current state getter. + */ + public function state(): string { + return $this->state; + } + + /** + * Returns the transition history for debugging and traceability. + */ + public function history(): array { + return $this->history; + } + + /** + * Serialized representation for passing to the frontend. + */ + public function serialize(): array { + return [ + 'state' => $this->state, + 'history' => $this->history, + 'transitions' => $this->transitions, + ]; + } +} diff --git a/src/Core/Plugin.php b/src/Core/Plugin.php new file mode 100644 index 0000000..4dd4dff --- /dev/null +++ b/src/Core/Plugin.php @@ -0,0 +1,70 @@ + ['ready'], + 'ready' => ['running', 'error'], + 'running' => ['ready', 'error'], + 'error' => ['ready'], + ]; + $this->sm = new StateMachine( $definition, 'init' ); + } + + /** + * Singleton accessor. + */ + public static function get_instance(): self { + if ( null === self::$instance ) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * Boot plugin features. + */ + public function boot(): void { + // Transition from init -> ready once plugin loads. + $this->sm->transition( 'ready' ); + + // Register admin UI & AJAX handlers through FSM-aware classes. + Menu::init( $this->sm ); + AjaxHandler::init( $this->sm ); + + // Optional on-screen debug panel in admin when query arg present. + if ( isset( $_GET['nhk_poc_debug'] ) ) { + add_action( 'admin_notices', [ $this, 'debug_panel' ] ); + } + } + + /** + * Expose current state machine. + */ + public function sm(): StateMachine { + return $this->sm; + } + + /** + * Render minimal debug info showing state history. + */ + public function debug_panel(): void { + echo '

FSM History: ' . esc_html( implode( ' → ', $this->sm->history() ) ) . '

'; + } +}