PMS Complete Workflow Map

Architecture & Relationship Report

Generated: {{ $generatedAt }}
Source: codebase audit (facts from source only)
Project: {{ $projectRoot }}
Executive summary
PMS does not have one workflow engine. Multiple parallel mechanisms touch the same pc_forms row: Draft, Forward, Legacy counter, Meeting (MWS), MoM co-signatory, Approval surface, Revision, Recovery, and PCFMS Import. Runtime authority is determined by PcForm::usesForwardWorkflow() plus meeting-link state — not by a single pipeline.

Table of contents

  1. Master relationship diagram
  2. Engine selection (runtime router)
  3. Per-workflow map (0–9)
  4. Coexistence on one row
  5. Conflict & bypass patterns
  6. Determination answers
  7. Merge recommendations
  8. Interaction matrix

1. Master relationship diagram

Conceptual stack (workflows are parallel engines, not a strict pipeline):

                    PC (entity: pc_forms / PcForm)
                              |
         +--------------------+--------------------+
         |                    |                    |
      DRAFT              IMPORT (PCFMS)      REVISION flags
         |                    |                    |
         v                    v                    |
    submit PC-I/II         lands row           begin workspace
         |               (incomplete)               |
         v                    |                    v
   FORWARD WORKFLOW <---------+---------------- submit
   (current_actor_id,         |
    workflow_stage,           |
    pc_form_forwards)         |
         |                    |
         | markMeetingLinked   |
         v                    |
   MEETING WORKFLOW (MWS)     |
         |                    |
         v                    |
   CO-SIGNATORY (MoM chain)   |
         | MoM complete        |
         v                    |
   APPROVAL SURFACE ----------+---- LEGACY COUNTER (state + counts + pc_form_reviews)
   (approve/reject)                syncLegacyState mirrors state
         |
         v
   APPROVED -> REVISION WORKFLOW (new row / chain)
         |
   RECOVERY (admin side-channel patches columns anytime)

2. Engine selection (authoritative runtime router)

PcForm loaded
    |
    +-- PC-I / PC-II? --NO--> uses_forward_workflow column? --YES--> Forward engine
    |                              |
    |                             NO--> Legacy engine
    |
    +-- YES: status=true AND state NOT IN (draft, rejected, approved)?
              |
             YES --> Forward engine (PcFormWorkflowService)
              |
             NO  --> Legacy engine (PcFormApprovalCountService)

Forward engine + hasLinkedMeeting() OR stage=meeting_linked
    --> forward/decline BLOCKED
    --> approve() allowed WITH meeting context (mws_meeting_id)

Forward engine + approve() WITHOUT meeting
    --> Exception: "Use Forward" (PcFormService:11693-11697)

Proven selector: PcForm::usesForwardWorkflow() — app/Models/PC/PcForm.php:496-508

3. Per-workflow map

0. PC (entity — not an engine)

EntryPOST pc-forms → PcFormsController::store() → PcFormService::create()
ExitTerminal state=approved/rejected; inactive status=0
ServicesPcFormService
Tablespc_forms + child section tables
ControllersPcFormsController
ModelsPcForm
Ownershipcreated_by; forward adds current_actor_id; legacy uses permission pools
Permissionspc-forms → form key "pc 1" (config/permission.php)

1. Draft Workflow

EntryGET pc-forms/create; POST draft-autosave; EAED DraftController API
ExitSubmit (save_as_draft=false) → PcForm state=requested or forward init
ServicesPcFormService, EaedDraftService
Tablespc_forms_drafts, pc_form_draft_*, pc_forms (state=draft)
ControllersPcFormsController, DraftController
ModelsPcFormDraft, PcForm
Ownershipcreated_by; DataVisibilityHelper for delete
Permissionspc-forms add/edit on autosave routes
InteractionsPC-I/II submit → finalizePcFormForwardSubmit(); revision workspace uses post_approval_revision_workspace

2. Forward Workflow

EntryinitializeOnSubmit(); POST forward/decline; GET pc-forms/{id}/show (lazy reconcile); CLI pc-forms:reconcile-actors
ExitTerminal workflow_stage=approved/declined; markMeetingLinked() → meeting engine
ServicesPcFormWorkflowService, PcFormForwardActorResolver, PcFormWorkflowRecipientResolver, PcFormWorkflowStageResolver, PcFormActorReconciliationService
Tablespc_forms (workflow_stage, current_actor_id, uses_forward_workflow); pc_form_forwards
ControllersPcFormsController: forward, decline, forwardRecipients, workflowActions
ModelsPcForm, PcFormForward
Ownershipcurrent_actor_id; currentUserMayAct() requires actor match (PcFormWorkflowService:394-421)
PermissionsEnsurePermissionForRoute actor check + can(pc-forms,view); PcFormWorkflowAction buckets
InteractionssyncLegacyState() mirrors state; blocks approve() without meeting; post-meeting uses legacy approve

3. Legacy Counter Workflow

