PMS Workflow Engine
Architecture Diagnosis Report
Generated: {{ $generatedAt }}
Source: codebase audit (facts from source only)
Project: {{ $projectRoot }}
Executive summary
The PMS does not have one workflow engine. At least six parallel mechanisms can touch the same
pc_forms row: forward workflow, legacy counter workflow, meeting workspace overlay,
draft/revision lifecycle, admin side-channels (bypass/recovery/rescue/reconcile), and PCN forward (separate entity).
For PC-I/PC-II, PcForm::usesForwardWorkflow() infers forward mode from kind + status + state
and does not require the uses_forward_workflow column — imported PCFMS rows can behave as forward PCs
while missing workflow_stage and current_actor_id.
Table of contents
- Workflow engines
- Complete state machine
- Who owns a PC
- Permission engine
- Imported PC workflow
- Draft system
- Meeting workflow
- Revision system
- Duplicate logic
- Production risks
- Root causes
- Recommendations & risk assessment
1. Workflow engines
1.1 Forward workflow (PC-I / PC-II primary)
| Entry | Submit → PcFormService::finalizePcFormForwardSubmit() → PcFormWorkflowService::finalizeSubmitAndForward() |
| Controller | PcFormsController: forward, decline, forwardRecipients, declineRecipients, workflowActions |
| Services | PcFormWorkflowService, PcFormForwardActorResolver, PcFormWorkflowRecipientResolver, PcFormWorkflowStageResolver |
| Tables | pc_forms, pc_form_forwards |
| Enter | initializeOnSubmit(): workflow_stage=ad_review, current_actor_id=submitter, uses_forward_workflow=true |
| Exit | workflow_stage=approved (AA auth / bypass) or state=rejected; meeting-linked locks pre-meeting actions |
| Actor | current_actor_id must match auth()->id() unless super-admin bypass |
1.2 Legacy counter workflow (PC-III+ default; post-meeting)
| Entry | Submit without forward init; MeetingWorkspaceMeetingController::approveLinkedPcForm → PcFormService::approve() |
| Tables | pc_forms, pc_form_reviews |
| Progression | state + check_count, approve_count, department_* counters |
| Actor | No current_actor_id — permission + scheme scope |
| Blocked when | usesForwardWorkflow() true without meeting context (approve() throws) |
1.3 Meeting workflow (overlay)
| Entry | MeetingWorkspaceMeetingService::syncSubjectLinks() |
| Effect | markMeetingLinked(): workflow_stage=meeting_linked, current_allowed_actions=null; does NOT change current_actor_id |
| Post-meeting | pc_form_reviews + PcFormService::approve() / forum decisions / AA auth |
1.4 Co-signatory workflow (MoM)
MwsSchemeMomCoSignatoryWorkflowService — gates meeting approval via MwsSchemeMomService::momSignatureGateState().
1.5 PCFMS import (partial reconstruction)
| Command | pc:migrate-pcfms → PcfmsPcMigrationService |
| Sets | state (PC1StatusID), status, is_revised, parent_id, counters=1 |
| Does NOT set | workflow_stage, current_actor_id, uses_forward_workflow, current_allowed_actions |
1.6 Admin side-channels
| Workflow Bypass | WorkflowBypassService — skip ladder to approved |
| Recovery Center | WorkflowRecoveryApplyService — manual stage/actor/meeting |
| Rescue Center | WorkflowRecoveryService — validated repairs + rollback |
| CLI | pc-forms:reconcile-actors, pc-forms:assign-whitelisted-actor |
1.7 Multiple engines on same PC?
Yes. Forward + legacy state sync, forward + meeting handoff, import gap + inferred forward, revision + forward, admin recovery + live workflow can all apply to one row.
2. Complete state machine
2.1 Legacy state (pc_forms.state)
| Value | Constant | Notes |
| draft | STATE_DRAFT | Unsubmitted |
| requested | STATE_REQUESTED | Checker / AD review sync |
| checking | STATE_CHECKING | Deprecated — not written at runtime |
| department_approval_pending | STATE_DEPARTMENT_APPROVAL_PENDING | Legacy dept / DDWP sync |
| under approval | STATE_APPROVING | P&D / meeting-linked sync |
| approved | STATE_APPROVED | Terminal |
| rejected | STATE_REJECTED | Terminal (UI: Deferred) |
2.2 Forward workflow_stage
| Stage | Terminal | Legacy sync |
| ad_draft | No | unchanged |
| ad_review | No | requested |
| ad_approve | No | requested |
| ddwp_department_handoff | No | department_approval_pending |
| pnd_development_section | No | under approval |
| pnd_internal_circulation | No | under approval |
| meeting_linked | No | under approval |
| approved | Yes | approved |
| declined | Yes | rejected (not set by main decline()) |
2.3 Forward flow (ASCII)
[draft] --submit--> ad_review (actor=creator)
ad_review --forward--> ad_review | ad_approve | ddwp_department_handoff
ad_approve --forward--> pnd_development_section | pnd_internal_circulation
pnd_* --forward--> pnd_* (circulation)
any in-progress --decline--> upstream stage (creator → ad_review)
any in-progress --link meeting--> meeting_linked (actions locked)
meeting_linked --AA auth / bypass--> approved (terminal)
Methods: PcFormWorkflowService::initializeOnSubmit, forward, decline, markMeetingLinked
DB: pc_forms + pc_form_forwards on each transition
2.4 Legacy counter flow (ASCII)
draft --submit--> requested
requested --checker approve (×N)--> department_approval_pending | under approval
department_approval_pending --dept approve (×N)--> under approval
under approval --approver approve (×N)--> approved
any in-flight --reject--> rejected
Methods: PcFormService::approve(), reject(); PcFormApprovalCountService
3. Who owns a PC
| Field | Authoritative for |
current_actor_id | Forward/decline/edit (forward path) — primary runtime owner |
created_by | Creator identity; return-to-creator routing; draft edit |
updated_by | Last editor (audit) |
| pc_form_forwards trail | Decline recipient resolution; visibility |
| ACL permissions | Legacy approve/reject when not forward-held |
status | Active vs superseded revision; gates usesForwardWorkflow() for PC-I/II |
Known routing defect: Return-to-creator requires created_by === recipient in PcFormForwardActorResolver.
Imported PCs with created_by NULL break intended return-to-submitter routing.
4. Permission engine
No Laravel Policy for PcForm. Authorization is service-layer + can() ACL (config/permission.php, app/helpers.php).
| Method | Location | Logic |
| userMayEditMainForm | PcFormService | can edit + forward: currentUserHoldsFormForEdit |
| currentUserMayAct | PcFormWorkflowService | Forward only; actor match OR super-admin bypass |
| currentUserMayReturn | PcFormWorkflowService | May act AND actor ≠ created_by |
| currentUserMayForward | PcFormService | Delegates to currentUserMayAct |
| currentUserMayApprove | PcFormService | Legacy counter + meeting variants |
| canBypassPermissions | helpers.php | User::isSuperAdmin() |
Duplicate paths: Route ACL + EnsurePermissionForRoute actor override + controller re-check + service actor check + UI modal canReviewCurrentCycle().
5. Imported PC workflow
PCFMS SQL → PcfmsPcMigrationService → pc_forms + pcfms_import_maps
SET: state, status, is_revised, parent_id, counters=1
NOT SET: workflow_stage, current_actor_id, uses_forward_workflow
↓
reconcile-actors / assign-whitelisted-actor → bootstrap forwards + actor + stage
↓
PcFormWorkflowService forward/decline → meeting link → legacy approve post-meeting
Lost on import: Full PCFMS approval ladder, per-step actors, workflow_stage history.
Reconstructed: Coarse state, revision chain, hardcoded counters.
6. Draft system
| Action | Result |
| Save draft | state=draft on pc_forms; optional pc_form_drafts snapshot |
| Submit | state=requested; forward init for PC-I/II |
| Revision resubmit | New row status=1; old row status=0 |
| Return | PcFormWorkflowService::decline() — upstream actor |
| Post-approval workspace | post_approval_revision_workspace=true, status=false |
Drafts can disappear via: resubmit deactivation, workspace purge, delete(), listing filters on status=0.
7. Meeting workflow
| Step | Service / effect |
| Link | MeetingWorkspaceMeetingService::syncSubjectLinks → MwsMeetingSubjectLink |
| Stage lock | PcFormWorkflowService::markMeetingLinked |
| MoM gate | MwsSchemeMomService::momSignatureGateState |
| Approve | MeetingWorkspaceMeetingController::approveLinkedPcForm → PcFormService::approve or forum |
| Circulation | MwsPrePdwpMomCirculationService |
Meeting pauses forward (hasLinkedMeeting blocks currentUserMayAct) and hands off to legacy review/approve.
8. Revision system
| Field | Meaning |
| parent_id | Points to root PC (flat chain) |
| is_revised | Enables "Revised (version N)" badge |
| status | 1=active live PC; 0=closed/superseded |
| Version N | 1-based index by id in chain (attachRevisionChainPositions) |
Health rules: at least one active revision; at most one status=1 per chain.
9. Duplicate logic
- Actor bootstrap: PcFormActorReconciliationService, AssignWhitelistedPcFormActorCommand, PcFormLegacyWorkflowReconciliationService
- Parallel PC/PCN stacks: PcFormWorkflow* vs PcnWorkflow*
- uses_forward_workflow column vs inferred forward for PC-I/II
- Recovery writers: WorkflowRecoveryApplyService vs WorkflowRecoveryService vs CLI
- Triple permission checks: middleware + controller + service
10. Production risks (localhost vs production)
| Risk | Why prod fails, local may not |
| Dual DB (pc + core) | Missing scheme/project data on prod |
| Schema conditionals | Migrations not run on prod |
| Inferred forward without columns | Import + no reconcile |
| Lazy reconcile on view | First viewer mutates prod state |
| CLI reconciliation not run | Dev reconciled, prod not |
| Sync notifications | Prod mail/DB slower; timeouts |
| Storage / signatures | storage:link, permissions |
| Config/opcache cache | Stale permission.php |
11. Root causes (engine-level)
| Engine | Problem |
| A — Dual workflow model | state + workflow_stage run together; post-meeting drops to legacy approve |
| B — Import gap | Forward inferred but stage/actor/trail missing |
| C — Ownership split | current_actor_id vs permissions vs created_by null |
| D — Side-channel mutation | Reconcile, bypass, recovery, rescue all write workflow fields |
| E — Revision semantics | status, state, is_revised independent — confusing listings |
| F — Permission layering | Route ACL vs actor vs service disagree |
12. Recommendations & risk assessment
Immediate fixes
- Mandatory post-import reconcile before forward actions
- Initialize created_by on all imported active PCs
- Close inactive revisions with state=approved (not requested)
- Document which engine owns each PC (kind + usesForwardWorkflow + meeting + import map)
Medium-term
- Single ownership model for forward path
- Align usesForwardWorkflow() with uses_forward_workflow column
- Consolidate admin recovery into one service
- Single permission gate per action
Long-term redesign
- Explicit workflow enum + transition table (no dual state sync)
- Event-sourced workflow log with compensating admin events
- Import snapshot + explicit "activate workflow" step
- Separate read models for listing vs write model
Risk assessment
| Area | Severity | Likelihood |
| Imported PC forward without actor | Critical | High |
| Dual state columns diverge | High | Medium |
| Admin side-channel overwrite | High | Medium |
| created_by null routing | High | High (imports) |
| Multiple active revisions | Medium | Medium |
| Permission path mismatch | Medium | Medium |