@continuum-dev/runtime 0.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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +403 -0
  3. package/index.d.ts +5 -0
  4. package/index.d.ts.map +1 -0
  5. package/index.js +4 -0
  6. package/lib/context.d.ts +128 -0
  7. package/lib/context.d.ts.map +1 -0
  8. package/lib/context.js +305 -0
  9. package/lib/reconcile.d.ts +17 -0
  10. package/lib/reconcile.d.ts.map +1 -0
  11. package/lib/reconcile.js +76 -0
  12. package/lib/reconciliation/collection-resolver.d.ts +13 -0
  13. package/lib/reconciliation/collection-resolver.d.ts.map +1 -0
  14. package/lib/reconciliation/collection-resolver.js +283 -0
  15. package/lib/reconciliation/component-resolver.d.ts +10 -0
  16. package/lib/reconciliation/component-resolver.d.ts.map +1 -0
  17. package/lib/reconciliation/differ.d.ts +12 -0
  18. package/lib/reconciliation/differ.d.ts.map +1 -0
  19. package/lib/reconciliation/differ.js +102 -0
  20. package/lib/reconciliation/migrator.d.ts +13 -0
  21. package/lib/reconciliation/migrator.d.ts.map +1 -0
  22. package/lib/reconciliation/migrator.js +92 -0
  23. package/lib/reconciliation/node-resolver.d.ts +10 -0
  24. package/lib/reconciliation/node-resolver.d.ts.map +1 -0
  25. package/lib/reconciliation/node-resolver.js +190 -0
  26. package/lib/reconciliation/state-builder.d.ts +13 -0
  27. package/lib/reconciliation/state-builder.d.ts.map +1 -0
  28. package/lib/reconciliation/state-builder.js +211 -0
  29. package/lib/reconciliation/validator.d.ts +14 -0
  30. package/lib/reconciliation/validator.d.ts.map +1 -0
  31. package/lib/reconciliation/validator.js +100 -0
  32. package/lib/reconciliation/view-traversal.d.ts +14 -0
  33. package/lib/reconciliation/view-traversal.d.ts.map +1 -0
  34. package/lib/reconciliation/view-traversal.js +72 -0
  35. package/lib/types.d.ts +153 -0
  36. package/lib/types.d.ts.map +1 -0
  37. package/lib/types.js +1 -0
  38. package/package.json +44 -0
