@rytass/bpm-core-nestjs-module 0.20.1 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +109 -5
  3. package/package.json +2 -2
  4. package/src/lib/attachment/attachment.service.js +2 -10
  5. package/src/lib/attachment/attachment.service.js.map +1 -1
  6. package/src/lib/attachment/file-upload-value-ids.d.ts +8 -0
  7. package/src/lib/attachment/file-upload-value-ids.js +15 -0
  8. package/src/lib/attachment/file-upload-value-ids.js.map +1 -0
  9. package/src/lib/bpm/bpm-root-options.d.ts +18 -0
  10. package/src/lib/bpm/bpm-root-options.js.map +1 -1
  11. package/src/lib/bpm/bpm-root.module.d.ts +11 -0
  12. package/src/lib/bpm/bpm-root.module.js +9 -4
  13. package/src/lib/bpm/bpm-root.module.js.map +1 -1
  14. package/src/lib/form/form-data-issue.d.ts +34 -0
  15. package/src/lib/form/form-data-issue.js +59 -0
  16. package/src/lib/form/form-data-issue.js.map +1 -0
  17. package/src/lib/form/form-member-value.validator.d.ts +2 -2
  18. package/src/lib/form/form-member-value.validator.js +2 -3
  19. package/src/lib/form/form-member-value.validator.js.map +1 -1
  20. package/src/lib/form/form-org-unit-value.validator.d.ts +2 -2
  21. package/src/lib/form/form-org-unit-value.validator.js +4 -1
  22. package/src/lib/form/form-org-unit-value.validator.js.map +1 -1
  23. package/src/lib/form/form-schema.validator.js +40 -0
  24. package/src/lib/form/form-schema.validator.js.map +1 -1
  25. package/src/lib/form-data-source/form-data-source.module.d.ts +8 -0
  26. package/src/lib/form-data-source/form-data-source.module.js +1 -1
  27. package/src/lib/form-data-source/form-data-source.module.js.map +1 -1
  28. package/src/lib/form-data-source/form-data-source.service.js +1 -1
  29. package/src/lib/form-data-source/form-data-source.service.js.map +1 -1
  30. package/src/lib/migrations/0000000031000-approval-instance-form-data-gin.d.ts +29 -0
  31. package/src/lib/migrations/0000000031000-approval-instance-form-data-gin.js +44 -0
  32. package/src/lib/migrations/0000000031000-approval-instance-form-data-gin.js.map +1 -0
  33. package/src/lib/migrations/index.d.ts +1 -0
  34. package/src/lib/migrations/index.js +3 -0
  35. package/src/lib/migrations/index.js.map +1 -1
  36. package/src/lib/notification/notification.service.js +6 -0
  37. package/src/lib/notification/notification.service.js.map +1 -1
  38. package/src/lib/organization/organization-time-zone.d.ts +4 -2
  39. package/src/lib/organization/organization-time-zone.js +3 -6
  40. package/src/lib/organization/organization-time-zone.js.map +1 -1
  41. package/src/lib/template/filterable-field-guard.d.ts +49 -0
  42. package/src/lib/template/filterable-field-guard.js +141 -0
  43. package/src/lib/template/filterable-field-guard.js.map +1 -0
  44. package/src/lib/template/filterable-field-issue.object.d.ts +21 -0
  45. package/src/lib/template/filterable-field-issue.object.js +65 -0
  46. package/src/lib/template/filterable-field-issue.object.js.map +1 -0
  47. package/src/lib/template/filterable-field.queries.d.ts +20 -0
  48. package/src/lib/template/filterable-field.queries.js +80 -0
  49. package/src/lib/template/filterable-field.queries.js.map +1 -0
  50. package/src/lib/template/index.d.ts +1 -0
  51. package/src/lib/template/index.js +1 -0
  52. package/src/lib/template/index.js.map +1 -1
  53. package/src/lib/template/template.module.js +2 -0
  54. package/src/lib/template/template.module.js.map +1 -1
  55. package/src/lib/template/template.service.d.ts +39 -0
  56. package/src/lib/template/template.service.js +218 -49
  57. package/src/lib/template/template.service.js.map +1 -1
  58. package/src/lib/template/workflow-definition.validator.js +84 -2
  59. package/src/lib/template/workflow-definition.validator.js.map +1 -1
  60. package/src/lib/workflow-engine/approval-instance-field-filter.compiler.d.ts +49 -0
  61. package/src/lib/workflow-engine/approval-instance-field-filter.compiler.js +501 -0
  62. package/src/lib/workflow-engine/approval-instance-field-filter.compiler.js.map +1 -0
  63. package/src/lib/workflow-engine/approval-instance-filterable-field.policy.d.ts +15 -0
  64. package/src/lib/workflow-engine/approval-instance-filterable-field.policy.js +20 -0
  65. package/src/lib/workflow-engine/approval-instance-filterable-field.policy.js.map +1 -0
  66. package/src/lib/workflow-engine/dto/approval-instance-field-filter.input.d.ts +15 -0
  67. package/src/lib/workflow-engine/dto/approval-instance-field-filter.input.js +55 -0
  68. package/src/lib/workflow-engine/dto/approval-instance-field-filter.input.js.map +1 -0
  69. package/src/lib/workflow-engine/dto/approval-instance-list-args.d.ts +9 -0
  70. package/src/lib/workflow-engine/dto/approval-instance-list-args.js +24 -0
  71. package/src/lib/workflow-engine/dto/approval-instance-list-args.js.map +1 -0
  72. package/src/lib/workflow-engine/dto/approval-instance-sort.input.d.ts +10 -0
  73. package/src/lib/workflow-engine/dto/approval-instance-sort.input.js +32 -0
  74. package/src/lib/workflow-engine/dto/approval-instance-sort.input.js.map +1 -0
  75. package/src/lib/workflow-engine/index.d.ts +1 -0
  76. package/src/lib/workflow-engine/index.js +1 -0
  77. package/src/lib/workflow-engine/index.js.map +1 -1
  78. package/src/lib/workflow-engine/initiator-custom-fields-resolver.token.d.ts +108 -0
  79. package/src/lib/workflow-engine/initiator-custom-fields-resolver.token.js +9 -0
  80. package/src/lib/workflow-engine/initiator-custom-fields-resolver.token.js.map +1 -0
  81. package/src/lib/workflow-engine/workflow-engine.enums.d.ts +13 -0
  82. package/src/lib/workflow-engine/workflow-engine.enums.js +22 -1
  83. package/src/lib/workflow-engine/workflow-engine.enums.js.map +1 -1
  84. package/src/lib/workflow-engine/workflow-engine.module.d.ts +2 -0
  85. package/src/lib/workflow-engine/workflow-engine.module.js +9 -3
  86. package/src/lib/workflow-engine/workflow-engine.module.js.map +1 -1
  87. package/src/lib/workflow-engine/workflow-engine.queries.d.ts +6 -4
  88. package/src/lib/workflow-engine/workflow-engine.queries.js +35 -12
  89. package/src/lib/workflow-engine/workflow-engine.queries.js.map +1 -1
  90. package/src/lib/workflow-engine/workflow-engine.service.d.ts +166 -18
  91. package/src/lib/workflow-engine/workflow-engine.service.js +918 -139
  92. package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
