hierarchical-approval 2.8.0 → 2.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,42 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  _Nothing yet._
9
9
 
10
+ ## [2.9.0] - 2026-09-04
11
+
12
+ ### Added — `simulate()`
13
+
14
+ - **Dry-runs a document through a template against scripted decisions.**
15
+ `explainChain()` (2.7.0) says what the chain will be; nothing said what
16
+ happens *to* it. Answering "if the CFO rejects at level 3, does it go back to
17
+ the submitter or die?" meant submitting a real approval into real storage and
18
+ cleaning it up afterwards, or reasoning about the state machine by hand.
19
+
20
+ ```ts
21
+ await engine.simulate({
22
+ templateName: 'purchase-order',
23
+ data: { amount: 20000 },
24
+ submittedBy: 'buyer-1',
25
+ decisions: [{ approve: 'mgr-1' }, { reject: 'cfo', reason: 'over budget' }],
26
+ });
27
+ // { finalStatus, levels, transcript, unreachedLevels, incomplete }
28
+ ```
29
+
30
+ - **Nothing escapes the simulation.** It runs against a private in-memory store
31
+ seeded with a copy of the template, with the notification, audit and metrics
32
+ adapters and the authorization policy detached — so a dry run cannot page an
33
+ approver, write somebody's audit log, or move a counter.
34
+
35
+ - **Custom resolvers and approver types are copied across.** A simulation that
36
+ could not resolve the caller's own `dynamic` approvers would answer a
37
+ different question from the one asked.
38
+
39
+ - **A refused decision stops the run and is reported, not thrown** — wrong
40
+ approver, wrong level, already acted. The refusal is usually the answer the
41
+ caller was looking for, and throwing would discard the transcript that
42
+ explains how the run got there.
43
+
44
+ New exports: `SimulationResult`, `SimulationStep`, `SimulatedDecision`.
45
+
10
46
  ## [2.8.0] - 2026-09-04
11
47
 
12
48
  ### Added — comment threads
package/README.md CHANGED
@@ -495,6 +495,35 @@ the current approvers: a remark aimed at somebody should reach them, and one
495
495
  aimed at nobody should not page the whole level. Comments are still written to
496
496
  the audit trail, since the record of who said what belongs there.
497
497
 
498
+ ### Dry-running a workflow
499
+
500
+ `explainChain()` says what the chain will be; `simulate()` says what happens to
501
+ it — "if the CFO rejects at level 3, does it go back to the submitter or die?":
502
+
503
+ ```ts
504
+ const result = await engine.simulate({
505
+ templateName: 'purchase-order',
506
+ data: { amount: 20000 },
507
+ submittedBy: 'buyer-1',
508
+ decisions: [
509
+ { approve: 'mgr-1' },
510
+ { approve: 'fin-1' },
511
+ { reject: 'cfo', reason: 'over budget' },
512
+ ],
513
+ });
514
+ // { finalStatus: 'rejected', levels: [...], transcript: [...], unreachedLevels: [], incomplete: false }
515
+ ```
516
+
517
+ The run executes against a **private in-memory store** seeded with a copy of the
518
+ template, so your storage is untouched and no events reach your notification
519
+ adapters. Custom resolvers and approver types are copied across — a simulation
520
+ that couldn't resolve your own `dynamic` approvers would answer a different
521
+ question from the one you asked.
522
+
523
+ A refused decision (wrong approver, wrong level, already acted) stops the run
524
+ and appears in the transcript with its reason rather than throwing: the refusal
525
+ is usually the answer you were looking for.
526
+
498
527
  ### Why does this chain look like this?
499
528
 
500
529
  `previewApprovalChain()` answers *what* the chain will be. `explainChain()`
@@ -1,5 +1,5 @@
1
1
  import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-C2QxUwbX.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, b as AuditEntry } from './instance-BvOyT00S.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-BvOyT00S.js';
3
3
  import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-DMvA4R-c.js';
4
4
  import { z } from 'zod';
5
5
  import { L as Logger } from './Logger-BplhlU7l.js';
