hierarchical-approval 0.7.0 → 0.9.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 (57) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +77 -0
  3. package/dist/{ApprovalEngine-BGfrrFxl.d.ts → ApprovalEngine-mD-qyxcZ.d.ts} +73 -8
  4. package/dist/{ApprovalEngine-BET-UqQo.d.cts → ApprovalEngine-pfQ2v-J9.d.cts} +73 -8
  5. package/dist/{IAuditAdapter-B_DhuPsU.d.cts → IAuditAdapter-0frLfzRq.d.cts} +1 -1
  6. package/dist/{IAuditAdapter-B3vvU09m.d.ts → IAuditAdapter-nlIK1em0.d.ts} +1 -1
  7. package/dist/{IAuthorizationPolicy-B6JzRNUk.d.cts → IAuthorizationPolicy-DG8vmD5j.d.ts} +2 -2
  8. package/dist/{IAuthorizationPolicy-CZESF3CJ.d.ts → IAuthorizationPolicy-DXEMaoxF.d.cts} +2 -2
  9. package/dist/{IMetricsAdapter-D9PUz4tM.d.cts → IMetricsAdapter-t7QpfA1I.d.cts} +1 -1
  10. package/dist/{IMetricsAdapter-D9PUz4tM.d.ts → IMetricsAdapter-t7QpfA1I.d.ts} +1 -1
  11. package/dist/{INotificationAdapter-DVVmXU6a.d.cts → INotificationAdapter-CFedkDQe.d.cts} +19 -2
  12. package/dist/{INotificationAdapter-BdfVjYa8.d.ts → INotificationAdapter-COeQ5kEv.d.ts} +19 -2
  13. package/dist/{IOperationMiddleware-KGAwT9f-.d.ts → IOperationMiddleware-Bx4_U0ca.d.ts} +1 -1
  14. package/dist/{IOperationMiddleware-CXgXmGUF.d.cts → IOperationMiddleware-CYXTV_TI.d.cts} +1 -1
  15. package/dist/{IStorageAdapter-D-oPxxun.d.ts → IStorageAdapter-ecVYcA0z.d.ts} +1 -1
  16. package/dist/{IStorageAdapter-BibHlgPw.d.cts → IStorageAdapter-qKgtuTY1.d.cts} +1 -1
  17. package/dist/adapters/MemoryAdapter.d.cts +2 -2
  18. package/dist/adapters/MemoryAdapter.d.ts +2 -2
  19. package/dist/adapters/PostgresAdapter.d.cts +2 -2
  20. package/dist/adapters/PostgresAdapter.d.ts +2 -2
  21. package/dist/index.cjs +252 -5
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +9 -9
  24. package/dist/index.d.ts +9 -9
  25. package/dist/index.js +252 -6
  26. package/dist/index.js.map +1 -1
  27. package/dist/{instance-D8D7b07N.d.cts → instance-BxUwmrEi.d.cts} +35 -3
  28. package/dist/{instance-D8D7b07N.d.ts → instance-BxUwmrEi.d.ts} +35 -3
  29. package/dist/nestjs.cjs +251 -5
  30. package/dist/nestjs.cjs.map +1 -1
  31. package/dist/nestjs.d.cts +8 -8
  32. package/dist/nestjs.d.ts +8 -8
  33. package/dist/nestjs.js +251 -5
  34. package/dist/nestjs.js.map +1 -1
  35. package/dist/plugins/audit.d.cts +2 -2
  36. package/dist/plugins/audit.d.ts +2 -2
  37. package/dist/plugins/metrics.cjs +1 -0
  38. package/dist/plugins/metrics.cjs.map +1 -1
  39. package/dist/plugins/metrics.d.cts +1 -1
  40. package/dist/plugins/metrics.d.ts +1 -1
  41. package/dist/plugins/metrics.js +1 -0
  42. package/dist/plugins/metrics.js.map +1 -1
  43. package/dist/plugins/notify.d.cts +2 -2
  44. package/dist/plugins/notify.d.ts +2 -2
  45. package/dist/plugins/resilience.d.cts +3 -3
  46. package/dist/plugins/resilience.d.ts +3 -3
  47. package/dist/plugins/tracing.d.cts +2 -2
  48. package/dist/plugins/tracing.d.ts +2 -2
  49. package/dist/plugins/webhook.d.cts +2 -2
  50. package/dist/plugins/webhook.d.ts +2 -2
  51. package/dist/testing.cjs +251 -5
  52. package/dist/testing.cjs.map +1 -1
  53. package/dist/testing.d.cts +8 -8
  54. package/dist/testing.d.ts +8 -8
  55. package/dist/testing.js +251 -5
  56. package/dist/testing.js.map +1 -1
  57. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,92 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  _Nothing yet._
