@memberjunction/version-history 0.0.1 → 4.1.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/README.md CHANGED
@@ -1,45 +1,270 @@
1
1
  # @memberjunction/version-history
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Version labeling, snapshot capture, diff, and restore for MemberJunction records. Provides point-in-time versioning with full entity dependency graph awareness.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ ## Overview
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
7
+ The `@memberjunction/version-history` package enables developers to create named version labels that capture record state at specific points in time, compare changes between labels, and restore records to previous states while respecting entity dependency ordering.
8
8
 
9
- ## Purpose
9
+ ```mermaid
10
+ graph TD
11
+ A["VersionHistoryEngine<br/>(Facade)"] --> B["LabelManager"]
12
+ A --> C["SnapshotBuilder"]
13
+ A --> D["DiffEngine"]
14
+ A --> E["RestoreEngine"]
15
+ A --> F["DependencyGraphWalker"]
10
16
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@memberjunction/version-history`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
17
+ B --> G["Version Labels"]
18
+ C --> H["Version Label Items"]
19
+ D --> I["DiffResult"]
20
+ E --> J["RestoreResult"]
21
+ F --> K["DependencyNode Tree"]
15
22
 
16
- ## What is OIDC Trusted Publishing?
23
+ style A fill:#2d6a9f,stroke:#1a4971,color:#fff
24
+ style B fill:#7c5295,stroke:#563a6b,color:#fff
25
+ style C fill:#7c5295,stroke:#563a6b,color:#fff
26
+ style D fill:#7c5295,stroke:#563a6b,color:#fff
27
+ style E fill:#7c5295,stroke:#563a6b,color:#fff
28
+ style F fill:#7c5295,stroke:#563a6b,color:#fff
29
+ style G fill:#2d8659,stroke:#1a5c3a,color:#fff
30
+ style H fill:#2d8659,stroke:#1a5c3a,color:#fff
31
+ style I fill:#b8762f,stroke:#8a5722,color:#fff
32
+ style J fill:#b8762f,stroke:#8a5722,color:#fff
33
+ style K fill:#b8762f,stroke:#8a5722,color:#fff
34
+ ```
17
35
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
36
+ ## Installation
19
37
 
20
- ## Setup Instructions
38
+ ```bash
39
+ npm install @memberjunction/version-history
40
+ ```
21
41
 
22
- To properly configure OIDC trusted publishing for this package:
42
+ ## Quick Start
23
43
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
44
+ ```typescript
45
+ import { VersionHistoryEngine } from '@memberjunction/version-history';
28
46
 
29
- ## DO NOT USE THIS PACKAGE
47
+ const engine = new VersionHistoryEngine();
30
48
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
49
+ // Create a label capturing a record and its dependencies
50
+ const { Label, CaptureResult } = await engine.CreateLabel({
51
+ Name: 'Before Refactor',
52
+ Scope: 'Record',
53
+ EntityName: 'AI Prompts',
54
+ RecordKey: promptKey,
55
+ IncludeDependencies: true,
56
+ }, contextUser);
36
57
 