EntrySubmit sets counters; POST approve/reject; PcFormService::show() auto-advance; CLI pc:sync-workflow-counts
Exitstate=approved when approve_count >= required_approvals; state=rejected on reject
ServicesPcFormApprovalCountService, PcFormService::approve/reject
Tablespc_forms (counts, state); pc_form_reviews; pc_form_department_approvals
ControllersPcFormsController; MeetingWorkspaceMeetingController::approveLinkedPcForm
ModelsPcForm, PcFormReview
OwnershipPermission pool by stage (checker → dept → approver); no current_actor_id
Permissionscan_department_approve, can_approve (service-layer; routes not in permission map)
InteractionsSkipped when usesForwardWorkflow(); forward reject → decline()

4. Meeting Workflow (MWS)

Entrymeeting-workspace/meetings/* → MeetingWorkspaceMeetingController; markMeetingLinked()
ExitMeeting Completed/Cancelled; PC decision via approve/defer
ServicesMeetingWorkspaceMeetingService, MwsPcFormMeetingDecisionService, MwsAdviseLetterService
Tablesmws_meetings, mws_meeting_subject_links, mws_participants, pc_form_reviews (mws_meeting_id)
ControllersMeetingWorkspaceMeetingController, MeetingWorkspacePlannerController
ModelsMwsMeeting, MwsMeetingSubjectLink, MwsMeetingSchemeMom
OwnershipMwsMeeting::userMayAccessMeeting(); decision gates in MwsPcFormMeetingDecisionService
Permissionsmeeting_workspace list/view/add/edit; MoM flags
InteractionsLocks pre-meeting forward; approve in meeting calls PcFormService::approve() with meeting flags

5. Co-signatory Workflow (MoM chain)

EntryPOST .../mom (storeMom); POST .../mom/co-signer-sign
Exitmom_co_signatory_status=complete or returned
ServicesMwsSchemeMomCoSignatoryWorkflowService, MwsSchemeMomService
Tablesmws_meeting_scheme_moms, mws_meeting_scheme_mom_actions
ControllersMeetingWorkspaceMeetingController::storeMom, signMomCoSignatory
ModelsMwsMeetingSchemeMom
Ownershippending_user_id in JSON; momUserIsActiveCoSignerTurn()
Permissionsmeeting_workspace edit/mom flags; co-sign route uses runtime turn checks
InteractionsBlocks PC meeting approve until MoM signatures complete; separate from pc_form_forwards

6. Approval Workflow (action surface, not separate engine)

EntryPOST pc-forms/approve/reject; meeting linked-pc-form/approve; WorkflowBypassService; PcFormAaAuthLetterService
Exitstate=approved + BpmsWebhookService::notifyAfterPcApproval()
ServicesPcFormService::approve/reject, MwsPcFormMeetingDecisionService, WorkflowBypassService
Tablespc_forms, pc_form_reviews
ControllersPcFormsController, MeetingWorkspaceMeetingController
ModelsPcForm, PcFormReview
OwnershipLegacy pools; forward actor until meeting; bypass sets terminal directly
Permissionscan_department_approve, can_approve; meeting_workspace view + service gates
InteractionsForward: approve throws without meeting; forward reject → decline()

7. Revision Workflow

EntryGET begin-approved-revision; shouldCreateRevisionOnSubmit() on resubmit
ExitSubmit creates new active row; workspace shell purged
ServicesPcFormService::spawnPostApprovalRevisionWorkspace, createPcFormRevisionFromSubmit
Tablespc_forms: is_revised, parent_id, post_approval_revision_workspace, status
ControllersPcFormsController::beginApprovedRevision
ModelsPcForm
OwnershipWorkspace: created_by + can(edit); one active row per chain (status=1)
Permissionscan(add) to begin; can(edit) to submit workspace
InteractionsInactive revisions excluded from forward; import sets is_revised/parent_id

8. Recovery Workflow (two admin modules)

Entryadmin-controls/workflow-recovery/* (Recovery Center); admin-controls/workflow-rescue/* (Rescue Center)
Exitworkflow_recovery_logs / workflow_rescue_logs; rollback restores snapshot fields
ServicesWorkflowRecoveryApplyService (write, NO syncLegacyState); WorkflowRecoveryService (Rescue, some actions sync)
Tablesworkflow_recovery_logs, workflow_rescue_logs, pc_forms, mws_meeting_subject_links
ControllersWorkflowRecoveryController, WorkflowRescueController
ModelsWorkflowRecoveryLog, WorkflowRescueLog
OwnershipPatches workflow_stage, current_actor_id, current_allowed_actions, uses_forward_workflow
Permissionslegacy_pc_recovery / workflow_rescue + Super Admin
InteractionsRecovery Center skips syncLegacyState (proven drift); Rescue safety gate blocks overwrite with forward history

9. Import Workflow (PCFMS)

EntryCLI pc:migrate-pcfms → PcfmsPcMigrationService::migrate()
Exitpcfms_import_maps row; stats returned
ServicesPcfmsPcMigrationService, PcfmsSchemeResolver
Tablespc_forms, pcfms_import_maps
ControllersNone (CLI only)
ModelsPcForm, PcfmsImportMap
OwnershipDoes NOT set current_actor_id, created_by, workflow_stage, uses_forward_workflow
PermissionsNone (CLI)
InteractionsSets state, status, counters=1, is_revised, parent_id; no pc_form_reviews or pc_form_forwards

4. Coexistence on one pc_forms row

Single pc_forms row simultaneously holds:

  state ...................... Legacy engine (PcFormApprovalCountService)
  check_count / approve_count .. Legacy progress
  workflow_stage ............... Forward engine (PcFormWorkflowService)
  current_actor_id ............. Forward owner
  status / is_revised / parent_id ... Revision chain
  post_approval_revision_workspace .. Revision prep shell
  meeting_linked_at ............ Meeting lock

Forward + Legacy BOTH active for PC-I/II in forward mode:
  Forward owns actor actions; syncLegacyState() mirrors state on each forward step.

MoM co-sign owns mws_meeting_scheme_moms (separate table) but gates PC meeting approve.

5. syncLegacyState mapping (proven)

workflow_stageSynced state
approvedapproved
declinedrejected
ad_review, ad_approverequested
pnd_*, meeting_linkedunder approval
ddwp_department_handoffdepartment_approval_pending

Source: PcFormWorkflowStageResolver::syncLegacyState() — app/Services/PC/PcForm/PcFormWorkflowStageResolver.php:67-82

6. Proven conflicts

ConflictEvidence
state vs workflow_stage driftWorkflowRecoveryApplyService does not call syncLegacyState (comment line 23)
Import gapstate=requested but no current_actor_id / no pc_form_forwards
Inactive revisionsstatus=0 still may show state=requested in DB; usesForwardWorkflow() returns false
Lazy reconcile path gapensureForwardActorAssigned only on GET pc-forms/show; not on forward/edit
Approve vs forwardapprove() throws on forward forms without meeting (PcFormService:11693-11697)

7. Proven bypasses

8. Determination answers

Can more than one workflow own a PC simultaneously?

YES — proven.

One row carries legacy state+counters, forward stage+actor, revision flags, and meeting lock concurrently. Forward mirrors legacy state via syncLegacyState().

Can workflows conflict?

YES — proven.

Recovery Center skips syncLegacyState; import leaves incomplete forward fields; reconcile runs on only one endpoint.

Can one workflow bypass another?

YES — proven.

Admin Recovery/Rescue/Bypass, lazy reconcile, meeting handoff, and canBypassPermissions() all bypass normal engine paths.

Which workflow is authoritative?

ConditionAuthoritative engine
usesForwardWorkflow() = falseLegacy counter (state + pc_form_reviews)
usesForwardWorkflow() = true, no meeting linkForward (current_actor_id + pc_form_forwards)
Meeting linkedMeeting + legacy approve (PcFormService::approve with mws context)
Admin Recovery/Rescue appliedAdmin write wins on patched columns (may drift from both engines)

Which workflows should be merged? (code-evidence only)

CandidateReason
Forward + LegacyAlready partially merged via syncLegacyState; dual progression models remain
Recovery Center + Rescue CenterBoth write same columns; Recovery skips syncLegacyState, Rescue does not
Import + Actor reconcileImport never sets forward fields; three separate fix paths exist
Lazy reconcile + CLI reconcileSame logic, different triggers (modal view vs artisan command)
Do NOT mergeMoM co-sign (separate table); Draft (pc_forms_drafts); Revision chain semantics

9. Interaction matrix

From → ToTriggerService
Draft → ForwardPC-I/II submitfinalizePcFormForwardSubmit()
Draft → LegacyPC-III+ submitPcFormService::create counter defaults
Forward → Legacy stateevery forward/declinesyncLegacyState()
Forward → Meetingmeeting linkedmarkMeetingLinked()
Meeting → Approvallinked PC approvePcFormService::approve()
Co-sign → ApprovalMoM incompleteblocks approveLinkedPcForm
Forward → Approvaldirect approvethrows unless meeting context
Legacy reject → Forwardforward form rejectPcFormWorkflowService::decline()
Revision → Forward/Legacyworkspace submitnew row normal submit
Import → gapno actor/stagerequires reconcile or Rescue
Recovery → Forward fieldsadmin saveWorkflowRecoveryApplyService::apply()

10. Key file index

WorkflowPrimary services
PC entityPcFormService
DraftPcFormService, EaedDraftService
ForwardPcFormWorkflowService, PcFormActorReconciliationService
LegacyPcFormApprovalCountService, PcFormService
MeetingMeetingWorkspaceMeetingService, MwsPcFormMeetingDecisionService
Co-signatoryMwsSchemeMomCoSignatoryWorkflowService
Approval surfacePcFormService::approve/reject
RevisionPcFormService revision methods
RecoveryWorkflowRecoveryApplyService, WorkflowRecoveryService
ImportPcfmsPcMigrationService
Admin bypassWorkflowBypassService

Parallel stack: PCN forward workflow on project_concept_notes (PcnWorkflowService, pcn_forwards) — separate entity, same pattern.