hierarchical-approval 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +34 -0
  3. package/dist/{ApprovalEngine-qwImBe9A.d.cts → ApprovalEngine-CdtOJOsu.d.cts} +6 -6
  4. package/dist/{ApprovalEngine-BDnQwKi9.d.ts → ApprovalEngine-DRC-o30z.d.ts} +6 -6
  5. package/dist/{IAuditAdapter-CWdZxS3U.d.cts → IAuditAdapter-B2I3CjaJ.d.cts} +1 -1
  6. package/dist/{IAuditAdapter-99JBU3Db.d.ts → IAuditAdapter-GixC9zS1.d.ts} +1 -1
  7. package/dist/{IAuthorizationPolicy-U-jZrl2F.d.ts → IAuthorizationPolicy-4ge9Gnxj.d.ts} +1 -1
  8. package/dist/{IAuthorizationPolicy-ChZIUJhQ.d.cts → IAuthorizationPolicy-BHDZK9QL.d.cts} +1 -1
  9. package/dist/{INotificationAdapter-BKwhxhe2.d.cts → INotificationAdapter-BKMeGmaG.d.cts} +1 -1
  10. package/dist/{INotificationAdapter-CsSWiCZE.d.ts → INotificationAdapter-BmSeB7FZ.d.ts} +1 -1
  11. package/dist/{IOperationMiddleware-D162RiJs.d.cts → IOperationMiddleware-BLw9Oc-S.d.cts} +1 -1
  12. package/dist/{IOperationMiddleware-CPkGSVfr.d.ts → IOperationMiddleware-BW9t8wPn.d.ts} +1 -1
  13. package/dist/{IStorageAdapter-BZarNxs9.d.cts → IStorageAdapter-CKV_FZFV.d.cts} +1 -1
  14. package/dist/{IStorageAdapter-DrY-rhhF.d.ts → IStorageAdapter-DQJmR6Mt.d.ts} +1 -1
  15. package/dist/adapters/MemoryAdapter.d.cts +2 -2
  16. package/dist/adapters/MemoryAdapter.d.ts +2 -2
  17. package/dist/adapters/PostgresAdapter.d.cts +2 -2
  18. package/dist/adapters/PostgresAdapter.d.ts +2 -2
  19. package/dist/index.cjs +70 -6
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +8 -8
  22. package/dist/index.d.ts +8 -8
  23. package/dist/index.js +70 -6
  24. package/dist/index.js.map +1 -1
  25. package/dist/{instance-DuEgSIIz.d.cts → instance-BsM0DUPf.d.cts} +15 -0
  26. package/dist/{instance-DuEgSIIz.d.ts → instance-BsM0DUPf.d.ts} +15 -0
  27. package/dist/nestjs.cjs +70 -6
  28. package/dist/nestjs.cjs.map +1 -1
  29. package/dist/nestjs.d.cts +7 -7
  30. package/dist/nestjs.d.ts +7 -7
  31. package/dist/nestjs.js +70 -6
  32. package/dist/nestjs.js.map +1 -1
  33. package/dist/plugins/audit.d.cts +2 -2
  34. package/dist/plugins/audit.d.ts +2 -2
  35. package/dist/plugins/notify.d.cts +2 -2
  36. package/dist/plugins/notify.d.ts +2 -2
  37. package/dist/plugins/resilience.d.cts +3 -3
  38. package/dist/plugins/resilience.d.ts +3 -3
  39. package/dist/plugins/tracing.d.cts +2 -2
  40. package/dist/plugins/tracing.d.ts +2 -2
  41. package/dist/plugins/webhook.d.cts +2 -2
  42. package/dist/plugins/webhook.d.ts +2 -2
  43. package/dist/testing.cjs +70 -6
  44. package/dist/testing.cjs.map +1 -1
  45. package/dist/testing.d.cts +7 -7
  46. package/dist/testing.d.ts +7 -7
  47. package/dist/testing.js +70 -6
  48. package/dist/testing.js.map +1 -1
  49. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,46 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  _Nothing yet._
9
9
 