@@ -347,6 +347,45 @@ interface ChainExplanation {
347
347
  skipped: ExplainedSkip[];
348
348
  rules: ExplainedRule[];
349
349
  }
350
+ /** One scripted decision in a {@link ApprovalEngine.simulate} run. */
351
+ type SimulatedDecision = {
352
+ approve: string;
353
+ level?: number;
354
+ comment?: string;
355
+ } | {
356
+ reject: string;
357
+ level?: number;
358
+ reason?: string;
359
+ };
360
+ /** What one scripted decision did. */
361
+ interface SimulationStep {
362
+ /** 1-based position in the script. */
363
+ step: number;
364
+ action: 'approve' | 'reject';
365
+ actorId: string;
366
+ /** The level the decision landed on, when it was accepted. */
367
+ level?: number;
368
+ /** Instance status after the decision. */
369
+ status: ApprovalStatus;
370
+ /** Why the decision was refused, when it was. The run stops at the first refusal. */
371
+ error?: string;
372
+ }
373
+ /** Outcome of a {@link ApprovalEngine.simulate} run. */
374
+ interface SimulationResult {
375
+ finalStatus: ApprovalStatus;
376
+ /** The chain the document would get, in order. */
377
+ levels: Array<{
378
+ level: number;
379
+ name: string;
380
+ status: LevelStatus;
381
+ approvers: string[];
382
+ }>;
383
+ transcript: SimulationStep[];
384
+ /** Levels never reached because the run ended first. */
385
+ unreachedLevels: number[];
386
+ /** True when the script ran out before the approval finished. */
387
+ incomplete: boolean;
388
+ }
350
389
  interface BulkResult {
351
390
  succeeded: ApprovalInstance[];
352
391
  failed: Array<{
@@ -756,6 +795,33 @@ declare class ApprovalEngine {
756
795
  * @param submittedBy - Submitter, used for approver resolution.
757
796
  */
758
797
  explainChain(templateName: string, data: Record<string, unknown>, submittedBy: string): Promise<ChainExplanation>;
798
+ /**
799
+ * Run a document through a template against scripted decisions, without
800
+ * persisting anything.
801
+ *
802
+ * `explainChain()` says what the chain will be; this says what happens to it —
803
+ * "if the CFO rejects at level 3, does it go back to the submitter or die?" —
804
+ * which previously meant submitting a real approval into a real store and
805
+ * cleaning it up afterwards, or reasoning about the state machine by hand.
806
+ *
807
+ * The run executes against a private in-memory store seeded with a copy of
808
+ * the template, so the caller's storage is untouched and no events reach the
809
+ * caller's notification adapters. Custom resolvers and approver types are
810
+ * copied across, because a simulation that could not resolve the caller's own
811
+ * `dynamic` approvers would answer a different question from the one asked.
812
+ *
813
+ * A refused decision — wrong approver, wrong level, already acted — stops the
814
+ * run and is reported in the transcript rather than thrown: the refusal is
815
+ * usually the answer the caller was looking for.
816
+ *
817
+ * @param opts - Template, document data, submitter and the decisions to play.
818
+ */
819
+ simulate(opts: {
820
+ templateName: string;
821
+ data: Record<string, unknown>;
822
+ submittedBy: string;
823
+ decisions?: SimulatedDecision[];
824
+ }): Promise<SimulationResult>;
759
825
  canApprove(instanceId: string, userId: string): Promise<CanApproveResult>;
760
826
  /** Emergency bypass — completes the instance as 'approved', skipping remaining levels. Requires template.allowOverride = true. */
761
827
  override(instanceId: string, raw: OverrideOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
@@ -1043,4 +1109,4 @@ declare class ApprovalEngine {
1043
1109
  private runExternalAudit;
1044
1110
  }
1045
1111
 
1046
- export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type TemplateBundle as F, type TransferResult as G, type HealthResult as H, type IdGeneratorFn as I, businessHoursCalendar as J, defaultIdGenerator as K, toComparableNumber as L, validateConditionExpression as M, weekendCalendar as N, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, TEMPLATE_BUNDLE_VERSION as T, 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 ApproverWorkload as f, type BusinessCalendar as g, type BusinessHoursCalendarOptions as h, type CancelOptions as i, type ChainExplanation as j, type ConditionOperatorFn as k, type CycleTimeStats as l, type ExplainedLevel as m, type ExplainedRule as n, type ExplainedSkip as o, type IdempotencyKeyFn as p, type ImportResult as q, type OutOfOfficeProvider as r, type OverrideOptions as s, type PreviewResult as t, type ProvideInfoOptions as u, type PurgeResult as v, type RejectOptions as w, type RequestInfoOptions as x, type ResubmitOptions as y, type RetryPolicy as z };
1112
+ export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type SimulationResult as F, type SimulationStep as G, type HealthResult as H, type IdGeneratorFn as I, type SubmitOptions as J, type TemplateBundle as K, type TransferResult as L, businessHoursCalendar as M, defaultIdGenerator as N, type OrgProvider as O, type PreviewChainLevel as P, toComparableNumber as Q, type ReassignOptions as R, type SimulatedDecision as S, TEMPLATE_BUNDLE_VERSION as T, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, validateConditionExpression as X, weekendCalendar as Y, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type ApproverWorkload as f, type BusinessCalendar as g, type BusinessHoursCalendarOptions as h, type CancelOptions as i, type ChainExplanation as j, type ConditionOperatorFn as k, type CycleTimeStats as l, type ExplainedLevel as m, type ExplainedRule as n, type ExplainedSkip as o, type IdempotencyKeyFn as p, type ImportResult as q, type OutOfOfficeProvider as r, type OverrideOptions as s, type PreviewResult as t, type ProvideInfoOptions as u, type PurgeResult as v, type RejectOptions as w, type RequestInfoOptions as x, type ResubmitOptions as y, type RetryPolicy as z };
@@ -1,5 +1,5 @@
1
1
  import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-C0qeZgs4.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, b as AuditEntry } from './instance-BvOyT00S.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-BvOyT00S.cjs';
3
3
  import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-DFKvGpOp.cjs';
4
4
  import { z } from 'zod';
5
5
  import { L as Logger } from './Logger-BplhlU7l.cjs';
@@ -347,6 +347,45 @@ interface ChainExplanation {
347
347
  skipped: ExplainedSkip[];
348
348
  rules: ExplainedRule[];
349
349
  }
350
+ /** One scripted decision in a {@link ApprovalEngine.simulate} run. */
351
+ type SimulatedDecision = {
352
+ approve: string;
353
+ level?: number;
354
+ comment?: string;
355
+ } | {
356
+ reject: string;
357
+ level?: number;
358
+ reason?: string;
359
+ };
360
+ /** What one scripted decision did. */
361
+ interface SimulationStep {
362
+ /** 1-based position in the script. */
363
+ step: number;
364
+ action: 'approve' | 'reject';
365
+ actorId: string;
366
+ /** The level the decision landed on, when it was accepted. */
367
+ level?: number;
368
+ /** Instance status after the decision. */
369
+ status: ApprovalStatus;
370
+ /** Why the decision was refused, when it was. The run stops at the first refusal. */
371
+ error?: string;
372
+ }
373
+ /** Outcome of a {@link ApprovalEngine.simulate} run. */
374
+ interface SimulationResult {
375
+ finalStatus: ApprovalStatus;
376
+ /** The chain the document would get, in order. */
377
+ levels: Array<{
378
+ level: number;
379
+ name: string;
380
+ status: LevelStatus;
381
+ approvers: string[];
382
+ }>;
383
+ transcript: SimulationStep[];
384
+ /** Levels never reached because the run ended first. */
385
+ unreachedLevels: number[];
386
+ /** True when the script ran out before the approval finished. */
387
+ incomplete: boolean;
388
+ }
350
389
  interface BulkResult {
351
390
  succeeded: ApprovalInstance[];
352
391
  failed: Array<{
@@ -756,6 +795,33 @@ declare class ApprovalEngine {
756
795
  * @param submittedBy - Submitter, used for approver resolution.
757
796
  */
758
797
  explainChain(templateName: string, data: Record<string, unknown>, submittedBy: string): Promise<ChainExplanation>;
798
+ /**
799
+ * Run a document through a template against scripted decisions, without
800
+ * persisting anything.
801
+ *
802
+ * `explainChain()` says what the chain will be; this says what happens to it —
803
+ * "if the CFO rejects at level 3, does it go back to the submitter or die?" —
804
+ * which previously meant submitting a real approval into a real store and
805
+ * cleaning it up afterwards, or reasoning about the state machine by hand.
806
+ *
807
+ * The run executes against a private in-memory store seeded with a copy of
808
+ * the template, so the caller's storage is untouched and no events reach the
809
+ * caller's notification adapters. Custom resolvers and approver types are
810
+ * copied across, because a simulation that could not resolve the caller's own
811
+ * `dynamic` approvers would answer a different question from the one asked.
812
+ *
813
+ * A refused decision — wrong approver, wrong level, already acted — stops the
814
+ * run and is reported in the transcript rather than thrown: the refusal is
815
+ * usually the answer the caller was looking for.
816
+ *
817
+ * @param opts - Template, document data, submitter and the decisions to play.
818
+ */
819
+ simulate(opts: {
820
+ templateName: string;
821
+ data: Record<string, unknown>;
822
+ submittedBy: string;
823
+ decisions?: SimulatedDecision[];
824
+ }): Promise<SimulationResult>;
759
825
  canApprove(instanceId: string, userId: string): Promise<CanApproveResult>;
760
826
  /** Emergency bypass — completes the instance as 'approved', skipping remaining levels. Requires template.allowOverride = true. */
761
827
  override(instanceId: string, raw: OverrideOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
@@ -1043,4 +1109,4 @@ declare class ApprovalEngine {
1043
1109
  private runExternalAudit;
1044
1110
  }
1045
1111
 
1046
- export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type TemplateBundle as F, type TransferResult as G, type HealthResult as H, type IdGeneratorFn as I, businessHoursCalendar as J, defaultIdGenerator as K, toComparableNumber as L, validateConditionExpression as M, weekendCalendar as N, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, TEMPLATE_BUNDLE_VERSION as T, 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 ApproverWorkload as f, type BusinessCalendar as g, type BusinessHoursCalendarOptions as h, type CancelOptions as i, type ChainExplanation as j, type ConditionOperatorFn as k, type CycleTimeStats as l, type ExplainedLevel as m, type ExplainedRule as n, type ExplainedSkip as o, type IdempotencyKeyFn as p, type ImportResult as q, type OutOfOfficeProvider as r, type OverrideOptions as s, type PreviewResult as t, type ProvideInfoOptions as u, type PurgeResult as v, type RejectOptions as w, type RequestInfoOptions as x, type ResubmitOptions as y, type RetryPolicy as z };
1112
+ export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type SimulationResult as F, type SimulationStep as G, type HealthResult as H, type IdGeneratorFn as I, type SubmitOptions as J, type TemplateBundle as K, type TransferResult as L, businessHoursCalendar as M, defaultIdGenerator as N, type OrgProvider as O, type PreviewChainLevel as P, toComparableNumber as Q, type ReassignOptions as R, type SimulatedDecision as S, TEMPLATE_BUNDLE_VERSION as T, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, validateConditionExpression as X, weekendCalendar as Y, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type ApproverWorkload as f, type BusinessCalendar as g, type BusinessHoursCalendarOptions as h, type CancelOptions as i, type ChainExplanation as j, type ConditionOperatorFn as k, type CycleTimeStats as l, type ExplainedLevel as m, type ExplainedRule as n, type ExplainedSkip as o, type IdempotencyKeyFn as p, type ImportResult as q, type OutOfOfficeProvider as r, type OverrideOptions as s, type PreviewResult as t, type ProvideInfoOptions as u, type PurgeResult as v, type RejectOptions as w, type RequestInfoOptions as x, type ResubmitOptions as y, type RetryPolicy as z };