hierarchical-approval 3.3.0 → 4.0.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 +205 -0
- package/README.md +21 -0
- package/dist/{ApprovalEngine-Cslh4Goz.d.ts → ApprovalEngine-D3RKWQfD.d.ts} +85 -6
- package/dist/{ApprovalEngine-zAgvH_aO.d.cts → ApprovalEngine-DDp4Ep1B.d.cts} +85 -6
- package/dist/{IAuditAdapter-udkAnnmu.d.ts → IAuditAdapter-DkbyFFjb.d.ts} +1 -1
- package/dist/{IAuditAdapter-BkxGx6TI.d.cts → IAuditAdapter-Dt8qjkQL.d.cts} +1 -1
- package/dist/{IAuthorizationPolicy-YQlSwj-8.d.cts → IAuthorizationPolicy-YJ_08Mof.d.cts} +1 -1
- package/dist/{IAuthorizationPolicy-Z4nkX-bu.d.ts → IAuthorizationPolicy-eo6glbpE.d.ts} +1 -1
- package/dist/{INotificationAdapter-C-PL3Pvp.d.ts → INotificationAdapter-BTO9FSKy.d.ts} +1 -1
- package/dist/{INotificationAdapter-D3PBQE1B.d.cts → INotificationAdapter-XbD69gss.d.cts} +1 -1
- package/dist/{IOperationMiddleware-tgJcKzTP.d.cts → IOperationMiddleware-BBLk8MIz.d.cts} +1 -1
- package/dist/{IOperationMiddleware-Cbd05rje.d.ts → IOperationMiddleware-CX7lo4lp.d.ts} +1 -1
- package/dist/{IStorageAdapter-CEKfu-U8.d.ts → IStorageAdapter-CdXMggPK.d.ts} +1 -1
- package/dist/{IStorageAdapter-DUrmnJUs.d.cts → IStorageAdapter-Cjw5zDwQ.d.cts} +1 -1
- package/dist/adapters/MemoryAdapter.d.cts +2 -2
- package/dist/adapters/MemoryAdapter.d.ts +2 -2
- package/dist/adapters/PostgresAdapter.cjs +10 -4
- package/dist/adapters/PostgresAdapter.cjs.map +1 -1
- package/dist/adapters/PostgresAdapter.d.cts +2 -2
- package/dist/adapters/PostgresAdapter.d.ts +2 -2
- package/dist/adapters/PostgresAdapter.js +10 -4
- package/dist/adapters/PostgresAdapter.js.map +1 -1
- package/dist/index.cjs +215 -60
- 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 +215 -60
- package/dist/index.js.map +1 -1
- package/dist/{instance-BXHxvTLE.d.cts → instance-uUQh5XW3.d.cts} +17 -0
- package/dist/{instance-BXHxvTLE.d.ts → instance-uUQh5XW3.d.ts} +17 -0
- package/dist/nestjs.cjs +215 -60
- 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 +215 -60
- 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 +226 -66
- 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 +226 -66
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,211 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
_Nothing yet._
|
|
9
9
|
|
|
10
|
+
## [4.0.0] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
Six defects fixed across 3.x shared one root cause: `currentLevel` is a single
|
|
13
|
+
number, and it cannot describe the approval frontier once a parallel group has
|
|
14
|
+
several levels open at once. Each fix corrected one more reader of it. This
|
|
15
|
+
release fixes the model instead.
|
|
16
|
+
|
|
17
|
+
### BREAKING — the frontier is explicit
|
|
18
|
+
|
|
19
|
+
- **`ApprovalInstance.openLevels: number[]`** lists every level currently
|
|
20
|
+
collecting decisions, ascending — one entry for a sequential chain, several
|
|
21
|
+
inside a parallel group, empty once the instance is terminal. It is the
|
|
22
|
+
supported way to ask what an instance is waiting on, and
|
|
23
|
+
`engine.getOpenLevels(id)` returns it without fetching the whole instance.
|
|
24
|
+
|
|
25
|
+
- **`currentLevel` is now derived**, defined as the lowest open level and
|
|
26
|
+
recomputed by the engine on every write. It remains for display and for the
|
|
27
|
+
audit trail, and a terminal instance keeps its last value so the record still
|
|
28
|
+
shows where the request stopped.
|
|
29
|
+
|
|
30
|
+
**Migrating:** anything using `currentLevel` to decide who may act, what to
|
|
31
|
+
notify, or what is overdue should read `openLevels`. On a sequential template
|
|
32
|
+
the two agree, so most callers need no change.
|
|
33
|
+
|
|
34
|
+
- **Custom `IStorageAdapter` implementations must round-trip `openLevels`**, as
|
|
35
|
+
they already must for `levels`. The bundled adapters do; `PostgresAdapter`
|
|
36
|
+
adds an `open_levels` column via `migrate()`.
|
|
37
|
+
|
|
38
|
+
`ApprovalInstance` now requires the field, so a hand-built instance (a test
|
|
39
|
+
fixture, say) will not compile until it supplies one.
|
|
40
|
+
|
|
41
|
+
### Fixed — returning past a rejected group
|
|
42
|
+
|
|
43
|
+
- **`returnTo: 'previous'` from inside a parallel group went back into that same
|
|
44
|
+
group.** It stepped back from `currentLevel`, which — now that the frontier
|
|
45
|
+
moves as branches close — could be another branch of the group being rejected.
|
|
46
|
+
It now steps back past the whole group, which is what "return to the previous
|
|
47
|
+
step" means when the step has several branches.
|
|
48
|
+
|
|
49
|
+
### Internal
|
|
50
|
+
|
|
51
|
+
- Every instance write goes through one path that recomputes the frontier first,
|
|
52
|
+
so a future operation cannot persist levels without updating `openLevels`.
|
|
53
|
+
This is the same discipline applied to level construction in 3.0.0 and to the
|
|
54
|
+
PostgreSQL column list in 1.6.0, both of which drifted for the same reason.
|
|
55
|
+
|
|
56
|
+
## [3.9.0] - 2026-09-04
|
|
57
|
+
|
|
58
|
+
### Fixed — replacing an approver on a weighted level bricked the approval
|
|
59
|
+
|
|
60
|
+
- **On a weighted level the weight belongs to the seat, not the person — but
|
|
61
|
+
replacing the person dropped it.** `weights` is keyed by approver id, and a
|
|
62
|
+
substitute was not in the map, so they fell back to the default weight of 1.
|
|
63
|
+
|
|
64
|
+
A CFO carrying weight 3 on a level with `threshold: 3` who was reassigned,
|
|
65
|
+
delegated, or covered by out-of-office left the level's achievable weight at
|
|
66
|
+
2. The next decision then threw
|
|
67
|
+
|
|
68
|
+
> `Level 1 ("Exec") needs a weight of 3 but the assigned approvers total only 2.`
|
|
69
|
+
|
|
70
|
+
and **nobody could approve the instance again**. `transferApprovals()` did this
|
|
71
|
+
in bulk, which made offboarding — the exact situation the feature exists for —
|
|
72
|
+
the most likely way to hit it.
|
|
73
|
+
|
|
74
|
+
A substitute now inherits the weight of the approver they replace, through
|
|
75
|
+
`reassign()`, `delegate()` and out-of-office cover alike. The original's weight
|
|
76
|
+
is copied rather than moved, so a vote they had already cast keeps counting.
|
|
77
|
+
|
|
78
|
+
Nothing is invented: an approver who never carried a weight still gets the
|
|
79
|
+
default, and escalation continues to *add* approvers at the default weight
|
|
80
|
+
rather than inheriting anyone's.
|
|
81
|
+
|
|
82
|
+
## [3.8.0] - 2026-09-04
|
|
83
|
+
|
|
84
|
+
### Fixed — a condition could build a parallel group the validator forbids
|
|
85
|
+
|
|
86
|
+
- **`validateTemplate()` checked group contiguity across the static levels
|
|
87
|
+
only.** A condition whose `addLevels` joined an existing group from further
|
|
88
|
+
down the chain therefore passed validation and then built, at runtime,
|
|
89
|
+
precisely the interleaving the rule exists to forbid.
|
|
90
|
+
|
|
91
|
+
With levels 1–2 in group `review` and an ungrouped level 3, a rule adding
|
|
92
|
+
level 4 to `review` produced a chain where levels 1, 2 **and 4** were all open
|
|
93
|
+
while level 3 sat waiting — the group had jumped over a level that was meant
|
|
94
|
+
to come first, and the template author's ordering was silently inverted.
|
|
95
|
+
|
|
96
|
+
Contiguity is now checked across every level a template can produce, static
|
|
97
|
+
and condition-added alike. A group whose members are interleaved with anything
|
|
98
|
+
else is rejected at definition time, where the author can see it.
|
|
99
|
+
|
|
100
|
+
## [3.7.0] - 2026-09-04
|
|
101
|
+
|
|
102
|
+
### Fixed — `override()` and expiry did not end the sub-workflow family
|
|
103
|
+
|
|
104
|
+
3.0.0 made a finished parent stop its children and report to its own parent,
|
|
105
|
+
but wired that into `approve()`, `reject()` and `cancel()` only. `override()`
|
|
106
|
+
and deadline expiry are terminal too, and were missed — so the fix was
|
|
107
|
+
incomplete in exactly the two paths nobody watches.
|
|
108
|
+
|
|
109
|
+
- **An expired child left its parent waiting forever.** The child reached
|
|
110
|
+
`cancelled`, but never told the parent, which sat pending on an approval that
|
|
111
|
+
could now never happen. A permanent deadlock, and the more serious of the two.
|
|
112
|
+
|
|
113
|
+
- **An overridden parent left its children running** — still notifying, still
|
|
114
|
+
escalating, still in `getWorkload()`, for a decision whose outcome nobody
|
|
115
|
+
would read.
|
|
116
|
+
|
|
117
|
+
Both now run the same post-decision step as every other terminal path.
|
|
118
|
+
`override()` continues to bypass the parent's own remaining levels, which is
|
|
119
|
+
what an administrative override is for; what changes is that it no longer
|
|
120
|
+
strands the work it started.
|
|
121
|
+
|
|
122
|
+
## [3.6.0] - 2026-09-04
|
|
123
|
+
|
|
124
|
+
### Fixed — `resubmit()` rebuilt an incomplete chain
|
|
125
|
+
|
|
126
|
+
- **A resubmitted approval could never finish.** `resubmit()` was a third
|
|
127
|
+
hand-written copy of level construction, after `submit()` and
|
|
128
|
+
`recomputeFutureChain()` (unified in 3.0.0), and the only one that decided
|
|
129
|
+
what to open by **array index** rather than by group. On a template whose
|
|
130
|
+
chain begins with a parallel group, resubmitting opened just the first branch
|
|
131
|
+
and left the rest of the group behind.
|
|
132
|
+
|
|
133
|
+
It also dropped `group`, `subWorkflow`, `escalationAfterHours` and every
|
|
134
|
+
reminder field — so a sub-workflow level came back unbound and threw
|
|
135
|
+
"No approvers resolved for this level" the moment the chain reached it,
|
|
136
|
+
leaving the approval permanently stuck.
|
|
137
|
+
|
|
138
|
+
All three paths now go through the same `buildLevelInstance()`.
|
|
139
|
+
|
|
140
|
+
### Changed — the release audit gate distinguishes a flaky endpoint from a real CVE
|
|
141
|
+
|
|
142
|
+
- `npm audit` exits non-zero both for "found an advisory" and for "could not
|
|
143
|
+
reach the advisory endpoint", and the v3.1.0 publish was blocked by the
|
|
144
|
+
latter. The CI and publish workflows now retry **only** the transient case,
|
|
145
|
+
with backoff.
|
|
146
|
+
|
|
147
|
+
A genuine high/critical advisory still fails on the first attempt, and an
|
|
148
|
+
endpoint that stays unreachable still fails the build — refusing to publish
|
|
149
|
+
unaudited, rather than passing silently the way an `|| true` would.
|
|
150
|
+
|
|
151
|
+
## [3.5.0] - 2026-09-04
|
|
152
|
+
|
|
153
|
+
### Fixed — an approval could complete with a branch rejected and another never decided
|
|
154
|
+
|
|
155
|
+
**The most serious defect found in this audit. Upgrade if you use
|
|
156
|
+
`returnTo: 'previous'` with parallel branch groups.**
|
|
157
|
+
|
|
158
|
+
Rejecting one branch of a parallel group with `returnTo: 'previous'` sent the
|
|
159
|
+
chain back a level but left the rest of the group as it was — the rejected
|
|
160
|
+
branch still `rejected`, its sibling still `pending`. Neither was `waiting`, and
|
|
161
|
+
the engine treats "no waiting level" as "nothing left to do". So when the
|
|
162
|
+
earlier level was approved again, the instance was marked **`approved`**:
|
|
163
|
+
|
|
164
|
+
- with one branch **rejected**, and
|
|
165
|
+
- with another branch **nobody had ever decided**.
|
|
166
|
+
|
|
167
|
+
A document could therefore reach fully-approved without Finance ever approving
|
|
168
|
+
it and over Legal's explicit rejection.
|
|
169
|
+
|
|
170
|
+
Two independent fixes, because one of them should never have been needed:
|
|
171
|
+
|
|
172
|
+
- **Returning to a previous level now resets every level above it** to a clean
|
|
173
|
+
`waiting` state, clearing decisions, approvers and deadlines so the chain
|
|
174
|
+
replays properly. A branch that had already approved must decide again — its
|
|
175
|
+
approval was for a version that was sent back.
|
|
176
|
+
|
|
177
|
+
- **Completion now requires that every level actually be `approved` or
|
|
178
|
+
`skipped`.** "No next group" alone is not enough. If the two ever disagree the
|
|
179
|
+
engine throws `INCOMPLETE_CHAIN` naming the offending levels, rather than
|
|
180
|
+
recording an approval nobody gave. This is a tripwire that should be
|
|
181
|
+
unreachable — it exists because the state it catches was reachable.
|
|
182
|
+
|
|
183
|
+
`override()` is unaffected: bypassing the remaining levels is exactly what an
|
|
184
|
+
administrative override is for.
|
|
185
|
+
|
|
186
|
+
## [3.4.0] - 2026-09-04
|
|
187
|
+
|
|
188
|
+
### Fixed — notifications went to the wrong people
|
|
189
|
+
|
|
190
|
+
`NotificationEvent.recipients` is what an adapter actually delivers to, and it
|
|
191
|
+
was read from the single level matching `instance.currentLevel`. That was wrong
|
|
192
|
+
in two directions at once.
|
|
193
|
+
|
|
194
|
+
- **An approver on any branch of a parallel group except the lowest was never
|
|
195
|
+
notified.** They were not told work had arrived, not told the instance
|
|
196
|
+
advanced — nothing. Combined with the inbox bug fixed in 3.3.0, an
|
|
197
|
+
upper-branch approver had no way at all to learn they owed a decision.
|
|
198
|
+
Recipients are now the union across every open level, matching what
|
|
199
|
+
`getCurrentApprovers()` has returned since 1.0.0.
|
|
200
|
+
|
|
201
|
+
- **An event that already named its own audience had it overwritten.** A comment
|
|
202
|
+
mentioning one person was delivered to the current level's approvers instead
|
|
203
|
+
of the person mentioned; a reminder aimed at those who had not yet voted, and
|
|
204
|
+
a clarification request addressed to the submitter, were redirected the same
|
|
205
|
+
way. An event that names recipients now keeps them.
|
|
206
|
+
|
|
207
|
+
This had been latent since mentions were added in 2.8.0: the payload carried
|
|
208
|
+
the right audience and the adapter boundary discarded it.
|
|
209
|
+
|
|
210
|
+
- **`ApprovalTestKit.fullyApprove()` could not drive a parallel group.** It read
|
|
211
|
+
`instance.currentLevel`, which does not move until the whole group closes, so
|
|
212
|
+
it approved the lowest branch and then re-offered the same decision until the
|
|
213
|
+
engine refused it. It now walks every open level.
|
|
214
|
+
|
|
10
215
|
## [3.3.0] - 2026-09-04
|
|
11
216
|
|
|
12
217
|
### Fixed — an approver on an upper parallel branch had an empty inbox
|
package/README.md
CHANGED
|
@@ -878,6 +878,27 @@ never change who can approve or when the level escalates.
|
|
|
878
878
|
|
|
879
879
|
Requires the escalation scheduler to be running (it is by default).
|
|
880
880
|
|
|
881
|
+
### Reading the approval frontier
|
|
882
|
+
|
|
883
|
+
`instance.openLevels` lists every level currently collecting decisions —
|
|
884
|
+
one entry for a sequential chain, several inside a parallel group, none once the
|
|
885
|
+
instance is terminal:
|
|
886
|
+
|
|
887
|
+
```ts
|
|
888
|
+
const { openLevels, currentLevel } = await engine.getInstance(id);
|
|
889
|
+
// sequential: openLevels [2], currentLevel 2
|
|
890
|
+
// parallel group: openLevels [2, 3], currentLevel 2
|
|
891
|
+
await engine.getOpenLevels(id); // same list, without the whole instance
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
**`currentLevel` names only the lowest open level.** That is the whole frontier
|
|
895
|
+
for a sequential chain and one branch of a parallel group, so use `openLevels`
|
|
896
|
+
to decide who may act, what to notify, or what is overdue. Reading `currentLevel`
|
|
897
|
+
as the frontier was the root cause of six defects fixed across 3.x.
|
|
898
|
+
|
|
899
|
+
Both are maintained by the engine; `currentLevel` is derived from `openLevels`
|
|
900
|
+
and recomputed on every write.
|
|
901
|
+
|
|
881
902
|
### Parallel branch groups
|
|
882
903
|
|
|
883
904
|
Give levels the same `group` name and they activate together, joining before
|
|
@@ -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 { m as ConditionExpression, r as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, C as Comment, e as ApprovalMode, f as ApprovalStatus, L as LevelStatus, b as AuditEntry } from './instance-
|
|
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-CdXMggPK.js';
|
|
2
|
+
import { m as ConditionExpression, r as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, C as Comment, e as ApprovalMode, f as ApprovalStatus, L as LevelStatus, b as AuditEntry } from './instance-uUQh5XW3.js';
|
|
3
|
+
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BTO9FSKy.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-CX7lo4lp.js';
|
|
8
|
+
import { I as IAuditAdapter } from './IAuditAdapter-DkbyFFjb.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-eo6glbpE.js';
|
|
12
12
|
|
|
13
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
14
14
|
templateName: z.ZodString;
|
|
@@ -856,6 +856,17 @@ declare class ApprovalEngine {
|
|
|
856
856
|
queryInstances(filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
|
|
857
857
|
queryInstancesByCursor(filter: InstanceFilter, opts: CursorPaginationOpts): Promise<CursorPaginatedResult<ApprovalInstance>>;
|
|
858
858
|
getHistory(instanceId: string): Promise<AuditEntry[]>;
|
|
859
|
+
/**
|
|
860
|
+
* The levels currently collecting decisions, ascending.
|
|
861
|
+
*
|
|
862
|
+
* The supported way to ask what an instance is waiting on.
|
|
863
|
+
* {@link ApprovalInstance.currentLevel} names only the lowest of these, which
|
|
864
|
+
* is the whole frontier for a sequential chain and one branch of it inside a
|
|
865
|
+
* parallel group.
|
|
866
|
+
*
|
|
867
|
+
* Empty once the instance is terminal.
|
|
868
|
+
*/
|
|
869
|
+
getOpenLevels(instanceId: string): Promise<number[]>;
|
|
859
870
|
getCurrentApprovers(instanceId: string): Promise<string[]>;
|
|
860
871
|
/** Check adapter connectivity and escalation scheduler health. */
|
|
861
872
|
healthCheck(): Promise<HealthResult>;
|
|
@@ -1008,6 +1019,42 @@ declare class ApprovalEngine {
|
|
|
1008
1019
|
* @param opts - `open` activates the level now, computing deadlines from `now`.
|
|
1009
1020
|
*/
|
|
1010
1021
|
private buildLevelInstance;
|
|
1022
|
+
/**
|
|
1023
|
+
* Carry a weighted level's vote weight from one approver to another.
|
|
1024
|
+
*
|
|
1025
|
+
* On a weighted level the weight belongs to the **seat**, not to the person
|
|
1026
|
+
* filling it. Without this, replacing an approver silently dropped their
|
|
1027
|
+
* weight to the default of 1: a CFO carrying weight 3 who was reassigned,
|
|
1028
|
+
* delegated or covered while away left the level unable to reach its
|
|
1029
|
+
* threshold, and the next decision threw "needs a weight of 3 but the
|
|
1030
|
+
* assigned approvers total only 2" — an approval nobody could complete.
|
|
1031
|
+
*
|
|
1032
|
+
* A no-op unless the level actually carries weights and the original had one.
|
|
1033
|
+
*/
|
|
1034
|
+
private inheritWeight;
|
|
1035
|
+
/** Apply {@link inheritWeight} for every out-of-office substitution made on a level. */
|
|
1036
|
+
private inheritWeightsForSubstitutions;
|
|
1037
|
+
/**
|
|
1038
|
+
* Recompute the approval frontier from the levels themselves.
|
|
1039
|
+
*
|
|
1040
|
+
* `openLevels` is every level currently collecting decisions, and
|
|
1041
|
+
* `currentLevel` is the lowest of them. Deriving both from one place means
|
|
1042
|
+
* they cannot drift out of step with the levels — the failure behind six
|
|
1043
|
+
* defects fixed across 3.x, each of which read `currentLevel` as though it
|
|
1044
|
+
* named the whole frontier.
|
|
1045
|
+
*
|
|
1046
|
+
* A terminal instance has no open levels; `currentLevel` keeps its last value
|
|
1047
|
+
* so the audit trail and any UI still show where it finished.
|
|
1048
|
+
*/
|
|
1049
|
+
private syncFrontier;
|
|
1050
|
+
/**
|
|
1051
|
+
* The single write path for an instance.
|
|
1052
|
+
*
|
|
1053
|
+
* Every mutation goes through here so the frontier is recomputed before the
|
|
1054
|
+
* instance is stored. Writing through the adapter directly would let a new
|
|
1055
|
+
* operation persist levels without updating `openLevels`.
|
|
1056
|
+
*/
|
|
1057
|
+
private persistInstance;
|
|
1011
1058
|
/** Level deadline from whichever of days/hours the template configured. */
|
|
1012
1059
|
private levelEscalationDue;
|
|
1013
1060
|
/** First rung of a ladder, sorted by delay, or undefined when there is none. */
|
|
@@ -1133,7 +1180,39 @@ declare class ApprovalEngine {
|
|
|
1133
1180
|
*/
|
|
1134
1181
|
private cancelOrphanedChildren;
|
|
1135
1182
|
private afterDecision;
|
|
1183
|
+
/**
|
|
1184
|
+
* Return every level above `levelNumber` to a clean `waiting` state.
|
|
1185
|
+
*
|
|
1186
|
+
* Used when a rejection sends the chain back: a level that keeps a stale
|
|
1187
|
+
* `approved`/`rejected` status is neither open nor replayable, and the engine
|
|
1188
|
+
* treats "nothing waiting" as "nothing left to do".
|
|
1189
|
+
*/
|
|
1190
|
+
private resetLevelsAfter;
|
|
1191
|
+
/**
|
|
1192
|
+
* Whether an instance has genuinely finished approving.
|
|
1193
|
+
*
|
|
1194
|
+
* "No next group" is not the same as "every level approved": a level left in
|
|
1195
|
+
* a stale state is neither open nor waiting, and treating that as completion
|
|
1196
|
+
* marked instances approved with a branch still undecided. Completion now
|
|
1197
|
+
* requires that every level actually reached a positive terminal state.
|
|
1198
|
+
*/
|
|
1199
|
+
private isFullyApproved;
|
|
1200
|
+
/**
|
|
1201
|
+
* Refuse to complete an instance that has not actually been approved throughout.
|
|
1202
|
+
*
|
|
1203
|
+
* A tripwire, not a routine check: it should be unreachable, and it exists
|
|
1204
|
+
* because the state it catches was reachable and silently produced an
|
|
1205
|
+
* approved document that one branch had rejected and another had never seen.
|
|
1206
|
+
*/
|
|
1207
|
+
private assertFullyApproved;
|
|
1136
1208
|
private findNextLevel;
|
|
1209
|
+
/**
|
|
1210
|
+
* The level to send an instance back to, given the level being rejected.
|
|
1211
|
+
*
|
|
1212
|
+
* Sends the chain back past the **whole group** the rejection came from, not
|
|
1213
|
+
* merely one level. Returning into a sibling branch of the same group would
|
|
1214
|
+
* put the instance back inside the step it was just rejected at.
|
|
1215
|
+
*/
|
|
1137
1216
|
private findPreviousLevel;
|
|
1138
1217
|
private guardBulkSize;
|
|
1139
1218
|
private runAuthorizationPolicy;
|
|
@@ -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 { m as ConditionExpression, r as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, C as Comment, e as ApprovalMode, f as ApprovalStatus, L as LevelStatus, b as AuditEntry } from './instance-
|
|
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-Cjw5zDwQ.cjs';
|
|
2
|
+
import { m as ConditionExpression, r as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, C as Comment, e as ApprovalMode, f as ApprovalStatus, L as LevelStatus, b as AuditEntry } from './instance-uUQh5XW3.cjs';
|
|
3
|
+
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-XbD69gss.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-BBLk8MIz.cjs';
|
|
8
|
+
import { I as IAuditAdapter } from './IAuditAdapter-Dt8qjkQL.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-YJ_08Mof.cjs';
|
|
12
12
|
|
|
13
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
14
14
|
templateName: z.ZodString;
|
|
@@ -856,6 +856,17 @@ declare class ApprovalEngine {
|
|
|
856
856
|
queryInstances(filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
|
|
857
857
|
queryInstancesByCursor(filter: InstanceFilter, opts: CursorPaginationOpts): Promise<CursorPaginatedResult<ApprovalInstance>>;
|
|
858
858
|
getHistory(instanceId: string): Promise<AuditEntry[]>;
|
|
859
|
+
/**
|
|
860
|
+
* The levels currently collecting decisions, ascending.
|
|
861
|
+
*
|
|
862
|
+
* The supported way to ask what an instance is waiting on.
|
|
863
|
+
* {@link ApprovalInstance.currentLevel} names only the lowest of these, which
|
|
864
|
+
* is the whole frontier for a sequential chain and one branch of it inside a
|
|
865
|
+
* parallel group.
|
|
866
|
+
*
|
|
867
|
+
* Empty once the instance is terminal.
|
|
868
|
+
*/
|
|
869
|
+
getOpenLevels(instanceId: string): Promise<number[]>;
|
|
859
870
|
getCurrentApprovers(instanceId: string): Promise<string[]>;
|
|
860
871
|
/** Check adapter connectivity and escalation scheduler health. */
|
|
861
872
|
healthCheck(): Promise<HealthResult>;
|
|
@@ -1008,6 +1019,42 @@ declare class ApprovalEngine {
|
|
|
1008
1019
|
* @param opts - `open` activates the level now, computing deadlines from `now`.
|
|
1009
1020
|
*/
|
|
1010
1021
|
private buildLevelInstance;
|
|
1022
|
+
/**
|
|
1023
|
+
* Carry a weighted level's vote weight from one approver to another.
|
|
1024
|
+
*
|
|
1025
|
+
* On a weighted level the weight belongs to the **seat**, not to the person
|
|
1026
|
+
* filling it. Without this, replacing an approver silently dropped their
|
|
1027
|
+
* weight to the default of 1: a CFO carrying weight 3 who was reassigned,
|
|
1028
|
+
* delegated or covered while away left the level unable to reach its
|
|
1029
|
+
* threshold, and the next decision threw "needs a weight of 3 but the
|
|
1030
|
+
* assigned approvers total only 2" — an approval nobody could complete.
|
|
1031
|
+
*
|
|
1032
|
+
* A no-op unless the level actually carries weights and the original had one.
|
|
1033
|
+
*/
|
|
1034
|
+
private inheritWeight;
|
|
1035
|
+
/** Apply {@link inheritWeight} for every out-of-office substitution made on a level. */
|
|
1036
|
+
private inheritWeightsForSubstitutions;
|
|
1037
|
+
/**
|
|
1038
|
+
* Recompute the approval frontier from the levels themselves.
|
|
1039
|
+
*
|
|
1040
|
+
* `openLevels` is every level currently collecting decisions, and
|
|
1041
|
+
* `currentLevel` is the lowest of them. Deriving both from one place means
|
|
1042
|
+
* they cannot drift out of step with the levels — the failure behind six
|
|
1043
|
+
* defects fixed across 3.x, each of which read `currentLevel` as though it
|
|
1044
|
+
* named the whole frontier.
|
|
1045
|
+
*
|
|
1046
|
+
* A terminal instance has no open levels; `currentLevel` keeps its last value
|
|
1047
|
+
* so the audit trail and any UI still show where it finished.
|
|
1048
|
+
*/
|
|
1049
|
+
private syncFrontier;
|
|
1050
|
+
/**
|
|
1051
|
+
* The single write path for an instance.
|
|
1052
|
+
*
|
|
1053
|
+
* Every mutation goes through here so the frontier is recomputed before the
|
|
1054
|
+
* instance is stored. Writing through the adapter directly would let a new
|
|
1055
|
+
* operation persist levels without updating `openLevels`.
|
|
1056
|
+
*/
|
|
1057
|
+
private persistInstance;
|
|
1011
1058
|
/** Level deadline from whichever of days/hours the template configured. */
|
|
1012
1059
|
private levelEscalationDue;
|
|
1013
1060
|
/** First rung of a ladder, sorted by delay, or undefined when there is none. */
|
|
@@ -1133,7 +1180,39 @@ declare class ApprovalEngine {
|
|
|
1133
1180
|
*/
|
|
1134
1181
|
private cancelOrphanedChildren;
|
|
1135
1182
|
private afterDecision;
|
|
1183
|
+
/**
|
|
1184
|
+
* Return every level above `levelNumber` to a clean `waiting` state.
|
|
1185
|
+
*
|
|
1186
|
+
* Used when a rejection sends the chain back: a level that keeps a stale
|
|
1187
|
+
* `approved`/`rejected` status is neither open nor replayable, and the engine
|
|
1188
|
+
* treats "nothing waiting" as "nothing left to do".
|
|
1189
|
+
*/
|
|
1190
|
+
private resetLevelsAfter;
|
|
1191
|
+
/**
|
|
1192
|
+
* Whether an instance has genuinely finished approving.
|
|
1193
|
+
*
|
|
1194
|
+
* "No next group" is not the same as "every level approved": a level left in
|
|
1195
|
+
* a stale state is neither open nor waiting, and treating that as completion
|
|
1196
|
+
* marked instances approved with a branch still undecided. Completion now
|
|
1197
|
+
* requires that every level actually reached a positive terminal state.
|
|
1198
|
+
*/
|
|
1199
|
+
private isFullyApproved;
|
|
1200
|
+
/**
|
|
1201
|
+
* Refuse to complete an instance that has not actually been approved throughout.
|
|
1202
|
+
*
|
|
1203
|
+
* A tripwire, not a routine check: it should be unreachable, and it exists
|
|
1204
|
+
* because the state it catches was reachable and silently produced an
|
|
1205
|
+
* approved document that one branch had rejected and another had never seen.
|
|
1206
|
+
*/
|
|
1207
|
+
private assertFullyApproved;
|
|
1136
1208
|
private findNextLevel;
|
|
1209
|
+
/**
|
|
1210
|
+
* The level to send an instance back to, given the level being rejected.
|
|
1211
|
+
*
|
|
1212
|
+
* Sends the chain back past the **whole group** the rejection came from, not
|
|
1213
|
+
* merely one level. Returning into a sibling branch of the same group would
|
|
1214
|
+
* put the instance back inside the step it was just rejected at.
|
|
1215
|
+
*/
|
|
1137
1216
|
private findPreviousLevel;
|
|
1138
1217
|
private guardBulkSize;
|
|
1139
1218
|
private runAuthorizationPolicy;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-uUQh5XW3.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, d as ApprovalLevelInstance } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-uUQh5XW3.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 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-uUQh5XW3.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-uUQh5XW3.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-Cjw5zDwQ.cjs';
|
|
2
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-uUQh5XW3.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-CdXMggPK.js';
|
|
2
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-uUQh5XW3.js';
|
|
3
3
|
|
|
4
4
|
declare class MemoryAdapter implements IStorageAdapter {
|
|
5
5
|
private templates;
|
|
@@ -118,6 +118,7 @@ var PostgresAdapter = class {
|
|
|
118
118
|
info_request JSONB,
|
|
119
119
|
attachments JSONB NOT NULL DEFAULT '[]',
|
|
120
120
|
comments JSONB NOT NULL DEFAULT '[]',
|
|
121
|
+
open_levels JSONB NOT NULL DEFAULT '[]',
|
|
121
122
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
122
123
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
123
124
|
PRIMARY KEY (tenant_id, id),
|
|
@@ -163,6 +164,7 @@ var PostgresAdapter = class {
|
|
|
163
164
|
ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS info_request JSONB;
|
|
164
165
|
ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS attachments JSONB NOT NULL DEFAULT '[]';
|
|
165
166
|
ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS comments JSONB NOT NULL DEFAULT '[]';
|
|
167
|
+
ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS open_levels JSONB NOT NULL DEFAULT '[]';
|
|
166
168
|
`);
|
|
167
169
|
}
|
|
168
170
|
/** Enable PostgreSQL Row Level Security for the instances and audit_log tables. */
|
|
@@ -223,8 +225,8 @@ var PostgresAdapter = class {
|
|
|
223
225
|
data, metadata, levels,
|
|
224
226
|
parent_instance_id, expires_at, deadline_action,
|
|
225
227
|
sla_deadline_at, sla_breached_at, template_snapshot, info_request, attachments, comments,
|
|
226
|
-
created_at, updated_at)
|
|
227
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25)
|
|
228
|
+
open_levels, created_at, updated_at)
|
|
229
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26)
|
|
228
230
|
ON CONFLICT (tenant_id, id) DO NOTHING`,
|
|
229
231
|
[
|
|
230
232
|
instance.id,
|
|
@@ -250,6 +252,7 @@ var PostgresAdapter = class {
|
|
|
250
252
|
instance.infoRequest ? JSON.stringify(instance.infoRequest) : null,
|
|
251
253
|
JSON.stringify(instance.attachments ?? []),
|
|
252
254
|
JSON.stringify(instance.comments ?? []),
|
|
255
|
+
JSON.stringify(instance.openLevels ?? []),
|
|
253
256
|
instance.createdAt.toISOString(),
|
|
254
257
|
instance.updatedAt.toISOString()
|
|
255
258
|
]
|
|
@@ -277,7 +280,8 @@ var PostgresAdapter = class {
|
|
|
277
280
|
template_snapshot = $15,
|
|
278
281
|
info_request = $16,
|
|
279
282
|
attachments = $17,
|
|
280
|
-
comments = $18
|
|
283
|
+
comments = $18,
|
|
284
|
+
open_levels = $19
|
|
281
285
|
WHERE tenant_id = $1 AND id = $2 AND version = $3
|
|
282
286
|
RETURNING id`,
|
|
283
287
|
[
|
|
@@ -298,7 +302,8 @@ var PostgresAdapter = class {
|
|
|
298
302
|
instance.templateSnapshot ? JSON.stringify(instance.templateSnapshot) : null,
|
|
299
303
|
instance.infoRequest ? JSON.stringify(instance.infoRequest) : null,
|
|
300
304
|
JSON.stringify(instance.attachments ?? []),
|
|
301
|
-
JSON.stringify(instance.comments ?? [])
|
|
305
|
+
JSON.stringify(instance.comments ?? []),
|
|
306
|
+
JSON.stringify(instance.openLevels ?? [])
|
|
302
307
|
]
|
|
303
308
|
);
|
|
304
309
|
if (result.rowCount === 0) throw new ApprovalConflictError(instance.id);
|
|
@@ -608,6 +613,7 @@ var PostgresAdapter = class {
|
|
|
608
613
|
submittedBy: row["submitted_by"],
|
|
609
614
|
status: row["status"],
|
|
610
615
|
currentLevel: row["current_level"],
|
|
616
|
+
openLevels: row["open_levels"] ?? [],
|
|
611
617
|
version: row["version"],
|
|
612
618
|
idempotencyKey: row["idempotency_key"] ?? void 0,
|
|
613
619
|
data: row["data"],
|