@@ -1,4 +1,4 @@
1
- import { MemberMetadata } from '@rytass/bpm-core-shared';
1
+ import { MemberMetadata, type ApprovalInstanceFieldFilter, type ApprovalInstanceSort } from '@rytass/bpm-core-shared';
2
2
  import { WorkflowWebhookDeliveryService } from '../workflow-webhook/workflow-webhook-delivery.service';
3
3
  import { Repository } from 'typeorm';
4
4
  import { AttachmentService } from '../attachment/attachment.service';
@@ -29,6 +29,7 @@ import { TaskCandidateEntity } from './task-candidate.entity';
29
29
  import { TaskEntity } from './task.entity';
30
30
  import { ApprovalInstanceListViewEnum, ApprovalInstanceStateEnum, MemberTaskRoleEnum, TaskCandidateStatusEnum, TaskReassignmentErrorCodeEnum, TaskReassignmentOutcomeEnum } from './workflow-engine.enums';
31
31
  import { WorkflowDryRunResultObject } from './workflow-dry-run.object';
32
+ import { BPMInitiatorCustomFieldsResolver } from './initiator-custom-fields-resolver.token';
32
33
  import { BPMWorkflowServiceTaskDispatcher } from './workflow-service-task-dispatcher.token';
33
34
  import { WorkflowTokenEntity } from './workflow-token.entity';
34
35
  import { BPMRootRuntimeOptions } from '../bpm/bpm-root-options';
@@ -140,10 +141,14 @@ export interface DecideTaskOptions {
140
141
  */
141
142
  readonly transferReason?: string;
142
143
  }
