hierarchical-approval 2.5.0 → 2.6.0

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 (49) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +27 -0
  3. package/dist/{ApprovalEngine-CW7Uu1bV.d.cts → ApprovalEngine-BWrblNQ0.d.cts} +28 -6
  4. package/dist/{ApprovalEngine-BHWls8-z.d.ts → ApprovalEngine-D4DdSx-M.d.ts} +28 -6
  5. package/dist/{IAuditAdapter-Dwx8ZP4J.d.cts → IAuditAdapter-2UU5edKG.d.cts} +1 -1
  6. package/dist/{IAuditAdapter-CMCirvY-.d.ts → IAuditAdapter-DSor54iY.d.ts} +1 -1
  7. package/dist/{IAuthorizationPolicy-BryQIKko.d.ts → IAuthorizationPolicy-BOWg-3AH.d.ts} +1 -1
  8. package/dist/{IAuthorizationPolicy-B3vaV69v.d.cts → IAuthorizationPolicy-DPpiKzyh.d.cts} +1 -1
  9. package/dist/{INotificationAdapter-BBq2czpO.d.ts → INotificationAdapter-BNGivLgU.d.ts} +1 -1
  10. package/dist/{INotificationAdapter-AwbhZEqz.d.cts → INotificationAdapter-C6HPrzh6.d.cts} +1 -1
  11. package/dist/{IOperationMiddleware-CqzFklMn.d.ts → IOperationMiddleware-CXtg2wzF.d.ts} +1 -1
  12. package/dist/{IOperationMiddleware-3J-_NSGq.d.cts → IOperationMiddleware-CxnPzpqL.d.cts} +1 -1
  13. package/dist/{IStorageAdapter-Dw2xXP9B.d.ts → IStorageAdapter-ChPT7ZDp.d.ts} +1 -1
  14. package/dist/{IStorageAdapter-Pq9J60xv.d.cts → IStorageAdapter-yX9ERfQE.d.cts} +1 -1
  15. package/dist/adapters/MemoryAdapter.d.cts +2 -2
  16. package/dist/adapters/MemoryAdapter.d.ts +2 -2
  17. package/dist/adapters/PostgresAdapter.d.cts +2 -2
  18. package/dist/adapters/PostgresAdapter.d.ts +2 -2
  19. package/dist/index.cjs +65 -8
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +8 -8
  22. package/dist/index.d.ts +8 -8
  23. package/dist/index.js +65 -8
  24. package/dist/index.js.map +1 -1
  25. package/dist/{instance-BO-i9-nq.d.cts → instance-weh2w7Ji.d.cts} +26 -1
  26. package/dist/{instance-BO-i9-nq.d.ts → instance-weh2w7Ji.d.ts} +26 -1
  27. package/dist/nestjs.cjs +65 -8
  28. package/dist/nestjs.cjs.map +1 -1
  29. package/dist/nestjs.d.cts +7 -7
  30. package/dist/nestjs.d.ts +7 -7
  31. package/dist/nestjs.js +65 -8
  32. package/dist/nestjs.js.map +1 -1
  33. package/dist/plugins/audit.d.cts +2 -2
  34. package/dist/plugins/audit.d.ts +2 -2
  35. package/dist/plugins/notify.d.cts +2 -2
  36. package/dist/plugins/notify.d.ts +2 -2
  37. package/dist/plugins/resilience.d.cts +3 -3
  38. package/dist/plugins/resilience.d.ts +3 -3
  39. package/dist/plugins/tracing.d.cts +2 -2
  40. package/dist/plugins/tracing.d.ts +2 -2
  41. package/dist/plugins/webhook.d.cts +2 -2
  42. package/dist/plugins/webhook.d.ts +2 -2
  43. package/dist/testing.cjs +65 -8
  44. package/dist/testing.cjs.map +1 -1
  45. package/dist/testing.d.cts +7 -7
  46. package/dist/testing.d.ts +7 -7
  47. package/dist/testing.js +65 -8
  48. package/dist/testing.js.map +1 -1
  49. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,44 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  _Nothing yet._