37
- ## More Information
58
+ // Later: see what changed since the label
59
+ const diff = await engine.DiffLabelToCurrentState(Label.ID, contextUser);
38
60
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
61
+ // Restore if needed
62
+ const result = await engine.RestoreToLabel(Label.ID, {}, contextUser);
63
+ ```
42
64
 
43
- ---
65
+ ## Label Scopes
44
66
 
45
- **Maintained for OIDC setup purposes only**
67
+ | Scope | Description | Use Case |
68
+ |-------|-------------|----------|
69
+ | `Record` | Single record and its dependencies | Safe point before editing a specific record |
70
+ | `Entity` | All records of a specific entity | Checkpoint before bulk updates |
71
+ | `System` | All tracked entities | Full system snapshot before a release |
72
+
73
+ ## Architecture
74
+
75
+ ### Sub-Engines
76
+
77
+ | Sub-Engine | Responsibility |
78
+ |-----------|----------------|
79
+ | **LabelManager** | Label CRUD and lifecycle management |
80
+ | **SnapshotBuilder** | Captures record state into label items with batched queries |
81
+ | **DependencyGraphWalker** | Traverses entity relationships to discover dependent records |
82
+ | **DiffEngine** | Compares snapshots between labels or between a label and current state |
83
+ | **RestoreEngine** | Applies labeled state back to records in dependency order |
84
+
85
+ ### Snapshot and Restore Flow
86
+
87
+ ```mermaid
88
+ sequenceDiagram
89
+ participant User
90
+ participant VHE as VersionHistoryEngine
91
+ participant SB as SnapshotBuilder
92
+ participant DGW as DependencyGraphWalker
93
+ participant DB as Database
94
+
95
+ User->>VHE: CreateLabel(params)
96
+ VHE->>SB: CaptureRecord(labelId, entity, key)
97
+ SB->>DGW: WalkDependents(entity, key)
98
+ DGW->>DB: Query related records
99
+ DGW-->>SB: DependencyNode tree
100
+ SB->>DB: Save VersionLabelItem for each record
101
+ SB-->>VHE: CaptureResult
102
+
103
+ Note over User,DB: Time passes, records are modified
104
+
105
+ User->>VHE: RestoreToLabel(labelId, options)
106
+ VHE->>VHE: Create safety Pre-Restore label
107
+ VHE->>DB: Load snapshots, apply in dependency order
108
+ VHE-->>User: RestoreResult
109
+ ```
110
+
111
+ ## Dependency Graph Walker
112
+
113
+ The walker discovers all records that should be included in a version label. It traverses both **reverse relationships** (child records that belong to the root) and **forward references** (records the root or its children point to).
114
+
115
+ ### Two Key Mechanisms
116
+
117
+ #### 1. EntityRelationship-Driven Reverse Walking
118
+
119
+ Instead of scanning every entity for foreign keys that point to the current entity, the walker uses **EntityRelationship metadata** that MemberJunction already maintains. Only explicitly registered children are walked.
120
+
121
+ ```mermaid
122
+ graph TD
123
+ A["AI Agents"] -->|"EntityRelationship"| B["AI Agent Prompts"]
124
+ A -->|"EntityRelationship"| C["AI Agent Actions"]
125
+ A -->|"EntityRelationship"| D["AI Agent Relationships"]
126
+ A -->|"EntityRelationship"| E["AI Agent Models"]
127
+ A -.-x|"NOT walked"| G["Random Table with FK"]
128
+
129
+ style A fill:#2d6a9f,stroke:#1a4971,color:#fff
130
+ style B fill:#2d8659,stroke:#1a5c3a,color:#fff
131
+ style C fill:#2d8659,stroke:#1a5c3a,color:#fff
132
+ style D fill:#2d8659,stroke:#1a5c3a,color:#fff
133
+ style E fill:#2d8659,stroke:#1a5c3a,color:#fff
134
+ style G fill:#b8762f,stroke:#8a5722,color:#fff
135
+ ```
136
+
137
+ #### 2. Ancestor Stack -- Prevents Backtracking
138
+
139
+ The walker maintains a stack of entity type names representing the path from root to the current node. When evaluating any relationship, if the target entity type is already on the ancestor stack, it is skipped. This surgically prevents graph explosion without arbitrary depth limits.
140
+
141
+ ### Walk Algorithm
142
+
143
+ ```mermaid
144
+ flowchart TD
145
+ Start(["walkChildren called"]) --> DepthCheck{"Depth >= MaxDepth?"}
146
+ DepthCheck -->|Yes| Stop(["Return"])
147
+ DepthCheck -->|No| Reverse["Walk Reverse Relationships"]
148
+
149
+ Reverse --> ReverseLoop{"For each EntityRelationship"}
150
+ ReverseLoop -->|Next| AncestorR{"On ancestor stack?"}
151
+ AncestorR -->|Yes| SkipR(["Skip"])
152
+ AncestorR -->|No| LoadChildren["Load child records"]
153
+ LoadChildren --> RecurseR["Recurse walkChildren"]
154
+ RecurseR --> ReverseLoop
155
+
156
+ ReverseLoop -->|Done| Forward["Walk Forward References"]
157
+ Forward --> ForwardLoop{"For each FK field"}
158
+ ForwardLoop -->|Next| SystemCheck{"System FK?"}
159
+ SystemCheck -->|Yes| SkipF(["Skip"])
160
+ SystemCheck -->|No| AncestorF{"On ancestor stack?"}
161
+ AncestorF -->|Yes| SkipF2(["Skip"])
162
+ AncestorF -->|No| LoadTarget["Load referenced record"]
163
+ LoadTarget --> RecurseF["Recurse walkChildren"]
164
+ RecurseF --> ForwardLoop
165
+ ForwardLoop -->|Done| Stop
166
+
167
+ style SkipR fill:#b8762f,stroke:#8a5722,color:#fff
168
+ style SkipF fill:#b8762f,stroke:#8a5722,color:#fff
169
+ style SkipF2 fill:#b8762f,stroke:#8a5722,color:#fff
170
+ style Start fill:#2d6a9f,stroke:#1a4971,color:#fff
171
+ style Stop fill:#2d8659,stroke:#1a5c3a,color:#fff
172
+ ```
173
+
174
+ ### Design Rationale
175
+
176
+ | Concern | Solution |
177
+ |---------|----------|
178
+ | Which children to walk? | **EntityRelationship** -- admin-controlled, CodeGen-maintained |
179
+ | Preventing graph explosion? | **Ancestor stack** -- blocks backtracking to any entity type on current path |
180
+ | Infrastructure FKs (UserID, etc.)? | **System FK skip list** -- regex patterns for known infrastructure fields |
181
+ | Cycle detection? | **Visited set** -- `entityName::recordID` prevents revisiting any record |
182
+ | Sub-agent recursion? | Ancestor stack is **path-based** -- pops on backtrack, allowing re-entry from a different branch |
183
+
184
+ ### Forward FK Skip Patterns
185
+
186
+ The following FK field name patterns are never followed during forward walking, as they reference system infrastructure rather than business data:
187
+
188
+ - `CreatedByUserID`, `UpdatedByUserID`, `UserID`
189
+ - `ContextUserID`, `ModifiedByUserID`
190
+ - `CreatedBy`, `UpdatedBy`
191
+ - `OwnerID`, `OwnerUserID`
192
+ - `AssignedToID`, `AssignedToUserID`
193
+ - `EntityID` (polymorphic reference)
194
+
195
+ ## Snapshot Builder -- Batched Capture
196
+
197
+ When capturing records into a version label, the SnapshotBuilder uses **batched queries** to minimize database round trips:
198
+
199
+ ```mermaid
200
+ sequenceDiagram
201
+ participant SB as SnapshotBuilder
202
+ participant DB as Database
203
+
204
+ Note over SB: Receive flat list of N nodes
205
+
206
+ SB->>SB: Group nodes by EntityID
207
+ loop For each entity group
208
+ SB->>DB: ONE RunView: RecordChanges<br/>WHERE EntityID = X<br/>AND RecordID IN (a, b, c, ...)
209
+ DB-->>SB: Latest changes for all records in group
210
+ end
211
+
212
+ Note over SB: Build lookup map
213
+
214
+ loop For each node without a RecordChange
215
+ SB->>DB: Create synthetic snapshot
216
+ end
217
+
218
+ loop For each node
219
+ SB->>DB: Create VersionLabelItem
220
+ end
221
+ ```
222
+
223
+ **Before batching**: N individual RunView calls.
224
+ **After batching**: ~5-10 RunView calls (one per unique entity type in the graph).
225
+
226
+ ## API Reference
227
+
228
+ ### VersionHistoryEngine
229
+
230
+ ```typescript
231
+ const engine = new VersionHistoryEngine();
232
+
233
+ // Create a label with dependency walking
234
+ const { Label, CaptureResult } = await engine.CreateLabel({
235
+ Name: 'Before Refactor v2',
236
+ Scope: 'Record',
237
+ EntityName: 'AI Agents',
238
+ RecordKey: agentKey,
239
+ IncludeDependencies: true,
240
+ MaxDepth: 10,
241
+ ExcludeEntities: ['AI Agent Runs'],
242
+ }, contextUser);
243
+
244
+ // Diff against current state
245
+ const diff = await engine.DiffLabelToCurrentState(Label.ID, contextUser);
246
+
247
+ // Restore if needed
248
+ const result = await engine.RestoreToLabel(Label.ID, { DryRun: true }, contextUser);
249
+ ```
250
+
251
+ ### WalkOptions
252
+
253
+ | Option | Default | Description |
254
+ |--------|---------|-------------|
255
+ | `MaxDepth` | `10` | Maximum recursion depth |
256
+ | `EntityFilter` | `[]` | Only include these entities (empty = all) |
257
+ | `ExcludeEntities` | `[]` | Skip these entities entirely |
258
+ | `IncludeDeleted` | `false` | Include soft-deleted records |
259
+
260
+ ## Dependencies
261
+
262
+ | Package | Purpose |
263
+ |---------|---------|
264
+ | `@memberjunction/core` | Entity system, metadata, and CompositeKey |
265
+ | `@memberjunction/core-entities` | VersionLabel entity types |
266
+ | `@memberjunction/global` | Global state management |
267
+
268
+ ## License
269
+
270
+ ISC
@@ -0,0 +1,165 @@
1
+ import { CompositeKey, UserInfo } from '@memberjunction/core';
2
+ import { DependencyNode, WalkOptions } from './types.js';
3
+ /**
4
+ * Walks entity relationship graphs to discover records for version labeling.
5
+ *
6
+ * ## Two-direction traversal
7
+ *
8
+ * **Reverse**: Uses the entity's `RelatedEntities` (EntityRelationship metadata)
9
+ * to find child records. This is curated by CodeGen and admins — only explicitly
10
+ * registered One-To-Many relationships are walked.
11
+ *
12
+ * **Forward**: Scans the current entity's own FK fields to find records it
13
+ * references (e.g., AI Agent Prompt → AI Prompt via PromptID). Skips
14
+ * system/infrastructure fields (UserID, CreatedBy, etc.).
15
+ *
16
+ * ## Discovery mode — prevents explosion from forward references
17
+ *
18
+ * Records found via **reverse walk** are "owned children" and get full treatment
19
+ * (both reverse + forward walks). Records found via **forward walk** are
20
+ * "referenced records" and only get forward-only treatment (their own FK refs
21
+ * are followed, but their EntityRelationship children are NOT discovered).
22
+ *
23
+ * This prevents the classic explosion pattern:
24
+ * ```
25
+ * Agent → AgentPrompt → Prompt → [reverse to ALL PromptModels] → Model
26
+ * → [reverse to ALL AgentModels across ALL agents] → explosion!
27
+ * ```
28
+ *
29
+ * With discovery mode:
30
+ * ```
31
+ * Agent → AgentPrompt(reverse,full) → Prompt(forward,forward-only)
32
+ * → Model(forward,forward-only) → STOP (no reverse walk on Model)
33
+ * ```
34
+ *
35
+ * ## Ancestor stack — prevents backtracking
36
+ *
37
+ * A set of entity names on the current path from root to the current node is
38
+ * maintained. When evaluating any relationship, if the target entity type is
39
+ * already on the ancestor stack, it is skipped. This is a secondary safety net
40
+ * that prevents cycles even within full-mode walks.
41
+ *
42
+ * ## Filters
43
+ *
44
+ * - Only walks entities with `TrackRecordChanges === true`
45
+ * - Skips system FK fields via regex patterns (UserID, CreatedBy, etc.)
46
+ * - Respects `ExcludeEntities` and `EntityFilter` options
47
+ * - Uses a global `visited` set for record-level cycle detection
48
+ */
49
+ export declare class DependencyGraphWalker {
50
+ /** Cache: entityID → reverse relationships from EntityRelationship metadata */
51
+ private reverseRelCache;
52
+ /** Cache: entityID → forward FK references from field metadata */
53
+ private forwardRefCache;
54
+ /**
55
+ * Walk from a root record through its relationships, building a tree of
56
+ * all records that should be included in a version label.
57
+ *
58
+ * @param entityName - The starting entity name
59
+ * @param recordKey - The starting record's primary key
60
+ * @param options - Controls depth, filtering, etc.
61
+ * @param contextUser - Server-side user context
62
+ * @returns The root DependencyNode with all descendants populated
63
+ */
64
+ WalkDependents(entityName: string, recordKey: CompositeKey, options: WalkOptions, contextUser: UserInfo): Promise<DependencyNode>;
65
+ /**
66
+ * Flatten a dependency tree into a topologically sorted list.
67
+ * Parents appear before their children, ensuring safe restore ordering.
68
+ */
69
+ FlattenTopological(root: DependencyNode): DependencyNode[];
70
+ /**
71
+ * Recursively discover and attach child nodes for a given parent node.
72
+ *
73
+ * The `discoveryMode` controls which directions are walked:
74
+ * - `'full'`: both reverse (EntityRelationship) and forward (FK refs)
75
+ * - `'forward-only'`: only forward FK refs — no reverse children discovered
76
+ *
77
+ * Records found via reverse walk are recursed with `'full'` mode.
78
+ * Records found via forward walk are recursed with `'forward-only'` mode.
79
+ */
80
+ private walkChildren;
81
+ /**
82
+ * Walk reverse relationships: for each EntityRelationship on the parent
83
+ * entity, load matching child records and recurse into them.
84
+ *
85
+ * Skips any child entity type that is already on the ancestor stack,
86
+ * preventing backtracking (e.g., from AI Prompt back to AI Agent Prompts).
87
+ *
88
+ * Children found via reverse walk are recursed with 'full' discovery mode,
89
+ * since they are "owned children" that may have their own children.
90
+ */
91
+ private walkReverseRelationships;
92
+ /**
93
+ * Walk forward FK references: for each FK field on the parent entity that
94
+ * points to another tracked entity, load the referenced record and recurse.
95
+ *
96
+ * Skips system/infrastructure FKs (UserID, CreatedBy, etc.) and any target
97
+ * entity type already on the ancestor stack.
98
+ *
99
+ * Targets found via forward walk are recursed with 'forward-only' discovery
100
+ * mode — they won't discover their own reverse children, preventing graph
101
+ * explosion from "hub" entities like AI Models.
102
+ */
103
+ private walkForwardReferences;
104
+ /**
105
+ * Discover reverse relationships for an entity using EntityRelationship
106
+ * metadata. Only includes One-To-Many relationships to entities that have
107
+ * TrackRecordChanges enabled.
108
+ *
109
+ * Uses EntityRelationship (already loaded in memory on EntityInfo) rather
110
+ * than scanning all entities' FK fields. This means only relationships
111
+ * that CodeGen or admins have explicitly registered are walked.
112
+ */
113
+ private discoverReverseRelationships;
114
+ /**
115
+ * Discover forward FK references on an entity — fields that point TO other
116
+ * entities. Only includes references to entities with TrackRecordChanges
117
+ * enabled, and skips system/infrastructure FK fields.
118
+ */
119
+ private discoverForwardReferences;
120
+ /**
121
+ * Load child records for a reverse relationship.
122
+ * Queries the child entity where the join field matches the parent's key value.
123
+ */
124
+ private loadChildRecords;
125
+ /**
126
+ * Load a single record's data by primary key.
127
+ */
128
+ private loadRecordData;
129
+ /**
130
+ * Build a DependencyNode from record data, register it in the visited set,
131
+ * and attach it as a child of the parent. Returns null if already visited.
132
+ */
133
+ private registerNode;
134
+ /** Create an empty stats accumulator. */
135
+ private createEmptyStats;
136
+ /** Increment the per-entity record counter. */
137
+ private incrementEntityCount;
138
+ /**
139
+ * Log a detailed summary of the walk: total records, per-entity counts,
140
+ * skips, and suppression stats.
141
+ */
142
+ private logWalkSummary;
143
+ /** Build a unique key for the visited set: "EntityName::recordID" */
144
+ private visitKey;
145
+ /**
146
+ * Determine the parent key field from an EntityRelationship record.
147
+ * Uses EntityKeyField if specified, otherwise falls back to the first PK.
148
+ */
149
+ private resolveParentKeyFieldFromRelationship;
150
+ /**
151
+ * Determine the target key field from a FK field definition.
152
+ * Uses RelatedEntityFieldName if specified, otherwise falls back to the
153
+ * target entity's first PK.
154
+ */
155
+ private resolveParentKeyFieldFromFK;
156
+ /** Check if a field name matches a system/infrastructure FK pattern. */
157
+ private isSystemFKField;
158
+ /** Check if an entity should be skipped based on filter options. */
159
+ private shouldSkipEntity;
160
+ /** Check if an entity has a soft delete field. */
161
+ private entityHasSoftDelete;
162
+ /** Apply defaults to walk options. */
163
+ private resolveDefaults;
164
+ }
165
+ //# sourceMappingURL=DependencyGraphWalker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DependencyGraphWalker.d.ts","sourceRoot":"","sources":["../src/DependencyGraphWalker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA0E,QAAQ,EAAuB,MAAM,sBAAsB,CAAC;AAC3J,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AA6FtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,qBAAa,qBAAqB;IAC9B,+EAA+E;IAC/E,OAAO,CAAC,eAAe,CAA4C;IACnE,kEAAkE;IAClE,OAAO,CAAC,eAAe,CAAyC;IAMhE;;;;;;;;;OASG;IACU,cAAc,CACvB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,WAAW,EACpB,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,cAAc,CAAC;IA2C1B;;;OAGG;IACI,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,EAAE;IAiBjE;;;;;;;;;OASG;YACW,YAAY;IA4B1B;;;;;;;;;OASG;YACW,wBAAwB;IAyCtC;;;;;;;;;;OAUG;YACW,qBAAqB;IAwDnC;;;;;;;;OAQG;IACH,OAAO,CAAC,4BAA4B;IA6BpC;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IA+BjC;;;OAGG;YACW,gBAAgB;IAsC9B;;OAEG;YACW,cAAc;IA0B5B;;;OAGG;IACH,OAAO,CAAC,YAAY;IAqCpB,yCAAyC;IACzC,OAAO,CAAC,gBAAgB;IAUxB,+CAA+C;IAC/C,OAAO,CAAC,oBAAoB;IAK5B;;;OAGG;IACH,OAAO,CAAC,cAAc;IA0BtB,qEAAqE;IACrE,OAAO,CAAC,QAAQ;IAIhB;;;OAGG;IACH,OAAO,CAAC,qCAAqC;IAU7C;;;;OAIG;IACH,OAAO,CAAC,2BAA2B;IAUnC,wEAAwE;IACxE,OAAO,CAAC,eAAe;IAIvB,oEAAoE;IACpE,OAAO,CAAC,gBAAgB;IAUxB,kDAAkD;IAClD,OAAO,CAAC,mBAAmB;IAI3B,sCAAsC;IACtC,OAAO,CAAC,eAAe;CAQ1B"}