package/lib/context.js ADDED
@@ -0,0 +1,305 @@
1
+ import { ISSUE_SEVERITY, ISSUE_CODES } from '@continuum/contract';
2
+ import { traverseViewNodes } from './reconciliation/view-traversal.js';
3
+ /**
4
+ * Scans a view tree for duplicate ids/keys and traversal-level issues.
5
+ *
6
+ * Useful for preflight checks when validating generated views before running
7
+ * full reconciliation.
8
+ *
9
+ * @param nodes Root nodes from a view definition.
10
+ * @returns Aggregated issues for duplicates and traversal anomalies.
11
+ */
12
+ export function collectDuplicateIssues(nodes) {
13
+ const issues = [];
14
+ const byId = new Map();
15
+ const byKey = new Map();
16
+ const traversal = traverseViewNodes(nodes);
17
+ for (const entry of traversal.visited) {
18
+ const indexedId = entry.nodeId;
19
+ if (byId.has(indexedId)) {
20
+ issues.push({
21
+ severity: ISSUE_SEVERITY.ERROR,
22
+ nodeId: entry.node.id,
23
+ message: `Duplicate node id: ${entry.node.id}`,
24
+ code: ISSUE_CODES.DUPLICATE_NODE_ID,
25
+ });
26
+ }
27
+ byId.set(indexedId, entry.node);
28
+ if (entry.node.key) {
29
+ const indexedKey = toIndexedKey(entry.node.key, entry.parentPath);
30
+ if (byKey.has(indexedKey)) {
31
+ issues.push({
32
+ severity: ISSUE_SEVERITY.WARNING,
33
+ nodeId: entry.node.id,
34
+ message: `Duplicate node key: ${entry.node.key}`,
35
+ code: ISSUE_CODES.DUPLICATE_NODE_KEY,
36
+ });
37
+ }
38
+ byKey.set(indexedKey, entry.node);
39
+ }
40
+ }
41
+ issues.push(...traversal.issues);
42
+ return issues;
43
+ }
44
+ /**
45
+ * Builds lookup maps used by the reconciliation matching pipeline.
46
+ *
47
+ * Context indexes scoped ids and semantic keys so downstream logic can resolve
48
+ * carry/migrate/restore decisions in O(1) lookups.
49
+ *
50
+ * @param newView Target view for this reconciliation cycle.
51
+ * @param priorView Previous view to compare against, if available.
52
+ * @returns Indexed reconciliation context consumed by resolver stages.
53
+ */
54
+ export function buildReconciliationContext(newView, priorView) {
55
+ const newById = new Map();
56
+ const newByKey = new Map();
57
+ const priorById = new Map();
58
+ const priorByKey = new Map();
59
+ const newIdsByRawId = new Map();
60
+ const priorIdsByRawId = new Map();
61
+ const newNodeIds = new WeakMap();
62
+ const priorNodeIds = new WeakMap();
63
+ const issues = [];
64
+ const newTraversal = traverseViewNodes(newView.nodes);
65
+ const priorTraversal = priorView ? traverseViewNodes(priorView.nodes) : null;
66
+ const newKeyCounts = collectKeyCounts(newTraversal.visited);
67
+ const priorKeyCounts = priorTraversal ? collectKeyCounts(priorTraversal.visited) : new Map();
68
+ issues.push(...newTraversal.issues);
69
+ if (priorTraversal) {
70
+ issues.push(...priorTraversal.issues);
71
+ }
72
+ indexNodesByIdAndKey(newTraversal.visited, newById, newByKey, newIdsByRawId, newNodeIds, newKeyCounts, issues);
73
+ if (priorView) {
74
+ indexNodesByIdAndKey(priorTraversal?.visited ?? [], priorById, priorByKey, priorIdsByRawId, priorNodeIds, priorKeyCounts, issues);
75
+ }
76
+ return {
77
+ newView,
78
+ priorView,
79
+ newById,
80
+ newByKey,
81
+ priorById,
82
+ priorByKey,
83
+ newIdsByRawId,
84
+ priorIdsByRawId,
85
+ newNodeIds,
86
+ priorNodeIds,
87
+ newKeyCounts,
88
+ priorKeyCounts,
89
+ issues,
90
+ };
91
+ }
92
+ /**
93
+ * Resolves the best prior-view match for a node in the new view.
94
+ *
95
+ * Matching is attempted in deterministic order: scoped id, semantic key,
96
+ * dot-suffix key fallback, and unique raw-id fallback.
97
+ *
98
+ * @param ctx Reconciliation context with prior/new indexes.
99
+ * @param newNode Node from the new view to resolve against prior view.
100
+ * @returns Matched prior node, or null when no candidate can be resolved.
101
+ */
102
+ export function findPriorNode(ctx, newNode) {
103
+ // 1. Exact Full Path ID
104
+ const newNodeId = ctx.newNodeIds.get(newNode) ?? newNode.id;
105
+ const byId = ctx.priorById.get(newNodeId) ?? ctx.priorById.get(newNode.id);
106
+ if (byId)
107
+ return byId;
108
+ // 2. Exact Key
109
+ if (newNode.key) {
110
+ const byKey = findByKey(ctx.priorByKey, newNode.key, newNodeId);
111
+ if (byKey)
112
+ return byKey;
113
+ // 3. Dot-Notation Suffix Key
114
+ if (newNode.key.includes('.')) {
115
+ const parts = newNode.key.split('.');
116
+ const suffix = parts[parts.length - 1];
117
+ if (suffix) {
118
+ const bySuffixKey = findByKey(ctx.priorByKey, suffix, newNodeId);
119
+ if (bySuffixKey)
120
+ return bySuffixKey;
121
+ }
122
+ }
123
+ }
124
+ // 4. Unique Raw ID Mapping
125
+ const candidates = ctx.priorIdsByRawId.get(newNode.id);
126
+ if (candidates && candidates.length === 1) {
127
+ const uniquePriorId = candidates[0];
128
+ const uniqueNode = ctx.priorById.get(uniquePriorId);
129
+ if (uniqueNode)
130
+ return uniqueNode;
131
+ }
132
+ // 5. Dot-Notation Suffix ID Match (Match newly dot-notated key to old unique raw ID)
133
+ if (newNode.key && newNode.key.includes('.')) {
134
+ const parts = newNode.key.split('.');
135
+ const suffix = parts[parts.length - 1];
136
+ if (suffix) {
137
+ const keyCandidates = ctx.priorIdsByRawId.get(suffix);
138
+ if (keyCandidates && keyCandidates.length === 1) {
139
+ const uniqueNode = ctx.priorById.get(keyCandidates[0]);
140
+ if (uniqueNode)
141
+ return uniqueNode;
142
+ }
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+ /**
148
+ * Builds a value lookup that supports id-based and key-based carry.
149
+ *
150
+ * The returned map contains direct prior ids plus remapped ids when semantic
151
+ * key matches indicate the value should move to a different node id.
152
+ *
153
+ * @param priorData Previous data snapshot.
154
+ * @param ctx Reconciliation context built from prior/new views.
155
+ * @returns Value lookup used by node resolution.
156
+ */
157
+ export function buildPriorValueLookupByIdAndKey(priorData, ctx) {
158
+ const map = new Map();
159
+ for (const [priorId, priorValue] of Object.entries(priorData.values)) {
160
+ map.set(priorId, priorValue);
161
+ const resolvedPriorId = resolveIdFromSnapshot(ctx.priorById, ctx.priorIdsByRawId, priorId);
162
+ if (resolvedPriorId && resolvedPriorId !== priorId) {
163
+ map.set(resolvedPriorId, priorValue);
164
+ }
165
+ const priorNode = resolvedPriorId ? ctx.priorById.get(resolvedPriorId) : undefined;
166
+ if (priorNode?.key) {
167
+ const newNode = findByKey(ctx.newByKey, priorNode.key, resolvedPriorId ?? priorNode.id);
168
+ if (newNode) {
169
+ const newNodeId = ctx.newNodeIds.get(newNode) ?? newNode.id;
170
+ map.set(newNodeId, priorValue);
171
+ }
172
+ }
173
+ }
174
+ return map;
175
+ }
176
+ /**
177
+ * Reports whether a match was resolved by id or by semantic key.
178
+ *
179
+ * This metadata is attached to reconciliation resolution records so consumers
180
+ * can explain why a value was carried/migrated/restored.
181
+ *
182
+ * @param ctx Reconciliation context with id/key indexes.
183
+ * @param newNode Node from the new view.
184
+ * @param priorNode Prior match candidate for the new node.
185
+ * @returns `id`, `key`, or null when no prior node exists.
186
+ */
187
+ export function determineNodeMatchStrategy(ctx, newNode, priorNode) {
188
+ if (!priorNode)
189
+ return null;
190
+ const newNodeId = ctx.newNodeIds.get(newNode) ?? newNode.id;
191
+ if (ctx.priorById.has(newNodeId) || ctx.priorById.has(newNode.id))
192
+ return 'id';
193
+ if (newNode.key && (ctx.priorByKey.has(newNode.key) || findByKey(ctx.priorByKey, newNode.key, newNodeId) === priorNode))
194
+ return 'key';
195
+ if (newNode.key && newNode.key.includes('.')) {
196
+ const parts = newNode.key.split('.');
197
+ const suffix = parts[parts.length - 1];
198
+ if (suffix && findByKey(ctx.priorByKey, suffix, newNodeId) === priorNode)
199
+ return 'key';
200
+ }
201
+ if (priorNode.id === newNode.id)
202
+ return 'id';
203
+ if (newNode.key && newNode.key.includes('.')) {
204
+ const parts = newNode.key.split('.');
205
+ const suffix = parts[parts.length - 1];
206
+ if (suffix && priorNode.id === suffix)
207
+ return 'id';
208
+ }
209
+ return 'id'; // fallback to id for other fuzzy matches
210
+ }
211
+ /**
212
+ * Resolves a snapshot value key to a unique scoped prior node id.
213
+ *
214
+ * @param ctx Reconciliation context with prior-view indexes.
215
+ * @param priorId Snapshot key from `priorData.values`.
216
+ * @returns Scoped prior id when uniquely resolvable; otherwise null.
217
+ */
218
+ export function resolvePriorSnapshotId(ctx, priorId) {
219
+ return resolveIdFromSnapshot(ctx.priorById, ctx.priorIdsByRawId, priorId);
220
+ }
221
+ /**
222
+ * Finds the best new-view node candidate for a given prior node by key.
223
+ *
224
+ * @param ctx Reconciliation context with new-view key indexes.
225
+ * @param priorNode Prior-view node to map forward.
226
+ * @returns Matching new-view node, or null when no key-compatible node exists.
227
+ */
228
+ export function findNewNodeByPriorNode(ctx, priorNode) {
229
+ if (!priorNode.key)
230
+ return null;
231
+ const priorId = ctx.priorNodeIds.get(priorNode) ?? priorNode.id;
232
+ return (findByKey(ctx.newByKey, priorNode.key, priorId) ?? null);
233
+ }
234
+ function collectKeyCounts(traversed) {
235
+ const counts = new Map();
236
+ for (const entry of traversed) {
237
+ if (entry.node.key) {
238
+ counts.set(entry.node.key, (counts.get(entry.node.key) ?? 0) + 1);
239
+ }
240
+ }
241
+ return counts;
242
+ }
243
+ function indexNodesByIdAndKey(traversed, byId, byKey, idsByRawId, nodeIds, keyCounts, issues) {
244
+ for (const entry of traversed) {
245
+ const indexedId = entry.nodeId;
246
+ if (byId.has(indexedId)) {
247
+ issues.push({
248
+ severity: ISSUE_SEVERITY.ERROR,
249
+ nodeId: entry.node.id,
250
+ message: `Duplicate node id: ${entry.node.id}`,
251
+ code: ISSUE_CODES.DUPLICATE_NODE_ID,
252
+ });
253
+ }
254
+ byId.set(indexedId, entry.node);
255
+ nodeIds.set(entry.node, indexedId);
256
+ idsByRawId.set(entry.node.id, [...(idsByRawId.get(entry.node.id) ?? []), indexedId]);
257
+ if (entry.node.key) {
258
+ const indexedKey = toIndexedKey(entry.node.key, entry.parentPath);
259
+ if (byKey.has(indexedKey)) {
260
+ issues.push({
261
+ severity: ISSUE_SEVERITY.WARNING,
262
+ nodeId: entry.node.id,
263
+ message: `Duplicate node key: ${entry.node.key}`,
264
+ code: ISSUE_CODES.DUPLICATE_NODE_KEY,
265
+ });
266
+ }
267
+ byKey.set(indexedKey, entry.node);
268
+ if (!byKey.has(entry.node.key) && (keyCounts.get(entry.node.key) ?? 0) === 1) {
269
+ byKey.set(entry.node.key, entry.node);
270
+ }
271
+ }
272
+ }
273
+ }
274
+ function toIndexedKey(key, parentPath) {
275
+ if (parentPath.length > 0) {
276
+ return `${parentPath}/${key}`;
277
+ }
278
+ return key;
279
+ }
280
+ function resolveIdFromSnapshot(byId, idsByRawId, id) {
281
+ if (byId.has(id)) {
282
+ return id;
283
+ }
284
+ const candidates = idsByRawId.get(id) ?? [];
285
+ if (candidates.length === 1) {
286
+ return candidates[0];
287
+ }
288
+ return null;
289
+ }
290
+ function findByKey(byKey, rawKey, scopedNodeId) {
291
+ const parentPath = parentOf(scopedNodeId);
292
+ if (parentPath) {
293
+ const scoped = byKey.get(`${parentPath}/${rawKey}`);
294
+ if (scoped)
295
+ return scoped;
296
+ }
297
+ return byKey.get(rawKey);
298
+ }
299
+ function parentOf(path) {
300
+ const idx = path.lastIndexOf('/');
301
+ if (idx === -1) {
302
+ return '';
303
+ }
304
+ return path.slice(0, idx);
305
+ }
@@ -0,0 +1,17 @@
1
+ import type { DataSnapshot, ViewDefinition } from '@continuum/contract';
2
+ import type { ReconciliationOptions, ReconciliationResult } from './types.js';
3
+ /**
4
+ * Reconciles user state across view mutations produced by AI or server-side layout changes.
5
+ *
6
+ * This is the main runtime entrypoint. Provide the new view plus optional prior view/data,
7
+ * and the runtime returns a deterministic result with merged state, diffs, issue diagnostics,
8
+ * and per-node resolutions.
9
+ *
10
+ * @param newView Current view definition to reconcile into.
11
+ * @param priorView Previous view definition. Pass null for first render or unknown history.
12
+ * @param priorData Previous data snapshot. Pass null for fresh sessions.
13
+ * @param options Optional reconciliation behavior flags and migration extension hooks.
14
+ * @returns The reconciled state and reconciliation metadata for this transition.
15
+ */
16
+ export declare function reconcile(newView: ViewDefinition, priorView: ViewDefinition | null, priorData: DataSnapshot | null, options?: ReconciliationOptions): ReconciliationResult;
17
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../../../packages/runtime/src/lib/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAA4B,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAElG,OAAO,KAAK,EAA6B,qBAAqB,EAAE,oBAAoB,EAAa,MAAM,YAAY,CAAC;AAOpH;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,cAAc,GAAG,IAAI,EAChC,SAAS,EAAE,YAAY,GAAG,IAAI,EAC9B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAYtB"}
@@ -0,0 +1,76 @@
1
+ import { DATA_RESOLUTIONS } from '@continuum/contract';
2
+ import { buildReconciliationContext, buildPriorValueLookupByIdAndKey } from './context.js';
3
+ import { buildFreshSessionResult, buildBlindCarryResult, assembleReconciliationResult } from './reconciliation/state-builder.js';
4
+ import { resolveAllNodes, detectRemovedNodes } from './reconciliation/node-resolver.js';
5
+ import { restoredDiff, restoredResolution } from './reconciliation/differ.js';
6
+ /**
7
+ * Reconciles user state across view mutations produced by AI or server-side layout changes.
8
+ *
9
+ * This is the main runtime entrypoint. Provide the new view plus optional prior view/data,
10
+ * and the runtime returns a deterministic result with merged state, diffs, issue diagnostics,
11
+ * and per-node resolutions.
12
+ *
13
+ * @param newView Current view definition to reconcile into.
14
+ * @param priorView Previous view definition. Pass null for first render or unknown history.
15
+ * @param priorData Previous data snapshot. Pass null for fresh sessions.
16
+ * @param options Optional reconciliation behavior flags and migration extension hooks.
17
+ * @returns The reconciled state and reconciliation metadata for this transition.
18
+ */
19
+ export function reconcile(newView, priorView, priorData, options = {}) {
20
+ const now = (options.clock ?? Date.now)();
21
+ if (!priorData) {
22
+ return buildFreshSessionResult(newView, now);
23
+ }
24
+ if (!priorView) {
25
+ return buildBlindCarryResult(newView, priorData, now, options);
26
+ }
27
+ return reconcileViewTransition(newView, priorView, priorData, now, options);
28
+ }
29
+ /**
30
+ * After resolving new nodes and detecting removals, check whether any nodes
31
+ * resolved as "added" (no prior match) can be restored from values that were
32
+ * just detached in the same push. This closes the gap where renaming both
33
+ * a node's ID and key in a single pushView would lose data until the next push.
34
+ */
35
+ function restoreFromSamePushDetachments(resolved, removals, ctx) {
36
+ const justDetached = removals.detachedValues;
37
+ if (!justDetached || Object.keys(justDetached).length === 0) {
38
+ return;
39
+ }
40
+ for (let i = 0; i < resolved.resolutions.length; i++) {
41
+ const resolution = resolved.resolutions[i];
42
+ if (resolution.resolution !== DATA_RESOLUTIONS.ADDED) {
43
+ continue;
44
+ }
45
+ const nodeId = resolution.nodeId;
46
+ if (resolved.values[nodeId] !== undefined) {
47
+ continue;
48
+ }
49
+ const newNode = ctx.newById.get(nodeId);
50
+ if (!newNode) {
51
+ continue;
52
+ }
53
+ const detachedKey = newNode.key ?? nodeId;
54
+ const detachedEntry = justDetached[detachedKey];
55
+ if (!detachedEntry) {
56
+ continue;
57
+ }
58
+ if (detachedEntry.previousNodeType !== newNode.type) {
59
+ continue;
60
+ }
61
+ resolved.values[nodeId] = detachedEntry.value;
62
+ resolved.restoredDetachedKeys.add(detachedKey);
63
+ resolved.diffs.push(restoredDiff(nodeId, detachedEntry.value));
64
+ resolved.resolutions[i] = restoredResolution(nodeId, newNode.type, detachedEntry.value);
65
+ }
66
+ }
67
+ function reconcileViewTransition(newView, priorView, priorData, now, options) {
68
+ const ctx = buildReconciliationContext(newView, priorView);
69
+ const priorValues = buildPriorValueLookupByIdAndKey(priorData, ctx);
70
+ const resolved = resolveAllNodes(ctx, priorValues, priorData, now, options);
71
+ const removals = detectRemovedNodes(ctx, priorData, options, now);
72
+ restoreFromSamePushDetachments(resolved, removals, ctx);
73
+ const result = assembleReconciliationResult(resolved, removals, priorData, newView, now);
74
+ result.issues.unshift(...ctx.issues);
75
+ return result;
76
+ }
@@ -0,0 +1,13 @@
1
+ import type { CollectionNode, CollectionNodeState, NodeValue } from '@continuum/contract';
2
+ import type { ReconciliationIssue, ReconciliationOptions } from '../types.js';
3
+ interface CollectionResolutionResult {
4
+ value: NodeValue<CollectionNodeState>;
5
+ issues: ReconciliationIssue[];
6
+ didMigrateItems: boolean;
7
+ }
8
+ export declare function resolveCollectionDefaultValues(node: CollectionNode): NodeValue<CollectionNodeState>;
9
+ export declare function createInitialCollectionValue(node: CollectionNode): NodeValue<CollectionNodeState>;
10
+ export declare function reconcileCollectionValue(priorNode: CollectionNode, newNode: CollectionNode, priorValue: unknown, options: ReconciliationOptions): CollectionResolutionResult;
11
+ export declare function normalizeCollectionValue(node: CollectionNode, value: unknown): NodeValue<CollectionNodeState>;
12
+ export {};
13
+ //# sourceMappingURL=collection-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection-resolver.d.ts","sourceRoot":"","sources":["../../../../../packages/runtime/src/lib/reconciliation/collection-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,SAAS,EAAY,MAAM,qBAAqB,CAAC;AAGpG,OAAO,KAAK,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAS9E,UAAU,0BAA0B;IAClC,KAAK,EAAE,SAAS,CAAC,mBAAmB,CAAC,CAAC;IACtC,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,CAAC,mBAAmB,CAAC,CA4BnG;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,CAAC,mBAAmB,CAAC,CAUjG;AAoFD,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,cAAc,EACzB,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,OAAO,EACnB,OAAO,EAAE,qBAAqB,GAC7B,0BAA0B,CA+H5B;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE,OAAO,GACb,SAAS,CAAC,mBAAmB,CAAC,CAShC"}