9
9
 
10
+ ## [2.6.0] - 2026-09-04
11
+
12
+ ### Added — escalation ladders
13
+
14
+ - **`escalationSteps` escalates repeatedly, up a chain.** A single `escalation`
15
+ could only ever fire once, so a request that stalled past its second deadline
16
+ had nowhere further to go — the usual "chase the manager, then the director,
17
+ then the VP" pattern had to be built outside the engine, on top of the events.
18
+
19
+ ```ts
20
+ escalationSteps: [
21
+ { afterDays: 2, escalateTo: { type: 'user', userId: 'director' } },
22
+ { afterDays: 4, escalateTo: { type: 'user', userId: 'vp' } },
23
+ { afterDays: 7, escalateTo: { type: 'role', role: 'exec' } },
24
+ ]
25
+ ```
26
+
27
+ Rungs are sorted by delay and fire in order, each **adding** an approver rather
28
+ than replacing one: escalation widens the pool, it does not hand the work over.
29
+
30
+ - **Delays are measured from when the level opened, not from the previous
31
+ escalation**, so a ladder reads the way it is written. The level's opening
32
+ time is recovered from its `submitted` / `level_advanced` audit entry.
33
+
34
+ - Rungs accept `afterHours` as well as `afterDays`, counted through the
35
+ working-hours calendar from 2.5.0 when one is configured. A per-level
36
+ `escalationAfterDays`/`escalationAfterHours` still decides *when* the first
37
+ rung fires — it is the more specific statement about that level — while the
38
+ ladder supplies *who*.
39
+
40
+ - Templates carrying only the single-step `escalation` behave exactly as before;
41
+ the ladder takes precedence when both are set. Levels track progress in
42
+ `escalationStep`, and the ladder is captured in the template snapshot, so an
43
+ in-flight approval keeps the ladder it was submitted under.
44
+
45
+ New exports: `EscalationStep`. `ApprovalTemplateConfig` gains
46
+ `escalationSteps`; levels gain `escalationStep`.
47
+
10
48
  ## [2.5.0] - 2026-09-04
11
49
 
12
50
  ### Added — deadlines in working hours
package/README.md CHANGED
@@ -468,6 +468,33 @@ The sweep is **not atomic**: it reports per-instance failures rather than rollin
468
468
  back. A partial transfer is the useful outcome — the approvals that can move
469
469
  should move, and the ones that cannot are named so a human can look at them.
470
470
 
471
+ ### Escalation ladders
472
+
473
+ A single `escalation` fires once, so a request that stalls past its second
474
+ deadline has nowhere further to go. `escalationSteps` is a ladder:
475
+
476
+ ```ts
477
+ escalationSteps: [
478
+ { afterDays: 2, escalateTo: { type: 'user', userId: 'director' } },
479
+ { afterDays: 4, escalateTo: { type: 'user', userId: 'vp' } },
480
+ { afterDays: 7, escalateTo: { type: 'role', role: 'exec' } },
481
+ ]
482
+ ```
483
+
484
+ Rungs are sorted by delay and fire in order, each **adding** an approver without
485
+ removing anyone already assigned — escalation widens the pool rather than
486
+ handing the work over.
487
+
488
+ Every delay is measured **from the moment the level opened**, not from the
489
+ previous escalation, so the ladder reads the way it is written: late at day 2,
490
+ later at day 4, latest at day 7. Rungs accept `afterHours` too, counted through
491
+ the working-hours calendar if one is configured.
492
+
493
+ A per-level `escalationAfterDays`/`escalationAfterHours` still wins for *when*
494
+ the first rung fires — it is the more specific statement about that level — while
495
+ the ladder supplies *who*. Templates with only the single-step `escalation`
496
+ behave exactly as before.
497
+
471
498
  ### SLAs in working hours
472
499
 
473
500
  Deadlines were whole days, so "respond within four working hours" could only be
