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.
- package/CHANGELOG.md +38 -0
- package/README.md +27 -0
- package/dist/{ApprovalEngine-CW7Uu1bV.d.cts → ApprovalEngine-BWrblNQ0.d.cts} +28 -6
- package/dist/{ApprovalEngine-BHWls8-z.d.ts → ApprovalEngine-D4DdSx-M.d.ts} +28 -6
- package/dist/{IAuditAdapter-Dwx8ZP4J.d.cts → IAuditAdapter-2UU5edKG.d.cts} +1 -1
- package/dist/{IAuditAdapter-CMCirvY-.d.ts → IAuditAdapter-DSor54iY.d.ts} +1 -1
- package/dist/{IAuthorizationPolicy-BryQIKko.d.ts → IAuthorizationPolicy-BOWg-3AH.d.ts} +1 -1
- package/dist/{IAuthorizationPolicy-B3vaV69v.d.cts → IAuthorizationPolicy-DPpiKzyh.d.cts} +1 -1
- package/dist/{INotificationAdapter-BBq2czpO.d.ts → INotificationAdapter-BNGivLgU.d.ts} +1 -1
- package/dist/{INotificationAdapter-AwbhZEqz.d.cts → INotificationAdapter-C6HPrzh6.d.cts} +1 -1
- package/dist/{IOperationMiddleware-CqzFklMn.d.ts → IOperationMiddleware-CXtg2wzF.d.ts} +1 -1
- package/dist/{IOperationMiddleware-3J-_NSGq.d.cts → IOperationMiddleware-CxnPzpqL.d.cts} +1 -1
- package/dist/{IStorageAdapter-Dw2xXP9B.d.ts → IStorageAdapter-ChPT7ZDp.d.ts} +1 -1
- package/dist/{IStorageAdapter-Pq9J60xv.d.cts → IStorageAdapter-yX9ERfQE.d.cts} +1 -1
- package/dist/adapters/MemoryAdapter.d.cts +2 -2
- package/dist/adapters/MemoryAdapter.d.ts +2 -2
- package/dist/adapters/PostgresAdapter.d.cts +2 -2
- package/dist/adapters/PostgresAdapter.d.ts +2 -2
- package/dist/index.cjs +65 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +65 -8
- package/dist/index.js.map +1 -1
- package/dist/{instance-BO-i9-nq.d.cts → instance-weh2w7Ji.d.cts} +26 -1
- package/dist/{instance-BO-i9-nq.d.ts → instance-weh2w7Ji.d.ts} +26 -1
- package/dist/nestjs.cjs +65 -8
- package/dist/nestjs.cjs.map +1 -1
- package/dist/nestjs.d.cts +7 -7
- package/dist/nestjs.d.ts +7 -7
- package/dist/nestjs.js +65 -8
- package/dist/nestjs.js.map +1 -1
- package/dist/plugins/audit.d.cts +2 -2
- package/dist/plugins/audit.d.ts +2 -2
- package/dist/plugins/notify.d.cts +2 -2
- package/dist/plugins/notify.d.ts +2 -2
- package/dist/plugins/resilience.d.cts +3 -3
- package/dist/plugins/resilience.d.ts +3 -3
- package/dist/plugins/tracing.d.cts +2 -2
- package/dist/plugins/tracing.d.ts +2 -2
- package/dist/plugins/webhook.d.cts +2 -2
- package/dist/plugins/webhook.d.ts +2 -2
- package/dist/testing.cjs +65 -8
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +7 -7
- package/dist/testing.d.ts +7 -7
- package/dist/testing.js +65 -8
- package/dist/testing.js.map +1 -1
- 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-
|
|
2
|
-
import { l as ConditionExpression,
|
|
3
|
-
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-
|
|
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-
|
|
8
|
-
import { I as IAuditAdapter } from './IAuditAdapter-
|
|
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-
|
|
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-
|
|
2
|
-
import { l as ConditionExpression,
|
|
3
|
-
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-
|
|
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-
|
|
8
|
-
import { I as IAuditAdapter } from './IAuditAdapter-
|
|
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-
|
|
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 { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-
|
|
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-
|
|
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 ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-
|
|
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-
|
|
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-
|
|
2
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-
|
|
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-
|
|
2
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-
|
|
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-
|
|
4
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-
|
|
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-
|
|
4
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-
|
|
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
|
-
|
|
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
|
|
3377
|
-
|
|
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
|
-
[
|
|
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 =
|
|
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
|
}
|