9
9
 
10
+ ## [0.9.0] - 2026-09-04
11
+
12
+ ### Added — `updateData()`: edit a pending document and recompute its chain
13
+
14
+ - **`engine.updateData(instanceId, opts)` changes an in-flight instance's
15
+ document data and re-evaluates the template's conditions against it.**
16
+ Documents change after submission — a corrected line item, a reclassified
17
+ vendor, a revised amount — and the chain computed at submit time can be wrong
18
+ the moment that happens. The only previous remedy was to cancel and resubmit,
19
+ which discarded every approval already collected along with its audit trail.
20
+
21
+ ```ts
22
+ await engine.updateData(instance.id, {
23
+ updatedBy: 'buyer-1',
24
+ data: { amount: 20000 }, // merged by default; mode: 'replace' swaps wholesale
25
+ reason: 'Corrected line items',
26
+ });
27
+ ```
28
+
29
+ `recomputeChain: false` applies a data correction without touching the chain.
30
+
31
+ - **Decided history is frozen.** Only levels after the current one are
32
+ recomputed. A level that is already approved, or is actively collecting
33
+ decisions, is never removed: a `skipLevels` condition that would drop it is
34
+ ignored, because editing data must not retract an approval that was given. A
35
+ condition that would *insert* a level at or before the current level throws
36
+ `ApprovalValidationError` rather than silently dropping an approval step the
37
+ template says is required.
38
+
39
+ A future level that survives re-evaluation is preserved object-identical, so a
40
+ delegation already arranged on it survives an unrelated edit elsewhere in the
41
+ document.
42
+
43
+ - **Wiring:** emits `approval:data_updated` (`changedFields`, `addedLevels`,
44
+ `removedLevels`), records a `data_updated` audit entry carrying the before and
45
+ after data, increments `approval.data_updated`, and adds `updateData` to the
46
+ authorization-policy operation set — so it runs through the same
47
+ authz/middleware/audit/notification pipeline as every other operation.
48
+
49
+ New exports: `UpdateDataOptions`, `DataUpdatedEvent`.
50
+
51
+ ## [0.8.0] - 2026-09-04
52
+
53
+ ### Added — boolean condition expressions
54
+
55
+ - **A rule's `when` now accepts `all`, `any` and `not` groups, nestable to any
56
+ depth.** Conditions previously supported a single test or an array meaning
57
+ AND, so "escalate when the amount is large **or** the vendor is high-risk"
58
+ could not be written as one rule — it took two rules with duplicated
59
+ `addLevels`, and anything involving negation or a mix of AND and OR had no
60
+ expression at all.
61
+
62
+ ```ts
63
+ when: {
64
+ any: [
65
+ { all: [
66
+ { field: 'amount', operator: '>', value: 1000 },
67
+ { field: 'dept', operator: '==', value: 'engineering' },
68
+ ] },
69
+ { field: 'override', operator: '==', value: true },
70
+ ],
71
+ }
72
+ ```
73
+
74
+ A group sets exactly one combinator. `any` short-circuits on the first child
75
+ that holds. New `ConditionExpression` and `ConditionGroup` types are exported
76
+ from the package root.
77
+
78
+ **Fully backward compatible.** A bare condition still works, and an array is
79
+ shorthand for `all` — exactly what `when: [...]` already meant.
80
+
81
+ - **Condition trees are validated when the template is defined.** `validateTemplate()`
82
+ and `defineTemplate()` now reject an empty `all`/`any`, a group setting more
83
+ than one combinator, a non-array `all`/`any`, and a leaf missing its `field`
84
+ or `operator` — reporting the offending path, e.g.
85
+ `conditions[0].when.any[1].operator`. Previously a malformed condition was
86
+ only discovered at submit time, on a real document.
87
+
88
+ Operator *names* are deliberately not checked at definition time, because
89
+ custom operators can be registered after a template is defined; an unknown
90
+ operator still throws when the condition is evaluated.
91
+
92
+ - **`validateConditionExpression(expression, path)` is exported** for callers
93
+ that build condition trees dynamically and want to check one before handing it
94
+ to a template. It collects every problem rather than throwing on the first.
95
+
10
96
  ## [0.7.0] - 2026-09-04