@@ -1,14 +1,14 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-Pq9J60xv.cjs';
2
- import { l as ConditionExpression, p as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BO-i9-nq.cjs';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-AwbhZEqz.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-yX9ERfQE.cjs';
2
+ import { l as ConditionExpression, q as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-weh2w7Ji.cjs';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-C6HPrzh6.cjs';
4
4
  import { z } from 'zod';
5
5
  import { L as Logger } from './Logger-BplhlU7l.cjs';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.cjs';
7
- import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-3J-_NSGq.cjs';
8
- import { I as IAuditAdapter } from './IAuditAdapter-Dwx8ZP4J.cjs';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-CxnPzpqL.cjs';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-2UU5edKG.cjs';
9
9
  import { I as IMetricsAdapter } from './IMetricsAdapter-DWq8IFaf.cjs';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.cjs';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-B3vaV69v.cjs';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-DPpiKzyh.cjs';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -852,6 +852,28 @@ declare class ApprovalEngine {
852
852
  private deadlineFromHours;
853
853
  /** Level deadline from whichever of days/hours the template configured. */
854
854
  private levelEscalationDue;
855
+ /** First rung of a ladder, sorted by delay, or undefined when there is none. */
856
+ private firstRungOf;
857
+ /**
858
+ * The escalation ladder for an instance, sorted by delay.
859
+ *
860
+ * Read from the instance's template snapshot so an in-flight approval keeps
861
+ * the ladder it was submitted under, exactly as the single-step
862
+ * {@link EscalationConfig} already did.
863
+ */
864
+ private escalationLadder;
865
+ /**
866
+ * When a level started collecting decisions.
867
+ *
868
+ * Escalation rungs are measured from this, not from the previous rung, so a
869
+ * ladder reads the way it is written. Recovered from the audit trail — the
870
+ * `submitted` entry for the opening level, the `level_advanced` entry
871
+ * otherwise — and falls back to `now` when no entry exists, which only leaves
872
+ * the ladder no worse off than the single-step behaviour it replaces.
873
+ */
874
+ private levelOpenedAt;
875
+ /** Deadline for one rung, measured from when the level opened. */
876
+ private stepDueAt;
855
877
  private requireInstance;
856
878
  /**
857
879
  * Identity of the parallel branch group a level belongs to.
@@ -1,14 +1,14 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-Dw2xXP9B.js';
2
- import { l as ConditionExpression, p as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BO-i9-nq.js';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BBq2czpO.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-ChPT7ZDp.js';
2
+ import { l as ConditionExpression, q as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-weh2w7Ji.js';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BNGivLgU.js';
4
4
  import { z } from 'zod';
5
5
  import { L as Logger } from './Logger-BplhlU7l.js';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.js';
7
- import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-CqzFklMn.js';
8
- import { I as IAuditAdapter } from './IAuditAdapter-CMCirvY-.js';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-CXtg2wzF.js';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-DSor54iY.js';
9
9
  import { I as IMetricsAdapter } from './IMetricsAdapter-DWq8IFaf.js';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.js';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-BryQIKko.js';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-BOWg-3AH.js';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -852,6 +852,28 @@ declare class ApprovalEngine {
852
852
  private deadlineFromHours;
853
853
  /** Level deadline from whichever of days/hours the template configured. */
854
854
  private levelEscalationDue;
855
+ /** First rung of a ladder, sorted by delay, or undefined when there is none. */
856
+ private firstRungOf;
857
+ /**
858
+ * The escalation ladder for an instance, sorted by delay.
859
+ *
860
+ * Read from the instance's template snapshot so an in-flight approval keeps
861
+ * the ladder it was submitted under, exactly as the single-step
862
+ * {@link EscalationConfig} already did.
863
+ */
864
+ private escalationLadder;
865
+ /**
866
+ * When a level started collecting decisions.
867
+ *
868
+ * Escalation rungs are measured from this, not from the previous rung, so a
869
+ * ladder reads the way it is written. Recovered from the audit trail — the
870
+ * `submitted` entry for the opening level, the `level_advanced` entry
871
+ * otherwise — and falls back to `now` when no entry exists, which only leaves
872
+ * the ladder no worse off than the single-step behaviour it replaces.
873
+ */
874
+ private levelOpenedAt;
875
+ /** Deadline for one rung, measured from when the level opened. */
876
+ private stepDueAt;
855
877
  private requireInstance;
856
878
  /**
857
879
  * Identity of the parallel branch group a level belongs to.
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-BO-i9-nq.cjs';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-weh2w7Ji.cjs';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-BO-i9-nq.js';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-weh2w7Ji.js';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BO-i9-nq.js';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-weh2w7Ji.js';
2
2
 
3
3
  interface AuthorizationContext {
4
4
  operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo' | 'addAttachment' | 'removeAttachment';
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BO-i9-nq.cjs';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-weh2w7Ji.cjs';
2
2
 
3
3
  interface AuthorizationContext {
4
4
  operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo' | 'addAttachment' | 'removeAttachment';
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-BO-i9-nq.js';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-weh2w7Ji.js';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-BO-i9-nq.cjs';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-weh2w7Ji.cjs';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance } from './instance-BO-i9-nq.js';
1
+ import { a as ApprovalInstance } from './instance-weh2w7Ji.js';
2
2
 
3
3
  declare class ApprovalError extends Error {
4
4
  readonly code: string;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance } from './instance-BO-i9-nq.cjs';
1
+ import { a as ApprovalInstance } from './instance-weh2w7Ji.cjs';
2
2
 
3
3
  declare class ApprovalError extends Error {
4
4
  readonly code: string;
@@ -1,4 +1,4 @@
1
- import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BO-i9-nq.js';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-weh2w7Ji.js';
2
2
 
3
3
  interface PaginationOpts {
4
4
  limit: number;
@@ -1,4 +1,4 @@
1
- import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BO-i9-nq.cjs';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-weh2w7Ji.cjs';
2
2
 
3
3
  interface PaginationOpts {
4
4
  limit: number;
@@ -1,5 +1,5 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-Pq9J60xv.cjs';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BO-i9-nq.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-yX9ERfQE.cjs';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-weh2w7Ji.cjs';
3
3
 
4
4
  declare class MemoryAdapter implements IStorageAdapter {
5
5
  private templates;
@@ -1,5 +1,5 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-Dw2xXP9B.js';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BO-i9-nq.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-ChPT7ZDp.js';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-weh2w7Ji.js';
3
3
 
4
4
  declare class MemoryAdapter implements IStorageAdapter {
5
5
  private templates;
@@ -1,7 +1,7 @@
1
1
  import * as tls from 'tls';
2
2
  import * as pg from 'pg';
3
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-Pq9J60xv.cjs';
4
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BO-i9-nq.cjs';
3
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-yX9ERfQE.cjs';
4
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-weh2w7Ji.cjs';
5
5
 
6
6
  interface PostgresAdapterOptions {
7
7
  connectionString?: string;
@@ -1,7 +1,7 @@
1
1
  import * as tls from 'tls';
2
2
  import * as pg from 'pg';
3
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-Dw2xXP9B.js';
4
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BO-i9-nq.js';
3
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-ChPT7ZDp.js';
4
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-weh2w7Ji.js';
5
5
 
6
6
  interface PostgresAdapterOptions {
7
7
  connectionString?: string;
package/dist/index.cjs CHANGED
@@ -1273,7 +1273,8 @@ var ApprovalEngine = class _ApprovalEngine {
1273
1273
  weights: cfg.weights,
1274
1274
  escalationAfterDays: cfg.escalationAfterDays,
1275
1275
  escalationAfterHours: cfg.escalationAfterHours,
1276
- escalationDueAt: inFirstGroup ? this.levelEscalationDue(now, cfg) : void 0,
1276
+ escalationStep: 0,
1277
+ escalationDueAt: inFirstGroup ? this.levelEscalationDue(now, cfg, this.firstRungOf(template.escalationSteps)) : void 0,
1277
1278
  subWorkflowTemplate: cfg.subWorkflow?.templateName,
1278
1279
  reminderAfterDays: cfg.reminderAfterDays,
1279
1280
  reminderEveryDays: cfg.reminderEveryDays,
@@ -1330,6 +1331,7 @@ var ApprovalEngine = class _ApprovalEngine {
1330
1331
  slaDeadlineAt,
1331
1332
  templateSnapshot: {
1332
1333
  escalation: template.escalation,
1334
+ escalationSteps: template.escalationSteps,
1333
1335
  slaDeadlineDays: template.slaDeadlineDays,
1334
1336
  slaDeadlineHours: template.slaDeadlineHours,
1335
1337
  allowOverride: template.allowOverride
@@ -2625,6 +2627,7 @@ var ApprovalEngine = class _ApprovalEngine {
2625
2627
  slaDeadlineAt,
2626
2628
  templateSnapshot: {
2627
2629
  escalation: template.escalation,
2630
+ escalationSteps: template.escalationSteps,
2628
2631
  slaDeadlineDays: template.slaDeadlineDays,
2629
2632
  slaDeadlineHours: template.slaDeadlineHours,
2630
2633
  allowOverride: template.allowOverride
@@ -3373,10 +3376,14 @@ var ApprovalEngine = class _ApprovalEngine {
3373
3376
  async escalateInternal(instanceId, escalatedBy = "system", auditCtx, levelNumber) {
3374
3377
  return this.withOptimisticRetry(instanceId, async (instance) => {
3375
3378
  if (instance.status !== "pending") return instance;
3376
- const escalationConfig = instance.templateSnapshot?.escalation ?? (await this.registry.get(instance.templateName)).escalation;
3377
- if (!escalationConfig) return instance;
3379
+ const ladder = await this.escalationLadder(instance);
3380
+ const targetLevel = levelNumber === void 0 ? this.currentLevelInstance(instance) : instance.levels.find((l) => l.level === levelNumber) ?? this.currentLevelInstance(instance);
3381
+ const rungIndex = targetLevel.escalationStep ?? 0;
3382
+ const rung = ladder[rungIndex];
3383
+ const escalateTo = rung?.escalateTo ?? (instance.templateSnapshot?.escalation ?? (await this.registry.get(instance.templateName)).escalation)?.escalateTo;
3384
+ if (!escalateTo) return instance;
3378
3385
  const newApprovers = await this.resolver.resolveApprovers(
3379
- [escalationConfig.escalateTo],
3386
+ [escalateTo],
3380
3387
  instance.submittedBy,
3381
3388
  instance.data,
3382
3389
  this.opts.orgProvider,
@@ -3394,10 +3401,13 @@ var ApprovalEngine = class _ApprovalEngine {
3394
3401
  );
3395
3402
  return instance;
3396
3403
  }
3397
- const level = levelNumber === void 0 ? this.currentLevelInstance(instance) : instance.levels.find((l) => l.level === levelNumber) ?? this.currentLevelInstance(instance);
3404
+ const level = targetLevel;
3398
3405
  level.approverIds = [.../* @__PURE__ */ new Set([...level.approverIds, ...filteredApprovers])];
3399
- level.escalationDueAt = void 0;
3400
3406
  const now = this.clock.now();
3407
+ level.escalationStep = rungIndex + 1;
3408
+ const nextRung = ladder[rungIndex + 1];
3409
+ const levelOpenedAt = this.levelOpenedAt(instance, level, now);
3410
+ level.escalationDueAt = nextRung ? this.stepDueAt(levelOpenedAt, nextRung) : void 0;
3401
3411
  const escalatedTo = filteredApprovers[0] ?? "unknown";
3402
3412
  const auditEntry = {
3403
3413
  action: "escalated",
@@ -3582,9 +3592,54 @@ var ApprovalEngine = class _ApprovalEngine {
3582
3592
  return addHours ? addHours(from, hours) : new Date(from.getTime() + hours * 36e5);
3583
3593
  }
3584
3594
  /** Level deadline from whichever of days/hours the template configured. */
3585
- levelEscalationDue(from, level) {
3595
+ levelEscalationDue(from, level, firstRung) {
3586
3596
  if (level.escalationAfterHours) return this.deadlineFromHours(from, level.escalationAfterHours);
3587
3597
  if (level.escalationAfterDays) return this.deadlineFrom(from, level.escalationAfterDays);
3598
+ return firstRung ? this.stepDueAt(from, firstRung) : void 0;
3599
+ }
3600
+ /** First rung of a ladder, sorted by delay, or undefined when there is none. */
3601
+ firstRungOf(steps) {
3602
+ if (!steps || steps.length === 0) return void 0;
3603
+ return [...steps].sort(
3604
+ (a, b) => (a.afterHours ?? (a.afterDays ?? 0) * 24) - (b.afterHours ?? (b.afterDays ?? 0) * 24)
3605
+ )[0];
3606
+ }
3607
+ /**
3608
+ * The escalation ladder for an instance, sorted by delay.
3609
+ *
3610
+ * Read from the instance's template snapshot so an in-flight approval keeps
3611
+ * the ladder it was submitted under, exactly as the single-step
3612
+ * {@link EscalationConfig} already did.
3613
+ */
3614
+ async escalationLadder(instance) {
3615
+ const snapshot = instance.templateSnapshot;
3616
+ const steps = snapshot?.escalationSteps ?? (await this.registry.get(instance.templateName)).escalationSteps ?? [];
3617
+ return [...steps].sort(
3618
+ (a, b) => (a.afterHours ?? (a.afterDays ?? 0) * 24) - (b.afterHours ?? (b.afterDays ?? 0) * 24)
3619
+ );
3620
+ }
3621
+ /**
3622
+ * When a level started collecting decisions.
3623
+ *
3624
+ * Escalation rungs are measured from this, not from the previous rung, so a
3625
+ * ladder reads the way it is written. Recovered from the audit trail — the
3626
+ * `submitted` entry for the opening level, the `level_advanced` entry
3627
+ * otherwise — and falls back to `now` when no entry exists, which only leaves
3628
+ * the ladder no worse off than the single-step behaviour it replaces.
3629
+ */
3630
+ levelOpenedAt(instance, level, fallback) {
3631
+ for (let i = instance.auditLog.length - 1; i >= 0; i--) {
3632
+ const entry = instance.auditLog[i];
3633
+ if (!entry) continue;
3634
+ const opensThisLevel = (entry.action === "level_advanced" || entry.action === "submitted") && entry.level === level.level;
3635
+ if (opensThisLevel) return new Date(entry.timestamp);
3636
+ }
3637
+ return fallback;
3638
+ }
3639
+ /** Deadline for one rung, measured from when the level opened. */
3640
+ stepDueAt(from, step) {
3641
+ if (step.afterHours) return this.deadlineFromHours(from, step.afterHours);
3642
+ if (step.afterDays) return this.deadlineFrom(from, step.afterDays);
3588
3643
  return void 0;
3589
3644
  }
3590
3645
  async requireInstance(id) {
@@ -3679,6 +3734,7 @@ var ApprovalEngine = class _ApprovalEngine {
3679
3734
  }
3680
3735
  /** Resolve approvers for a group, set its deadlines, and mark it pending. */
3681
3736
  async activateGroup(instance, group, now) {
3737
+ const firstRung = this.firstRungOf(instance.templateSnapshot?.escalationSteps);
3682
3738
  for (const lvl of group) {
3683
3739
  if (lvl.subWorkflowTemplate) {
3684
3740
  lvl.approverIds = [];
@@ -3692,7 +3748,8 @@ var ApprovalEngine = class _ApprovalEngine {
3692
3748
  now
3693
3749
  );
3694
3750
  }
3695
- lvl.escalationDueAt = this.levelEscalationDue(now, lvl);
3751
+ lvl.escalationDueAt = this.levelEscalationDue(now, lvl, firstRung);
3752
+ lvl.escalationStep = 0;
3696
3753
  this.scheduleReminder(lvl, now);
3697
3754
  lvl.status = "pending";
3698
3755
  }