10
+ ## [1.2.0] - 2026-09-04
11
+
12
+ ### Added — template inheritance
13
+
14
+ - **`extends` derives one template from another.** ERP tenants run many
15
+ near-identical workflows — one per region, legal entity or document class —
16
+ that share a spine and differ in a level or two. Each had to be written out in
17
+ full, so a change to the shared part meant editing every copy and hoping none
18
+ were missed.
19
+
20
+ ```ts
21
+ await engine.defineTemplate({
22
+ name: 'PO-EU',
23
+ extends: 'PO-base',
24
+ documentType: 'purchase_order',
25
+ removeLevels: [2],
26
+ levels: [{ level: 3, approvers: [{ type: 'user', userId: 'eu-fin' }] }],
27
+ });
28
+ ```
29
+
30
+ Levels are keyed by number: a child level overrides the base level *field by
31
+ field* (so a derived template can swap just the approvers and inherit name,
32
+ mode and deadlines), child-only levels are appended, and `removeLevels` drops
33
+ inherited ones. `conditions` are replaced wholesale when supplied — merging
34
+ two rule lists would produce a chain neither author intended. `escalation`,
35
+ `slaDeadlineDays` and `allowOverride` are inherited unless the child sets them.
36
+
37
+ - **Resolution happens once, at define time, and the flattened result is what
38
+ gets stored.** Resolving lazily on read would mean editing a base silently
39
+ reshapes every derived template — and every in-flight instance running against
40
+ one — which is exactly the surprise `templateSnapshot` exists to prevent
41
+ elsewhere in the engine. A base that itself extends something is already
42
+ flattened, so chains resolve naturally and cycles cannot form.
43
+
44
+ - **Validation runs on the flattened template**, not the fragment: a derived
45
+ config carrying no levels of its own is valid (it inherits the chain), while
46
+ one whose `removeLevels` strips the chain entirely is rejected at definition
47
+ time. `extends` and `removeLevels` are directives for the call and are not
48
+ persisted.
49
+
10
50
  ## [1.1.0] - 2026-09-04
11
51
 
12
52
  ### Added — approval reminders
package/README.md CHANGED
@@ -399,6 +399,40 @@ engine.registerConditionOperator('between', (actual, expected) => {
399
399
  });