11
97
 
12
98
  ### Fixed — two condition-evaluation bypasses
package/README.md CHANGED
@@ -305,6 +305,47 @@ conditions: [
305
305
 
306
306
  **Built-in operators:** `>`, `<`, `>=`, `<=`, `==`, `!=`, `in`, `not_in`
307
307
 
308
+ #### Combining conditions
309
+
310
+ A `when` can be a single condition, an array (every element must hold), or a
311
+ boolean group — `all`, `any`, `not` — which nest to any depth:
312
+
313
+ ```ts
314
+ conditions: [
315
+ {
316
+ // (amount > 1000 AND dept is engineering) OR an explicit override flag
317
+ when: {
318
+ any: [
319
+ {
320
+ all: [
321
+ { field: 'amount', operator: '>', value: 1000 },
322
+ { field: 'dept', operator: '==', value: 'engineering' },
323
+ ],
324
+ },
325
+ { field: 'override', operator: '==', value: true },
326
+ ],
327
+ },
328
+ addLevels: [{ level: 3, name: 'CFO', approvers: [{ type: 'user', userId: 'cfo' }], mode: 'any' }],
329
+ },
330
+ {
331
+ // Everything outside the US skips the domestic finance review
332
+ when: { not: { field: 'region', operator: '==', value: 'US' } },
333
+ skipLevels: [2],
334
+ },
335
+ ]
336
+ ```
337
+
338
+ A group sets exactly one of `all` / `any` / `not`. `any` short-circuits on the
339
+ first child that holds. An array is shorthand for `all`, so every template
340
+ written before groups existed keeps working unchanged.
341
+
342
+ Groups are checked when the template is defined, not when a document is
343
+ submitted — `validateTemplate()` and `defineTemplate()` reject an empty `all`,
344
+ a group setting two combinators, or a leaf missing its `field`/`operator`, and
345
+ report the offending path (`conditions[0].when.any[1].operator`). Operator
346
+ *names* are deliberately not checked there, since custom operators may be
347
+ registered after the template is defined.
348
+
308
349
  #### How values are compared
309
350
 
310
351
  `==` and `!=` compare **strictly** — `'100'` does not equal `100`.
@@ -358,6 +399,42 @@ engine.registerConditionOperator('between', (actual, expected) => {
358
399
  });
359
400
  ```
360
401
 
402
+ ### Editing a document mid-flight
403
+
404
+ Documents change after submission — a line item is corrected, a vendor
405
+ reclassified, an amount revised. `updateData()` applies the change and
406
+ re-evaluates the template's conditions, so the chain reflects the corrected
407
+ document without cancelling and resubmitting (which would throw away the
408
+ approvals already collected):
409
+
410
+ ```ts
411
+ const updated = await engine.updateData(instance.id, {
412
+ updatedBy: 'buyer-1',
413
+ data: { amount: 20000 }, // merged into existing data
414
+ reason: 'Corrected line items',
415
+ });
416
+ // amount crossed the 10k threshold -> the CFO level is now in the chain
417
+ ```
418
+
419
+ | Option | Default | Meaning |
420
+ |---|---|---|
421
+ | `data` | required | Fields to apply |
422
+ | `mode` | `'merge'` | `'merge'` keeps untouched keys; `'replace'` swaps the whole object |
423
+ | `reason` | — | Recorded on the audit entry |
424
+ | `recomputeChain` | `true` | Set `false` to correct data without touching the chain |
425
+
426
+ **Decided history is frozen.** Only levels *after* the current one are
427
+ recomputed. A level that is already approved, or is actively collecting
428
+ decisions, is never removed — a `skipLevels` condition that would drop it is
429
+ ignored, because an approval that has been given cannot be retracted by editing
430
+ data. A condition that would *insert* a level at or before the current one
431
+ throws rather than silently dropping a required approval step; cancel and
432
+ resubmit if the chain genuinely must change retroactively.
433
+
434
+ Emits `approval:data_updated` with `changedFields`, `addedLevels` and
435
+ `removedLevels`, and records a `data_updated` audit entry holding the before and
436
+ after data. Only pending instances can be edited.
437
+
361
438
  ---
362
439
 
363
440
  ## Installation and setup
@@ -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-D-oPxxun.js';
2
- import { m as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-D8D7b07N.js';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BdfVjYa8.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-ecVYcA0z.js';
2
+ import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BxUwmrEi.js';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-COeQ5kEv.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-KGAwT9f-.js';
8
- import { I as IAuditAdapter } from './IAuditAdapter-B3vvU09m.js';
9
- import { I as IMetricsAdapter } from './IMetricsAdapter-D9PUz4tM.js';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-Bx4_U0ca.js';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-nlIK1em0.js';
9
+ import { I as IMetricsAdapter } from './IMetricsAdapter-t7QpfA1I.js';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.js';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-CZESF3CJ.js';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-DG8vmD5j.js';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -67,6 +67,16 @@ declare const OverrideOptionsSchema: z.ZodObject<{
67
67
  overriddenBy: z.ZodString;
68
68
  justification: z.ZodString;
69
69
  }, z.core.$strip>;
70
+ declare const UpdateDataOptionsSchema: z.ZodObject<{
71
+ updatedBy: z.ZodString;
72
+ data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
73
+ mode: z.ZodDefault<z.ZodEnum<{
74
+ merge: "merge";
75
+ replace: "replace";
76
+ }>>;
77
+ reason: z.ZodOptional<z.ZodString>;
78
+ recomputeChain: z.ZodDefault<z.ZodBoolean>;
79
+ }, z.core.$strip>;
70
80
  type SubmitOptions = z.infer<typeof SubmitOptionsSchema>;
71
81
  type ApproveOptions = z.infer<typeof ApproveOptionsSchema>;
72
82
  type RejectOptions = z.infer<typeof RejectOptionsSchema>;
@@ -77,6 +87,7 @@ type EscalateOptions = z.infer<typeof EscalateOptionsSchema>;
77
87
  type ResubmitOptions = z.infer<typeof ResubmitOptionsSchema>;
78
88
  type AddCommentOptions = z.infer<typeof AddCommentOptionsSchema>;
79
89
  type OverrideOptions = z.infer<typeof OverrideOptionsSchema>;
90
+ type UpdateDataOptions = z.infer<typeof UpdateDataOptionsSchema>;
80
91
 
81
92
  /**
82
93
  * Computes deadline dates from a number of days. The default engine behaviour
@@ -150,6 +161,24 @@ type ConditionOperatorFn = (actual: unknown, expected: unknown) => boolean;
150
161
  * @returns The numeric value, or `null` when the value is not comparable.
151
162
  */
152
163
  declare function toComparableNumber(value: unknown): number | null;
164
+ /**
165
+ * Statically check a condition expression tree, collecting every problem rather
166
+ * than throwing on the first.
167
+ *
168
+ * Runs at template-definition time so a malformed group is caught while the
169
+ * author is looking at it, instead of at submit time on a real document. The
170
+ * operator check is deliberately deferred: custom operators can be registered
171
+ * after a template is defined, so an unknown name is only an error once the
172
+ * condition is actually evaluated.
173
+ *
174
+ * @param expression - The `when` expression to check.
175
+ * @param path - Field path prefix used in reported errors.
176
+ * @returns One entry per problem found; empty when the tree is well formed.
177
+ */
178
+ declare function validateConditionExpression(expression: ConditionExpression, path: string): Array<{
179
+ field: string;
180
+ message: string;
181
+ }>;
153
182
 
154
183
  interface ValidationResult {
155
184
  valid: boolean;
@@ -355,6 +384,42 @@ declare class ApprovalEngine {
355
384
  reassign(instanceId: string, raw: ReassignOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
356
385
  cancel(instanceId: string, raw: CancelOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
357
386
  escalate(instanceId: string, raw: EscalateOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
387
+ /**
388
+ * Change an instance's document data while it is still pending, and
389
+ * re-evaluate the template's conditions against the new values.
390
+ *
391
+ * Documents change after submission — a line item is corrected, a vendor is
392
+ * reclassified, an amount is revised — and the approval chain that was
393
+ * computed at submit time can be wrong the moment that happens. Without this,
394
+ * the only way to reflect a correction was to cancel and resubmit, losing the
395
+ * approvals already collected and the audit trail with them.
396
+ *
397
+ * **Only the part of the chain that has not been reached is recomputed.**
398
+ * Levels before {@link ApprovalInstance.currentLevel}, and the current level
399
+ * itself, are frozen: an approval that has already been given cannot be
400
+ * retracted by editing data, and a level that is actively collecting
401
+ * decisions is not pulled out from under its approvers. Conditions that would
402
+ * skip such a level are therefore ignored — history cannot be rewritten — and
403
+ * a condition that would *insert* a level at or before the current one throws
404
+ * rather than silently dropping an approval step that should have run.
405
+ *
406
+ * @param instanceId - The pending instance to update.
407
+ * @param raw - Who is updating, the new data, and how to apply it.
408
+ * @param auditCtx - Optional compliance context recorded on the audit entry.
409
+ * @returns The updated instance.
410
+ * @throws ApprovalError if the instance is not pending.
411
+ * @throws ApprovalValidationError if re-evaluation would insert a level at or
412
+ * before the current level, or would leave the chain with no levels.
413
+ */
414
+ updateData(instanceId: string, raw: UpdateDataOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
415
+ /**
416
+ * Recompute the not-yet-reached portion of an instance's level chain against
417
+ * new data, mutating `instance.levels` in place.
418
+ *
419
+ * Levels at or before `currentLevel` are treated as immutable history. See
420
+ * {@link updateData} for why.
421
+ */
422
+ private recomputeFutureChain;
358
423
  /** Add a comment to an instance without approving or rejecting. */
359
424
  addComment(instanceId: string, raw: AddCommentOptions, auditCtx?: AuditContext): Promise<void>;
360
425
  /** Resubmit a rejected instance, creating a new linked instance from level 1. */
@@ -435,4 +500,4 @@ declare class ApprovalEngine {
435
500
  private runExternalAudit;
436
501
  }
437
502
 
438
- export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type OverrideOptions as k, type PreviewResult as l, type RejectOptions as m, type ResubmitOptions as n, type RetryPolicy as o, defaultIdGenerator as p, toComparableNumber as t, weekendCalendar as w };
503
+ export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type OverrideOptions as k, type PreviewResult as l, type RejectOptions as m, type ResubmitOptions as n, type RetryPolicy as o, defaultIdGenerator as p, toComparableNumber as t, validateConditionExpression as v, weekendCalendar as w };
@@ -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-BibHlgPw.cjs';
2
- import { m as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-D8D7b07N.cjs';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-DVVmXU6a.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-qKgtuTY1.cjs';
2
+ import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BxUwmrEi.cjs';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-CFedkDQe.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-CXgXmGUF.cjs';
8
- import { I as IAuditAdapter } from './IAuditAdapter-B_DhuPsU.cjs';
9
- import { I as IMetricsAdapter } from './IMetricsAdapter-D9PUz4tM.cjs';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-CYXTV_TI.cjs';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-0frLfzRq.cjs';
9
+ import { I as IMetricsAdapter } from './IMetricsAdapter-t7QpfA1I.cjs';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.cjs';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-B6JzRNUk.cjs';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-DXEMaoxF.cjs';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -67,6 +67,16 @@ declare const OverrideOptionsSchema: z.ZodObject<{
67
67
  overriddenBy: z.ZodString;
68
68
  justification: z.ZodString;
69
69
  }, z.core.$strip>;
70
+ declare const UpdateDataOptionsSchema: z.ZodObject<{
71
+ updatedBy: z.ZodString;
72
+ data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
73
+ mode: z.ZodDefault<z.ZodEnum<{
74
+ merge: "merge";
75
+ replace: "replace";
76
+ }>>;
77
+ reason: z.ZodOptional<z.ZodString>;
78
+ recomputeChain: z.ZodDefault<z.ZodBoolean>;
79
+ }, z.core.$strip>;
70
80
  type SubmitOptions = z.infer<typeof SubmitOptionsSchema>;
71
81
  type ApproveOptions = z.infer<typeof ApproveOptionsSchema>;
72
82
  type RejectOptions = z.infer<typeof RejectOptionsSchema>;
@@ -77,6 +87,7 @@ type EscalateOptions = z.infer<typeof EscalateOptionsSchema>;
77
87
  type ResubmitOptions = z.infer<typeof ResubmitOptionsSchema>;
78
88
  type AddCommentOptions = z.infer<typeof AddCommentOptionsSchema>;
79
89
  type OverrideOptions = z.infer<typeof OverrideOptionsSchema>;
90
+ type UpdateDataOptions = z.infer<typeof UpdateDataOptionsSchema>;
80
91
 
81
92
  /**
82
93
  * Computes deadline dates from a number of days. The default engine behaviour
@@ -150,6 +161,24 @@ type ConditionOperatorFn = (actual: unknown, expected: unknown) => boolean;
150
161
  * @returns The numeric value, or `null` when the value is not comparable.
151
162
  */
152
163
  declare function toComparableNumber(value: unknown): number | null;
164
+ /**
165
+ * Statically check a condition expression tree, collecting every problem rather
166
+ * than throwing on the first.
167
+ *
168
+ * Runs at template-definition time so a malformed group is caught while the
169
+ * author is looking at it, instead of at submit time on a real document. The
170
+ * operator check is deliberately deferred: custom operators can be registered
171
+ * after a template is defined, so an unknown name is only an error once the
172
+ * condition is actually evaluated.
173
+ *
174
+ * @param expression - The `when` expression to check.
175
+ * @param path - Field path prefix used in reported errors.
176
+ * @returns One entry per problem found; empty when the tree is well formed.
177
+ */
178
+ declare function validateConditionExpression(expression: ConditionExpression, path: string): Array<{
179
+ field: string;
180
+ message: string;
181
+ }>;
153
182
 
154
183
  interface ValidationResult {
155
184
  valid: boolean;
@@ -355,6 +384,42 @@ declare class ApprovalEngine {
355
384
  reassign(instanceId: string, raw: ReassignOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
356
385
  cancel(instanceId: string, raw: CancelOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
357
386
  escalate(instanceId: string, raw: EscalateOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
387
+ /**
388
+ * Change an instance's document data while it is still pending, and
389
+ * re-evaluate the template's conditions against the new values.
390
+ *
391
+ * Documents change after submission — a line item is corrected, a vendor is
392
+ * reclassified, an amount is revised — and the approval chain that was
393
+ * computed at submit time can be wrong the moment that happens. Without this,
394
+ * the only way to reflect a correction was to cancel and resubmit, losing the
395
+ * approvals already collected and the audit trail with them.
396
+ *
397
+ * **Only the part of the chain that has not been reached is recomputed.**
398
+ * Levels before {@link ApprovalInstance.currentLevel}, and the current level
399
+ * itself, are frozen: an approval that has already been given cannot be
400
+ * retracted by editing data, and a level that is actively collecting
401
+ * decisions is not pulled out from under its approvers. Conditions that would
402
+ * skip such a level are therefore ignored — history cannot be rewritten — and
403
+ * a condition that would *insert* a level at or before the current one throws
404
+ * rather than silently dropping an approval step that should have run.
405
+ *
406
+ * @param instanceId - The pending instance to update.
407
+ * @param raw - Who is updating, the new data, and how to apply it.
408
+ * @param auditCtx - Optional compliance context recorded on the audit entry.
409
+ * @returns The updated instance.
410
+ * @throws ApprovalError if the instance is not pending.
411
+ * @throws ApprovalValidationError if re-evaluation would insert a level at or
412
+ * before the current level, or would leave the chain with no levels.
413
+ */
414
+ updateData(instanceId: string, raw: UpdateDataOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
415
+ /**
416
+ * Recompute the not-yet-reached portion of an instance's level chain against
417
+ * new data, mutating `instance.levels` in place.
418
+ *
419
+ * Levels at or before `currentLevel` are treated as immutable history. See
420
+ * {@link updateData} for why.
421
+ */
422
+ private recomputeFutureChain;
358
423
  /** Add a comment to an instance without approving or rejecting. */
359
424
  addComment(instanceId: string, raw: AddCommentOptions, auditCtx?: AuditContext): Promise<void>;
360
425
  /** Resubmit a rejected instance, creating a new linked instance from level 1. */
@@ -435,4 +500,4 @@ declare class ApprovalEngine {
435
500
  private runExternalAudit;
436
501
  }
437
502
 
438
- export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type OverrideOptions as k, type PreviewResult as l, type RejectOptions as m, type ResubmitOptions as n, type RetryPolicy as o, defaultIdGenerator as p, toComparableNumber as t, weekendCalendar as w };
503
+ export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type OverrideOptions as k, type PreviewResult as l, type RejectOptions as m, type ResubmitOptions as n, type RetryPolicy as o, defaultIdGenerator as p, toComparableNumber as t, validateConditionExpression as v, weekendCalendar as w };
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-D8D7b07N.cjs';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-BxUwmrEi.cjs';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-D8D7b07N.js';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-BxUwmrEi.js';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,7 +1,7 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-D8D7b07N.cjs';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BxUwmrEi.js';
2
2
 
3
3
  interface AuthorizationContext {
4
- operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment';
4
+ operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData';
5
5
  actorId: string;
6
6
  instance: Readonly<ApprovalInstance>;
7
7
  level?: Readonly<ApprovalLevelInstance>;
@@ -1,7 +1,7 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-D8D7b07N.js';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BxUwmrEi.cjs';
2
2
 
3
3
  interface AuthorizationContext {
4
- operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment';
4
+ operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData';
5
5
  actorId: string;
6
6
  instance: Readonly<ApprovalInstance>;
7
7
  level?: Readonly<ApprovalLevelInstance>;
@@ -1,4 +1,4 @@
1
- type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
1
+ type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
2
2
  interface IMetricsAdapter {
3
3
  /** Increment a counter. Synchronous — never awaited. */
4
4
  increment(metric: MetricName, labels?: Record<string, string>): void;
@@ -1,4 +1,4 @@
1
- type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
1
+ type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
2
2
  interface IMetricsAdapter {
3
3
  /** Increment a counter. Synchronous — never awaited. */
4
4
  increment(metric: MetricName, labels?: Record<string, string>): void;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-D8D7b07N.cjs';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-BxUwmrEi.cjs';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -62,6 +62,22 @@ interface ExpiredEvent extends ApprovalEvent {
62
62
  interface SlaBreachedEvent extends ApprovalEvent {
63
63
  slaDeadlineAt: Date;
64
64
  }
65
+ /**
66
+ * Emitted when an instance's document data is changed while it is still
67
+ * pending. {@link addedLevels} and {@link removedLevels} describe how the
68
+ * remaining approval chain was recomputed — both empty when the data change
69
+ * did not affect it.
70
+ */
71
+ interface DataUpdatedEvent extends ApprovalEvent {
72
+ updatedBy: string;
73
+ reason?: string;
74
+ /** Field paths whose values differ after the update. */
75
+ changedFields: string[];
76
+ /** Level numbers added to the future chain by re-evaluating conditions. */
77
+ addedLevels: number[];
78
+ /** Level numbers removed from the future chain by re-evaluating conditions. */
79
+ removedLevels: number[];
80
+ }
65
81
  interface ApprovalEventMap {
66
82
  'approval:submitted': SubmittedEvent;
67
83
  'approval:approved': ApprovedEvent;
@@ -76,6 +92,7 @@ interface ApprovalEventMap {
76
92
  'approval:overridden': OverriddenEvent;
77
93
  'approval:expired': ExpiredEvent;
78
94
  'approval:sla_breached': SlaBreachedEvent;
95
+ 'approval:data_updated': DataUpdatedEvent;
79
96
  }
80
97
  type ApprovalEventName = keyof ApprovalEventMap;
81
98
  interface HistoryEntry extends AuditEntry {
@@ -99,4 +116,4 @@ interface INotificationAdapter {
99
116
  notify(event: NotificationEvent): Promise<void>;
100
117
  }
101
118
 
102
- export type { ApprovalEvent as A, CancelledEvent as C, DelegatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, ExpiredEvent as d, RejectedEvent as e, ResubmittedEvent as f, SubmittedEvent as g };
119
+ export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, DelegatedEvent as d, ExpiredEvent as e, RejectedEvent as f, ResubmittedEvent as g, SubmittedEvent as h };
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-D8D7b07N.js';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-BxUwmrEi.js';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -62,6 +62,22 @@ interface ExpiredEvent extends ApprovalEvent {
62
62
  interface SlaBreachedEvent extends ApprovalEvent {
63
63
  slaDeadlineAt: Date;
64
64
  }
65
+ /**
66
+ * Emitted when an instance's document data is changed while it is still
67
+ * pending. {@link addedLevels} and {@link removedLevels} describe how the
68
+ * remaining approval chain was recomputed — both empty when the data change
69
+ * did not affect it.
70
+ */
71
+ interface DataUpdatedEvent extends ApprovalEvent {
72
+ updatedBy: string;
73
+ reason?: string;
74
+ /** Field paths whose values differ after the update. */
75
+ changedFields: string[];
76
+ /** Level numbers added to the future chain by re-evaluating conditions. */
77
+ addedLevels: number[];
78
+ /** Level numbers removed from the future chain by re-evaluating conditions. */
79
+ removedLevels: number[];
80
+ }
65
81
  interface ApprovalEventMap {
66
82
  'approval:submitted': SubmittedEvent;
67
83
  'approval:approved': ApprovedEvent;
@@ -76,6 +92,7 @@ interface ApprovalEventMap {
76
92
  'approval:overridden': OverriddenEvent;
77
93
  'approval:expired': ExpiredEvent;
78
94
  'approval:sla_breached': SlaBreachedEvent;
95
+ 'approval:data_updated': DataUpdatedEvent;
79
96
  }
80
97
  type ApprovalEventName = keyof ApprovalEventMap;
81
98
  interface HistoryEntry extends AuditEntry {
@@ -99,4 +116,4 @@ interface INotificationAdapter {
99
116
  notify(event: NotificationEvent): Promise<void>;
100
117
  }
101
118
 
102
- export type { ApprovalEvent as A, CancelledEvent as C, DelegatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, ExpiredEvent as d, RejectedEvent as e, ResubmittedEvent as f, SubmittedEvent as g };
119
+ export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, DelegatedEvent as d, ExpiredEvent as e, RejectedEvent as f, ResubmittedEvent as g, SubmittedEvent as h };
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance } from './instance-D8D7b07N.js';
1
+ import { a as ApprovalInstance } from './instance-BxUwmrEi.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-D8D7b07N.cjs';
1
+ import { a as ApprovalInstance } from './instance-BxUwmrEi.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-D8D7b07N.js';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BxUwmrEi.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-D8D7b07N.cjs';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BxUwmrEi.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-BibHlgPw.cjs';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-D8D7b07N.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-qKgtuTY1.cjs';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BxUwmrEi.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-D-oPxxun.js';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-D8D7b07N.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-ecVYcA0z.js';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BxUwmrEi.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-BibHlgPw.cjs';
4
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-D8D7b07N.cjs';
3
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-qKgtuTY1.cjs';
4
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BxUwmrEi.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-D-oPxxun.js';
4
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-D8D7b07N.js';
3
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-ecVYcA0z.js';
4
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BxUwmrEi.js';
5
5
 
6
6
  interface PostgresAdapterOptions {
7
7
  connectionString?: string;