143
- interface ListApprovalInstancesOptions {
144
+ export interface ListApprovalInstancesOptions {
145
+ /** ADR 25 §7: conditions on form fields. Needs `templateId` when not empty. */
146
+ readonly fieldFilters?: readonly ApprovalInstanceFieldFilter[];
144
147
  readonly page?: number;
145
148
  readonly pageSize?: number;
146
149
  readonly searchText?: string;
150
+ /** ADR 25 §7.5: sort keys in order; the list always ends with `id`. */
151
+ readonly sort?: readonly ApprovalInstanceSort[];
147
152
  readonly state?: readonly ApprovalInstanceStateEnum[];
148
153
  readonly templateId?: string;
149
154
  readonly view?: ApprovalInstanceListViewEnum;
@@ -174,8 +179,11 @@ export declare class WorkflowEngineService {
174
179
  private readonly formDataSourceValueResolver?;
175
180
  private readonly webhookDeliveryService?;
176
181
  private readonly logger;
177
- constructor(approvalInstanceRepository: Repository<ApprovalInstanceEntity>, workflowTokenRepository: Repository<WorkflowTokenEntity>, taskRepository: Repository<TaskEntity>, taskCandidateRepository: Repository<TaskCandidateEntity>, taskDecisionRepository: Repository<TaskDecisionEntity>, notificationRepository: Repository<NotificationEntity>, activityLogRepository: Repository<ActivityLogEntity>, adhocDirectiveRepository: Repository<AdhocDirectiveEntity>, approvalTemplateRepository: Repository<ApprovalTemplateEntity>, approvalTemplateVersionRepository: Repository<ApprovalTemplateVersionEntity>, formDefinitionVersionRepository: Repository<FormDefinitionVersionEntity>, attachmentService: AttachmentService, conditionService: ConditionService, delegationService: DelegationService, notificationService: NotificationService, signatureService: SignatureService, slaScheduleService: BPMSlaScheduleService, injectedServiceTaskDispatcher?: BPMWorkflowServiceTaskDispatcher, formDataSourceValueResolver?: BPMFormDataSourceValueResolver | undefined, rootOptions?: BPMRootRuntimeOptions, webhookDeliveryService?: WorkflowWebhookDeliveryService | undefined);
182
+ private readonly approvalInstanceFieldFilterCompiler;
183
+ private readonly approvalInstanceFilterableFieldPolicy;
184
+ constructor(approvalInstanceRepository: Repository<ApprovalInstanceEntity>, workflowTokenRepository: Repository<WorkflowTokenEntity>, taskRepository: Repository<TaskEntity>, taskCandidateRepository: Repository<TaskCandidateEntity>, taskDecisionRepository: Repository<TaskDecisionEntity>, notificationRepository: Repository<NotificationEntity>, activityLogRepository: Repository<ActivityLogEntity>, adhocDirectiveRepository: Repository<AdhocDirectiveEntity>, approvalTemplateRepository: Repository<ApprovalTemplateEntity>, approvalTemplateVersionRepository: Repository<ApprovalTemplateVersionEntity>, formDefinitionVersionRepository: Repository<FormDefinitionVersionEntity>, attachmentService: AttachmentService, conditionService: ConditionService, delegationService: DelegationService, notificationService: NotificationService, signatureService: SignatureService, slaScheduleService: BPMSlaScheduleService, injectedServiceTaskDispatcher?: BPMWorkflowServiceTaskDispatcher, formDataSourceValueResolver?: BPMFormDataSourceValueResolver | undefined, rootOptions?: BPMRootRuntimeOptions, webhookDeliveryService?: WorkflowWebhookDeliveryService | undefined, injectedInitiatorCustomFieldsResolver?: BPMInitiatorCustomFieldsResolver);
178
185
  private readonly serviceTaskDispatcher;
186
+ private readonly initiatorCustomFieldsResolver;
179
187
  /** Webhooks each open engine transaction will send once it has committed. */
180
188
  private readonly deferredWebhookDispatches;
181
189
  private readonly organizationTimeZone;
@@ -310,8 +318,20 @@ export declare class WorkflowEngineService {
310
318
  */
311
319
  private readDryRunMemberIds;
312
320
  getApprovalInstance(id: string, scope?: WorkflowReadScope): Promise<ApprovalInstanceEntity>;
321
+ /**
322
+ * ADR 25 §6: a value written to a filterable field must fit its type, on
323
+ * every path that writes form data. The fields checked are the ones the
324
+ * template's *current* version marks filterable, not the instance's own
325
+ * snapshot: an older instance can be re-filled, resubmitted or written by a
326
+ * service task after a newer version enabled filtering on a field, and the
327
+ * enable check (ADR 25 §5.4) guarantees the type is the same in every version
328
+ * that holds instances, so the current definition judges the old value too.
329
+ */
330
+ private assertCurrentFilterableFormDataValues;
331
+ /** The form schema of the template's current version, or `null` when there is none. */
332
+ private readCurrentFormSchema;
313
333
  listApprovalInstances(scope?: WorkflowReadScope, options?: ListApprovalInstancesOptions): Promise<readonly ApprovalInstanceEntity[]>;
314
- countApprovalInstances(scope?: WorkflowReadScope, options?: Omit<ListApprovalInstancesOptions, 'page' | 'pageSize'>): Promise<number>;
334
+ countApprovalInstances(scope?: WorkflowReadScope, options?: Omit<ListApprovalInstancesOptions, 'page' | 'pageSize' | 'sort'>): Promise<number>;
315
335
  readApprovalInstancePageInfo(scope?: WorkflowReadScope, options?: ListApprovalInstancesOptions): Promise<{
316
336
  readonly hasNextPage: boolean;
317
337
  readonly hasPreviousPage: boolean;
@@ -320,7 +340,20 @@ export declare class WorkflowEngineService {
320
340
  readonly totalCount: number;
321
341
  readonly totalPages: number;
322
342
  }>;
323
- private createFilteredApprovalInstanceQueryBuilder;
343
+ private createFilteredApprovalInstanceQuery;
344
+ /**
345
+ * The WHERE conditions and sort keys of the field filters and sort on the
346
+ * approval-instance list (ADR 25 §7). The template's current schema is read,
347
+ * and the static option lists of every version holding instances, only when
348
+ * a condition or a non-time sort key needs them.
349
+ */
350
+ private compileApprovalInstanceFieldQuery;
351
+ /**
352
+ * Static option lists by field path, across the current schema and the form
353
+ * version of every template version that holds instances (ADR 25 §7.3): a
354
+ * CONTAINS on a retired option still matches its name.
355
+ */
356
+ private readStaticOptionsByPathForTemplate;
324
357
  private applyReadableInstanceFilter;
325
358
  readWorkflowDashboardSummary(scope?: WorkflowReadScope, options?: WorkflowDashboardSummaryOptions): Promise<{
326
359
  readonly activeInstanceCount: number;
@@ -331,7 +364,12 @@ export declare class WorkflowEngineService {
331
364
  readonly totalInstanceCount: number;
332
365
  }>;
333
366
  private readDashboardOpenTasks;
334
- listLaunchableApprovalTemplates(memberId: string): Promise<readonly ApprovalTemplateEntity[]>;
367
+ /**
368
+ * The active templates `memberId` may start now. `authContext` is handed to
369
+ * the host's initiator `customFields` resolver; a host calling this without
370
+ * one gets a bare context for the member, as a submission does.
371
+ */
372
+ listLaunchableApprovalTemplates(memberId: string, authContext?: BPMAuthContext): Promise<readonly ApprovalTemplateEntity[]>;
335
373
  listWorkflowTokens(instanceId: string, scope?: WorkflowReadScope): Promise<readonly WorkflowTokenEntity[]>;
336
374
  listTasks(instanceId: string, scope?: WorkflowReadScope): Promise<readonly TaskEntity[]>;
337
375
  listInboxTasks(assigneeMemberId: string): Promise<readonly TaskEntity[]>;
@@ -363,6 +401,54 @@ export declare class WorkflowEngineService {
363
401
  */
364
402
  private readTaskNodeSummaries;
365
403
  private readDefaultInitiatorMetadataSnapshot;
404
+ /**
405
+ * The initiator snapshot the engine builds itself: what it knows of the
406
+ * initiator, with `customFields` filled by the host's resolver when there is
407
+ * one. Without a resolver this is exactly the default snapshot
408
+ * (`customFields` is `{}`). A submission (`submission` set) stores it and
409
+ * judges the initiator policy with it; the launchable template list
410
+ * (`submission: null`) only judges the policies with it, so both see the same
411
+ * `customFields` for the same member.
412
+ *
413
+ * Only `customFields` comes from the host. The other keys are the engine's
414
+ * and are written back after the resolver returns, so nothing it does can
415
+ * change who the initiator is. The resolver gets copies of the snapshot and
416
+ * of the form data for the same reason; `authContext` is the caller's own
417
+ * object, as every other host hook receives it.
418
+ *
419
+ * The two callers treat a resolver failure (an error, a bad value, the
420
+ * timeout) differently, on purpose:
421
+ *
422
+ * - A submission fails, and no case is created. Its snapshot is stored and
423
+ * routes the case from then on, so carrying on with `{}` would start a
424
+ * case on data the host never gave. It runs before the engine
425
+ * transaction, so a failure leaves nothing behind and a slow lookup holds
426
+ * no lock.
427
+ * - The list carries on with `customFields: {}` and logs a warning: one
428
+ * failing lookup must not take the whole list away. That fails closed: a
429
+ * policy that reads `initiator.customFields` then throws on the missing key
430
+ * and the list's per-template catch leaves that template out, while a
431
+ * template whose policy does not read it is listed as usual. Submitting is
432
+ * still judged against the resolver's real answer.
433
+ */
434
+ private buildInitiatorMetadataSnapshot;
435
+ /**
436
+ * Calls the host's initiator `customFields` resolver within
437
+ * {@link INITIATOR_CUSTOM_FIELDS_RESOLVER_TIMEOUT_MS} and returns what it
438
+ * gave as the object the snapshot keeps.
439
+ *
440
+ * Every failure becomes an `InternalServerErrorException` with a message
441
+ * meant for the member, and the cause goes to the log only, so nothing of
442
+ * the host's lookup reaches the client: running out of time, an error that
443
+ * is not an `HttpException`, and a value that is not a plain JSON object. An
444
+ * `HttpException` the resolver throws is passed on as it is, because the
445
+ * host chose that answer for the member.
446
+ *
447
+ * A resolver that settles after the time has run out is not waited for; what
448
+ * it settles with is logged and dropped, so a late rejection is never left
449
+ * unhandled.
450
+ */
451
+ private resolveInitiatorCustomFields;
366
452
  listActivityLogs(instanceId: string, scope?: WorkflowReadScope): Promise<readonly ActivityLogEntity[]>;
367
453
  private assertTaskReadable;
368
454
  private isApprovalInstanceReadableByMember;
@@ -393,10 +479,11 @@ export declare class WorkflowEngineService {
393
479
  * Re-resolves the delegation of a candidate whose turn has come. When the
394
480
  * delegate already holds another candidate row on the task, the stored values
395
481
  * are kept: (task_id, member_id) is unique, and that member signs from the
396
- * row they already have. On a node that excludes the initiator the stored
397
- * values are kept as well when the new delegation path would run through the
398
- * initiator or the submitter: creation already filtered such paths out, and a
399
- * delegation added while the candidate waited must not bring them back.
482
+ * row they already have. On a node that keeps members out (the initiator and
483
+ * submitter, or the approvers of earlier stages) the stored values are kept
484
+ * as well when the new delegation path would run through one of them:
485
+ * creation already filtered such paths out, and a delegation added while the
486
+ * candidate waited must not bring them back.
400
487
  */
401
488
  private resolveActivationDelegation;
402
489
  private markTaskCandidateDecision;
@@ -499,12 +586,52 @@ export declare class WorkflowEngineService {
499
586
  */
500
587
  private notifySeatReplaced;
501
588
  /**
502
- * Resolves who gets the node's task. `null` means the node asked to be
503
- * skipped (`excludeInitiator.whenEmpty = SKIP_NODE`) because the initiator
504
- * was the only approver — the sole caller creates no task and advances the
505
- * token instead.
589
+ * Resolves who gets the node's task. A {@link SkippedRuntimeTask} means the
590
+ * node asked to be skipped (`excludeInitiator.whenEmpty` or
591
+ * `excludeNodeActors.whenEmpty` = SKIP_NODE) because everyone left was kept
592
+ * out: the sole caller creates no task and advances the token instead.
593
+ *
594
+ * With both exclusions set the initiator rule runs first. When it alone
595
+ * leaves nobody, `excludeInitiator.whenEmpty` decides; when it leaves
596
+ * somebody and the earlier-stage rule then removes them all,
597
+ * `excludeNodeActors.whenEmpty` decides.
506
598
  */
507
599
  private resolveRuntimeTaskCandidates;
600
+ /**
601
+ * Everyone `node` keeps out of its approvers, per rule: the initiator and
602
+ * the submitter for `excludeInitiator`, and everyone who actually approved
603
+ * one of the listed earlier stages for `excludeNodeActors`. Read through
604
+ * `manager` so an approval written earlier in this transaction counts.
605
+ */
606
+ private readNodeExclusions;
607
+ /**
608
+ * The people who approved any of `nodeIds`, each listed node read on its own
609
+ * with the `NODE_ACTOR` rules ({@link readNodeActorMemberIds}) and the
610
+ * results combined. A source reads only the most recent of its listed nodes;
611
+ * an exclusion must cover every listed node, so it asks one node at a time.
612
+ *
613
+ * There is no round boundary here: a node's newest batch is what counts. The
614
+ * publish lint makes that the right batch, since every listed node runs and
615
+ * is signed by a person on every path to this node (a return to the start
616
+ * runs it again; a return to a stage below it leaves its earlier approval
617
+ * standing, and the exclusion with it).
618
+ */
619
+ private readExcludedNodeActorMemberIds;
620
+ /**
621
+ * Whether `targetMemberId` may not take a seat on `node` under
622
+ * `excludeNodeActors`, checked the way task creation checks a candidate: the
623
+ * target is one of the excluded members, or one end of a delegation between
624
+ * the target and an excluded member is on the path. A delegation is read in
625
+ * both directions, since the target may be the delegate of an excluded
626
+ * member, or delegate an excluded member's seat onward.
627
+ */
628
+ private isExcludedNodeActorTarget;
629
+ /**
630
+ * The principals the approvals stand for: a delegate who approved counts as
631
+ * the member who delegated to them, read from the candidate row each approval
632
+ * was signed on (see {@link readRuleDelegatorMemberIds}).
633
+ */
634
+ private readApprovalPrincipalMemberIds;
508
635
  private resolveInitiatorManagerReplacements;
509
636
  private buildDelegationContext;
510
637
  private delegateResolvedCandidates;
@@ -569,6 +696,12 @@ export declare class WorkflowEngineService {
569
696
  * tasks and approvals the SLA timeout made do not count.
570
697
  */
571
698
  private readNodeActorMemberIds;
699
+ /**
700
+ * The real approvals (earliest first) of the most recent batch among
701
+ * `nodeIds`: the rows {@link readNodeActorMemberIds} reads the approvers from.
702
+ * Kept as decisions so the exclusion can also reach each decision's task.
703
+ */
704
+ private readNodeActorApprovals;
572
705
  private resolvePositionCandidates;
573
706
  /**
574
707
  * The members who hold the position today (organisation time zone) within
@@ -578,12 +711,27 @@ export declare class WorkflowEngineService {
578
711
  private readActivePositionMemberIds;
579
712
  private resolveInitiatorManagerCandidates;
580
713
  /**
581
- * Walks up the initiator's manager chain, nearest manager first, until a
582
- * manager whose position level reaches `minPositionLevel` is added. The
583
- * list is returned nearest manager first. Members in `skippedMemberIds` are
584
- * walked past without joining the chain or counting as the stopping point.
714
+ * Walks up a manager chain, nearest manager first, until a manager whose
715
+ * position level reaches `minPositionLevel` is added. The chain starts at
716
+ * the initiator unless `startFrom` names someone else, and the start person
717
+ * is never on it: the walk begins at their direct manager. The list is
718
+ * returned nearest manager first. Members in `skippedMemberIds` are walked
719
+ * past without joining the chain or counting as the stopping point, and so
720
+ * are managers below `startPositionLevel`, until the first one at or above
721
+ * it starts the chain.
585
722
  */
586
723
  private resolveOrgManagerChainCandidates;
724
+ /**
725
+ * Whose manager an `ORG_MANAGER_CHAIN` starts from. `memberId` is `null`
726
+ * when the start names nobody (an empty form field, an earlier stage nobody
727
+ * approved), with `missingReason` saying why; `description` is the lead-in
728
+ * of the "no manager reaches the level" reason, empty for the initiator so
729
+ * a chain without a start keeps today's wording.
730
+ *
731
+ * A start that picks more than one person is refused rather than guessed,
732
+ * the same as a manager rule that names several managers at one priority.
733
+ */
734
+ private resolveOrgManagerChainStart;
587
735
  /** Highest position level over the member's active memberships, or `null`. */
588
736
  private readMemberPositionLevel;
589
737
  /**