400
400
  ```
401
401
 
402
+ ### Template inheritance
403
+
404
+ ERP tenants run many near-identical workflows — one per region, entity or
405
+ document class — that share a spine and differ in a level or two. `extends`
406
+ derives one template from another:
407
+
408
+ ```ts
409
+ await engine.defineTemplate({
410
+ name: 'PO-EU',
411
+ extends: 'PO-base',
412
+ documentType: 'purchase_order',
413
+ removeLevels: [2], // drop an inherited level
414
+ levels: [
415
+ { level: 3, approvers: [{ type: 'user', userId: 'eu-fin' }] }, // override just the approvers
416
+ { level: 4, name: 'EU Compliance', approvers: [...], mode: 'any' }, // add a new level
417
+ ],
418
+ });
419
+ ```
420
+
421
+ | Field | Merge rule |
422
+ |---|---|
423
+ | `levels` | Keyed by level number: a child level overrides the base level *field by field*, child-only levels are appended, `removeLevels` drops inherited ones |
424
+ | `conditions` | Replaced wholesale when the child supplies them |
425
+ | `escalation`, `slaDeadlineDays`, `allowOverride` | Child value if set, otherwise inherited |
426
+
427
+ **Resolution happens once, at define time, and the flattened result is stored.**
428
+ Editing a base later never silently reshapes a derived template or an instance
429
+ already running against one — the same insulation `templateSnapshot` gives
430
+ in-flight instances. A base that itself extends something is already flattened,
431
+ so chains resolve naturally and cycles cannot form.
432
+
433
+ Validation runs on the *flattened* template, so a derived config that strips its
434
+ whole chain is rejected at definition time.
435
+
402
436
  ### Reminders
403
437
 
404
438
  Escalation reassigns work; a reminder only nudges, which is usually what an
@@ -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-BZarNxs9.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-DuEgSIIz.cjs';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BKwhxhe2.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-CKV_FZFV.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-BsM0DUPf.cjs';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BKMeGmaG.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-D162RiJs.cjs';
8
- import { I as IAuditAdapter } from './IAuditAdapter-CWdZxS3U.cjs';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-BLw9Oc-S.cjs';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-B2I3CjaJ.cjs';
9
9
  import { I as IMetricsAdapter } from './IMetricsAdapter-QvyaKQCc.cjs';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.cjs';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-ChZIUJhQ.cjs';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-BHDZK9QL.cjs';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -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-DrY-rhhF.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-DuEgSIIz.js';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-CsSWiCZE.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-DQJmR6Mt.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-BsM0DUPf.js';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BmSeB7FZ.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-CPkGSVfr.js';
8
- import { I as IAuditAdapter } from './IAuditAdapter-99JBU3Db.js';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-BW9t8wPn.js';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-GixC9zS1.js';
9
9
  import { I as IMetricsAdapter } from './IMetricsAdapter-QvyaKQCc.js';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.js';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-U-jZrl2F.js';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-4ge9Gnxj.js';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-DuEgSIIz.cjs';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-BsM0DUPf.cjs';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-DuEgSIIz.js';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-BsM0DUPf.js';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-DuEgSIIz.js';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BsM0DUPf.js';
2
2
 
3
3
  interface AuthorizationContext {
4
4
  operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData';
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-DuEgSIIz.cjs';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BsM0DUPf.cjs';
2
2
 
3
3
  interface AuthorizationContext {
4
4
  operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData';
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-DuEgSIIz.cjs';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-BsM0DUPf.cjs';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-DuEgSIIz.js';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-BsM0DUPf.js';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance } from './instance-DuEgSIIz.cjs';
1
+ import { a as ApprovalInstance } from './instance-BsM0DUPf.cjs';
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-DuEgSIIz.js';
1
+ import { a as ApprovalInstance } from './instance-BsM0DUPf.js';
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-DuEgSIIz.cjs';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BsM0DUPf.cjs';
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-DuEgSIIz.js';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BsM0DUPf.js';
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-BZarNxs9.cjs';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DuEgSIIz.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-CKV_FZFV.cjs';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BsM0DUPf.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-DrY-rhhF.js';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DuEgSIIz.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-DQJmR6Mt.js';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BsM0DUPf.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-BZarNxs9.cjs';
4
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DuEgSIIz.cjs';
3
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-CKV_FZFV.cjs';
4
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BsM0DUPf.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-DrY-rhhF.js';
4
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DuEgSIIz.js';
3
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-DQJmR6Mt.js';
4
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BsM0DUPf.js';
5
5
 
6
6
  interface PostgresAdapterOptions {
7
7
  connectionString?: string;
package/dist/index.cjs CHANGED
@@ -224,6 +224,66 @@ var TemplateRegistry = class {
224
224
  this.clock = opts?.clock ?? systemClock;
225
225
  this.generateId = opts?.generateId ?? defaultIdGenerator;
226
226
  }
227
+ /**
228
+ * Flatten a template that declares `extends` against its base.
229
+ *
230
+ * Resolution is done once, at define time, and the flattened result is what
231
+ * gets stored. Resolving lazily on read would mean editing a base silently
232
+ * reshapes every derived template — and every in-flight instance running
233
+ * against one — which is exactly the surprise `templateSnapshot` exists to
234
+ * prevent elsewhere in the engine.
235
+ *
236
+ * Merge rules:
237
+ * - **Levels** are keyed by level number. A child level with the same number
238
+ * overrides the base level field by field, so a derived template can change
239
+ * just the approvers and inherit the rest. Child-only levels are appended,
240
+ * and `removeLevels` drops inherited ones.
241
+ * - **conditions** are replaced wholesale when the child supplies them.
242
+ * Concatenating two rule lists would produce a chain neither author
243
+ * intended, and there is no meaningful way to "merge" boolean rules.
244
+ * - **escalation**, **slaDeadlineDays** and **allowOverride** are taken from
245
+ * the child when set, otherwise inherited.
246
+ *
247
+ * A base that itself extends something is already flattened in storage, so
248
+ * chains resolve naturally and cycles cannot form: the base must exist before
249
+ * the child can name it.
250
+ *
251
+ * @param config - The raw config as supplied by the caller.
252
+ * @returns The config with inheritance applied, or unchanged if no `extends`.
253
+ */
254
+ async resolveInheritance(config) {
255
+ if (!config.extends) return config;
256
+ if (config.extends === config.name) {
257
+ throw new ApprovalValidationError(`Template "${config.name}" cannot extend itself.`);
258
+ }
259
+ const base = await this.adapter.getTemplate(this.tenantId, config.extends);
260
+ if (!base) {
261
+ throw new ApprovalTemplateNotFoundError(
262
+ `${config.extends} (referenced by "${config.name}" via extends)`
263
+ );
264
+ }
265
+ const removed = new Set(config.removeLevels ?? []);
266
+ const byLevel = /* @__PURE__ */ new Map();
267
+ for (const lvl of base.levels) {
268
+ if (!removed.has(lvl.level)) byLevel.set(lvl.level, lvl);
269
+ }
270
+ for (const lvl of config.levels ?? []) {
271
+ const inherited = byLevel.get(lvl.level);
272
+ byLevel.set(lvl.level, inherited ? { ...inherited, ...lvl } : lvl);
273
+ }
274
+ const levels = [...byLevel.values()].sort((a, b) => a.level - b.level);
275
+ const resolved = {
276
+ ...config,
277
+ levels,
278
+ conditions: config.conditions ?? base.conditions,
279
+ escalation: config.escalation ?? base.escalation,
280
+ slaDeadlineDays: config.slaDeadlineDays ?? base.slaDeadlineDays,
281
+ allowOverride: config.allowOverride ?? base.allowOverride
282
+ };
283
+ delete resolved.extends;
284
+ delete resolved.removeLevels;
285
+ return resolved;
286
+ }
227
287
  async define(config) {
228
288
  const existing = await this.adapter.getTemplate(this.tenantId, config.name);
229
289
  if (existing) {
@@ -855,9 +915,11 @@ var ApprovalEngine = class _ApprovalEngine {
855
915
  /** Validate a template config without persisting. Synchronous; never throws. */
856
916
  validateTemplate(config) {
857
917
  const errors = [];
858
- if (!config.levels || config.levels.length === 0) {
918
+ const hasOwnLevels = Boolean(config.levels && config.levels.length > 0);
919
+ if (!hasOwnLevels && config.extends === void 0) {
859
920
  errors.push({ field: "levels", message: "Template must have at least one level." });
860
- } else {
921
+ }
922
+ if (hasOwnLevels) {
861
923
  const levelNums = /* @__PURE__ */ new Set();
862
924
  config.levels.forEach((l, i) => {
863
925
  if (levelNums.has(l.level)) {
@@ -986,25 +1048,27 @@ var ApprovalEngine = class _ApprovalEngine {
986
1048
  return { valid: errors.length === 0, errors };
987
1049
  }
988
1050
  async defineTemplate(config) {
989
- const validation = this.validateTemplate(config);
1051
+ const resolved = await this.registry.resolveInheritance(config);
1052
+ const validation = this.validateTemplate(resolved);
990
1053
  if (!validation.valid) {
991
1054
  const first = validation.errors[0];
992
1055
  throw new ApprovalValidationError(
993
1056
  `Invalid template configuration: ${first?.message ?? "unknown error"}`
994
1057
  );
995
1058
  }
996
- return this.registry.define(config);
1059
+ return this.registry.define(resolved);
997
1060
  }
998
1061
  /** Update an existing template, incrementing its version. In-flight instances are protected by their templateSnapshot. */
999
1062
  async updateTemplate(config) {
1000
- const validation = this.validateTemplate(config);
1063
+ const resolved = await this.registry.resolveInheritance(config);
1064
+ const validation = this.validateTemplate(resolved);
1001
1065
  if (!validation.valid) {
1002
1066
  const first = validation.errors[0];
1003
1067
  throw new ApprovalValidationError(
1004
1068
  `Invalid template configuration: ${first?.message ?? "unknown error"}`
1005
1069
  );
1006
1070
  }
1007
- return this.registry.update(config);
1071
+ return this.registry.update(resolved);
1008
1072
  }
1009
1073
  async getTemplate(name) {
1010
1074
  return this.registry.get(name);