@substrat-run/kernel 0.138.0 → 0.139.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 (69) hide show
  1. package/dist/attachment-extractor.d.ts +9 -1
  2. package/dist/attachment-extractor.d.ts.map +1 -1
  3. package/dist/attachment-extractor.js +21 -1
  4. package/dist/attachment-extractor.js.map +1 -1
  5. package/dist/attachment-text.d.ts +64 -0
  6. package/dist/attachment-text.d.ts.map +1 -1
  7. package/dist/attachment-text.js +87 -16
  8. package/dist/attachment-text.js.map +1 -1
  9. package/dist/attribution.d.ts +36 -3
  10. package/dist/attribution.d.ts.map +1 -1
  11. package/dist/attribution.js +21 -8
  12. package/dist/attribution.js.map +1 -1
  13. package/dist/entity-state-reads.d.ts +43 -0
  14. package/dist/entity-state-reads.d.ts.map +1 -0
  15. package/dist/entity-state-reads.js +135 -0
  16. package/dist/entity-state-reads.js.map +1 -0
  17. package/dist/entity-state.d.ts +177 -0
  18. package/dist/entity-state.d.ts.map +1 -0
  19. package/dist/entity-state.js +345 -0
  20. package/dist/entity-state.js.map +1 -0
  21. package/dist/findings.d.ts +143 -0
  22. package/dist/findings.d.ts.map +1 -0
  23. package/dist/findings.js +463 -0
  24. package/dist/findings.js.map +1 -0
  25. package/dist/index.d.ts +13 -7
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +10 -6
  28. package/dist/index.js.map +1 -1
  29. package/dist/list-index.d.ts +25 -3
  30. package/dist/list-index.d.ts.map +1 -1
  31. package/dist/list-index.js +70 -26
  32. package/dist/list-index.js.map +1 -1
  33. package/dist/membership-executor.d.ts +17 -5
  34. package/dist/membership-executor.d.ts.map +1 -1
  35. package/dist/membership-executor.js +26 -11
  36. package/dist/membership-executor.js.map +1 -1
  37. package/dist/module-migrations.d.ts +6 -3
  38. package/dist/module-migrations.d.ts.map +1 -1
  39. package/dist/module-migrations.js +12 -5
  40. package/dist/module-migrations.js.map +1 -1
  41. package/dist/permission-eval.d.ts +41 -0
  42. package/dist/permission-eval.d.ts.map +1 -1
  43. package/dist/permission-eval.js +53 -0
  44. package/dist/permission-eval.js.map +1 -1
  45. package/dist/platform-sweep.d.ts +7 -1
  46. package/dist/platform-sweep.d.ts.map +1 -1
  47. package/dist/platform-sweep.js +12 -0
  48. package/dist/platform-sweep.js.map +1 -1
  49. package/dist/scope-host.d.ts +187 -11
  50. package/dist/scope-host.d.ts.map +1 -1
  51. package/dist/scope-host.js +10 -2
  52. package/dist/scope-host.js.map +1 -1
  53. package/dist/scope-role-admin.d.ts +68 -0
  54. package/dist/scope-role-admin.d.ts.map +1 -0
  55. package/dist/scope-role-admin.js +80 -0
  56. package/dist/scope-role-admin.js.map +1 -0
  57. package/dist/search-index.d.ts +13 -1
  58. package/dist/search-index.d.ts.map +1 -1
  59. package/dist/search-index.js +9 -16
  60. package/dist/search-index.js.map +1 -1
  61. package/dist/spine-guard.d.ts +64 -2
  62. package/dist/spine-guard.d.ts.map +1 -1
  63. package/dist/spine-guard.js +345 -10
  64. package/dist/spine-guard.js.map +1 -1
  65. package/dist/sql-identifier.d.ts +12 -0
  66. package/dist/sql-identifier.d.ts.map +1 -0
  67. package/dist/sql-identifier.js +17 -0
  68. package/dist/sql-identifier.js.map +1 -0
  69. package/package.json +2 -2
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The kernel-composed reads over archivable and trashable entities (#119), written once for
3
+ * both adapters: the view predicate `ctx.page`/`ctx.search` add, and the two trashed readers.
4
+ *
5
+ * Kept apart from `entity-state.ts` because these need the list and search composers, and those
6
+ * need the view predicate from there — one direction of import, not a cycle.
7
+ */
8
+ import { listLimitOf, substratError, } from '@substrat-run/contracts';
9
+ import { entityStateWhere, stateColumnsOf, stateKeyOf } from './entity-state.js';
10
+ import { cursorOf, listQuery, NotListable } from './list-index.js';
11
+ import { NotSearchable, searchLimit, searchMatchExpression, searchQuery, } from './search-index.js';
12
+ /**
13
+ * Refuse the bin on the UNCHECKED readers. `ctx.page` and `ctx.search` check no permission, so
14
+ * the trashed view is not theirs to serve — it reaches them as a string from the wire as easily
15
+ * as from code, and must be refused there rather than answered.
16
+ */
17
+ export function uncheckedView(verb, entityType, view) {
18
+ if (view !== 'trashed')
19
+ return view;
20
+ throw substratError('validation_failed', `${verb}: the trash of '${entityType}' is read with ${verb === 'ctx.page' ? 'ctx.pageTrashed' : 'ctx.searchTrashed'}, ` +
21
+ 'which checks the declared trash key on every row', { errors: [{ path: 'view', message: 'trashed is not a view of this read' }] });
22
+ }
23
+ /**
24
+ * The `src`-aliased predicate a search over `entityType` adds for `view`, or `undefined` when
25
+ * the entity declares no state (every row is active).
26
+ */
27
+ export function searchStateWhere(statePlans, entityType, view) {
28
+ const plan = statePlans.get(entityType);
29
+ return entityStateWhere(entityType, plan && stateColumnsOf(plan), uncheckedView('ctx.search', entityType, view), 'src');
30
+ }
31
+ /**
32
+ * How many binned rows one `ctx.pageTrashed` call reads, at most, looking for rows the caller
33
+ * may see. Each costs a permission check, and a Durable Object has a CPU budget per request, so
34
+ * a bin full of other people's rows cannot be walked without bound inside one call.
35
+ */
36
+ export const TRASH_SCAN_BUDGET = 2_000;
37
+ /**
38
+ * Keep the rows of a trashed read the caller may see in the bin.
39
+ *
40
+ * The declared trash key is checked per row, inside the kernel, so a handler cannot forget it:
41
+ * a member who holds the key on their own lists sees their own bin and nobody else's. Per row
42
+ * because the key is usually entity-narrowed; a scope-wide holder passes every row and pays one
43
+ * cheap check each.
44
+ */
45
+ async function keepVisible(check, key, entityType, rows, idOf) {
46
+ const kept = [];
47
+ for (const row of rows) {
48
+ const entity = { entityType, entityId: idOf(row) };
49
+ if ((await check(key, entity)).allowed)
50
+ kept.push(row);
51
+ }
52
+ return kept;
53
+ }
54
+ export function createTrashedReads(deps) {
55
+ return {
56
+ /**
57
+ * The bin, walked so that **no position of a row the caller may not see ever leaves the
58
+ * kernel.** A cursor is a row's sort value and id, so minting one from a refused row would
59
+ * hand the caller that row's id and timestamp — the very thing the per-row check withholds.
60
+ *
61
+ * So the walk runs internally past refused rows until it has `limit` visible ones or reaches
62
+ * the end, and the cursor is minted from the last VISIBLE row of a FULL page. Past
63
+ * `TRASH_SCAN_BUDGET` rows it stops, and a page that stops short of `limit` — at the end or
64
+ * at the budget — answers the same way either way: the visible rows it found, and no cursor.
65
+ * A cursor on a short page would say "the budget ran out", which is the bin's size again.
66
+ *
67
+ * That answer can be wrong: a page that stops at the budget truncates the walk silently, and
68
+ * a caller whose rows sit more than the budget past the previous one's never sees them. It is chosen over the alternatives on purpose. A cursor
69
+ * would carry a hidden row's position (the leak above), and a refusal — or a count in a
70
+ * message — tells the caller the bin holds more than the budget of rows they cannot see,
71
+ * which is its own disclosure. Only a sealed (authenticated, opaque) continuation can carry
72
+ * the walk on without saying where it is, and a hosted scope holds no key to seal one with
73
+ * (K-45). Until then the walk ends early, silently, and K-45 says so.
74
+ */
75
+ async pageTrashed(entityType, params) {
76
+ const { key } = stateKeyOf(deps.statePlans, 'ctx.pageTrashed', entityType, 'trash');
77
+ const plan = deps.listPlans.get(entityType);
78
+ if (!plan)
79
+ throw new NotListable(entityType);
80
+ if (params.total) {
81
+ throw substratError('validation_failed', 'ctx.pageTrashed: a trashed page carries no total — a count over rows the caller may not see would disclose them');
82
+ }
83
+ const limit = listLimitOf(params.limit);
84
+ const budget = deps.scanBudget ?? TRASH_SCAN_BUDGET;
85
+ const kept = [];
86
+ let cursor = params.cursor;
87
+ let scanned = 0;
88
+ let sortColumn = '';
89
+ let order = 'asc';
90
+ for (;;) {
91
+ const batch = Math.min(limit, budget - scanned);
92
+ const q = listQuery(plan, {
93
+ limit: batch,
94
+ sort: params.sort,
95
+ order: params.order,
96
+ cursor,
97
+ filters: params.filters,
98
+ view: 'trashed',
99
+ });
100
+ ({ sortColumn, order } = q);
101
+ const rows = deps.query(q.sql, q.params);
102
+ for (const row of rows) {
103
+ scanned += 1;
104
+ const entity = { entityType, entityId: String(row[plan.idColumn]) };
105
+ if (!(await deps.check(key, entity)).allowed)
106
+ continue;
107
+ kept.push(row);
108
+ if (kept.length === limit) {
109
+ return { entries: kept, nextCursor: cursorOf(row, sortColumn, plan.idColumn, order, 'trashed') };
110
+ }
111
+ }
112
+ // A short batch is the end of the bin: nothing after it, so no cursor at all.
113
+ if (rows.length < batch)
114
+ return { entries: kept, nextCursor: null };
115
+ // Internal only — never returned while it points at a refused row.
116
+ cursor = cursorOf(rows[rows.length - 1], sortColumn, plan.idColumn, order, 'trashed');
117
+ if (scanned >= budget)
118
+ break;
119
+ }
120
+ // Fewer than `limit` visible rows, and the budget spent: answered exactly as the end of the
121
+ // bin is — whatever was found, and no cursor — deliberately; see above.
122
+ return { entries: kept, nextCursor: null };
123
+ },
124
+ async searchTrashed(entityType, term, options) {
125
+ const { plan: state, key } = stateKeyOf(deps.statePlans, 'ctx.searchTrashed', entityType, 'trash');
126
+ const plan = deps.searchPlans.get(entityType);
127
+ if (!plan)
128
+ throw new NotSearchable(entityType);
129
+ const q = searchQuery(plan, searchMatchExpression(term, plan.tokenizer), searchLimit(options?.limit), entityStateWhere(entityType, stateColumnsOf(state), 'trashed', 'src'));
130
+ const hits = deps.query(q.sql, q.params).map((row) => ({ entityType, id: row.id, rank: row.rank }));
131
+ return keepVisible(deps.check, key, entityType, hits, (hit) => hit.id);
132
+ },
133
+ };
134
+ }
135
+ //# sourceMappingURL=entity-state-reads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-state-reads.js","sourceRoot":"","sources":["../src/entity-state-reads.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Entity archive and trash (#119): the kernel half, written once — both adapters hand
3
+ * `createEntityStateVerbs` the same things, the way `createEntityEdgeVerbs` is shared.
4
+ *
5
+ * ## What the kernel owns, and what it leaves to the vertical
6
+ *
7
+ * | Kernel | Vertical |
8
+ * |---|---|
9
+ * | The two columns on the entity's table, added by a derived migration | Which entities declare `archive` / `trash`, and the keys |
10
+ * | The transitions, their refusals, and the event each one emits | The operations that call the verbs, and their routes |
11
+ * | The declared key, re-checked on the entity by every verb and every trashed read | What an archived row looks like on a screen |
12
+ * | `ctx.page` / `ctx.search` leaving archived and trashed rows out by default | Its own hand-written `SELECT`s — see the gap below |
13
+ *
14
+ * ## The state is two columns
15
+ *
16
+ * `_substrat_archived_at` and `_substrat_trashed_at`, each an ISO instant or NULL. Apart, not
17
+ * one enum, so a trash never forgets an archive: `restore` clears the trash and leaves the
18
+ * archive, and an archived row that went to the bin comes back archived. The visible state is
19
+ * derived (contracts `entity-state.ts`): trashed, else archived, else active.
20
+ *
21
+ * Module code cannot write either column: `ctx.sql` refuses a write naming a `_substrat_*`
22
+ * column (`spine-guard.ts`), so the only way into or out of the trash is a verb that checks
23
+ * the key and emits the event. It can READ them, which is how a vertical's own `SELECT` keeps
24
+ * archived rows out of a screen `ctx.page` does not compose.
25
+ *
26
+ * ## The gap, stated
27
+ *
28
+ * The kernel filters the reads it composes — `ctx.page` and `ctx.search` — and nothing else.
29
+ * A handler's own `SELECT … FROM todo_lists WHERE id = ?` sees an archived or trashed row
30
+ * exactly as before. That is the honest limit of a kernel that does not parse module SQL:
31
+ * a get-by-id asks `ctx.entityState(ref)` and decides, and a hand-written list adds the
32
+ * predicate itself (`entityStateWhere`).
33
+ *
34
+ * ## Trash is not erasure, and not tenant deletion
35
+ *
36
+ * Trash keeps the row and every event about it. Subject erasure (#37) reaches a trashed row
37
+ * exactly as it reaches any other, because the row never moved tables; a tenant's deletion
38
+ * (#36) removes it with everything else. Nothing here extends a retention or shortens one.
39
+ */
40
+ import { type DomainEventInput, type EntityStateDeclaration, type EntityStateName, type Instant, type PermissionKey } from '@substrat-run/contracts';
41
+ import type { OperationContext, ScopedSql, SqlMigration } from './scope-host.js';
42
+ /** A resolved declaration: everything the DDL, the reads and the verbs need. */
43
+ export interface EntityStatePlan {
44
+ readonly moduleId: string;
45
+ readonly entityType: string;
46
+ readonly table: string;
47
+ readonly idColumn: string;
48
+ /** Present when the entity can be archived. */
49
+ readonly archivePermission?: PermissionKey;
50
+ /** Present when the entity can be trashed. */
51
+ readonly trashPermission?: PermissionKey;
52
+ }
53
+ /** The operation's own check — a pass is recorded as one of its authorizations (K-34). */
54
+ export type StateCheck = OperationContext['check'];
55
+ /**
56
+ * Resolve one module's declarations. Refuses rather than skips, for `searchIndexPlans`'s
57
+ * reason: a declaration the author believes is live that silently is not leaves a trashed
58
+ * row in every list with no error anywhere.
59
+ */
60
+ export declare function entityStatePlans(moduleId: string, declarations: readonly EntityStateDeclaration[] | undefined): EntityStatePlan[];
61
+ /**
62
+ * The migrations that add the columns — one per column, so declaring `trash` on an entity
63
+ * that already declared `archive` adds one column and re-runs nothing.
64
+ *
65
+ * **The version is the declaration**, as for search and list indexes. Unlike theirs, this DDL
66
+ * is not drop-then-create: a column holds the state, and dropping it would un-trash every row.
67
+ * So it runs once per column, ever. Withdrawing a declaration leaves the column where it is,
68
+ * holding what it held; re-declaring it finds the migration already applied.
69
+ *
70
+ * Applied after the module's own migrations (the table must exist) and before its list
71
+ * indexes (their partial `WHERE` names these columns) — `moduleMigrations` writes that order.
72
+ */
73
+ export declare function entityStateMigrations(moduleId: string, declarations: readonly EntityStateDeclaration[] | undefined): SqlMigration[];
74
+ /** The prefix of every trigger this module derives — kernel-owned, so the reserved one. */
75
+ export declare const ENTITY_STATE_TRIGGER_PREFIX = "_substrat_state_";
76
+ /**
77
+ * The kernel's authorization for ONE move (#119, Codex r2): the row `ctx.archive` & co. write
78
+ * just before their `UPDATE` and delete just after it, in the same transaction. The update
79
+ * trigger below refuses any change to the columns that has no such row — so the only writer the
80
+ * columns admit is the one that can write a `_substrat_*` TABLE, which `ctx.sql` refuses by name
81
+ * (a far simpler reading than finding an assignment target in an expression).
82
+ *
83
+ * Never holds a row between operations: written and removed inside one move, and a move that
84
+ * throws rolls back with its operation. Shared by both adapters' `KERNEL_DDL`, like the other
85
+ * kernel-owned spine tables, so the two cannot part company.
86
+ */
87
+ export declare const ENTITY_STATE_MOVES_TABLE = "_substrat_state_moves";
88
+ export declare const ENTITY_STATE_MOVES_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_state_moves (\n entity_type TEXT NOT NULL,\n entity_id TEXT NOT NULL,\n PRIMARY KEY (entity_type, entity_id)\n );\n";
89
+ /**
90
+ * The invariant below the column guard, as two triggers on the entity's table.
91
+ *
92
+ * `ctx.sql` refuses the writes that would set the columns (`assertNoReservedColumnWrite`), but
93
+ * that is a reading of SQL text, and a reading can miss a form — it did, twice. These hold
94
+ * whatever the text says:
95
+ *
96
+ * - **born** — `BEFORE INSERT`: a row is never inserted archived or trashed. Every row enters
97
+ * active.
98
+ * - **moved** — `BEFORE UPDATE OF` the columns: a change to either aborts unless the kernel's
99
+ * authorization row for this entity exists (`ENTITY_STATE_MOVES_TABLE`), which only a move
100
+ * writes. An `UPDATE` that does not name the columns does not fire it.
101
+ *
102
+ * Drop-then-create, like the search triggers, and re-run after a dump load: a load drops the
103
+ * table and with it the triggers, then inserts the rows first — a restored binned row is
104
+ * legitimately born trashed, so the triggers are put back only after them.
105
+ */
106
+ export declare function entityStateTriggerDdl(plan: EntityStatePlan): string;
107
+ /**
108
+ * Add one module's plans to a scope's registry, refusing a second declaration of a type — the
109
+ * table is one module's, and two answers to "who may trash it" is no answer — and a key the
110
+ * module does not declare. The entity is the module's, so its keys are too: a key nobody
111
+ * declared reaches no role, no grant shape and no `PERMISSIONS.md`, and would make the verb
112
+ * look gated while nobody could ever pass it.
113
+ */
114
+ export declare function addStatePlans(byType: Map<string, EntityStatePlan>, moduleId: string, declarations: readonly EntityStateDeclaration[] | undefined, declaredKeys: readonly {
115
+ readonly key: string;
116
+ }[]): void;
117
+ /** The tables whose rows carry the columns, lowercased as SQLite resolves a name — `guardSpine`'s input. */
118
+ export declare const statefulTablesOf: (plans: ReadonlyMap<string, EntityStatePlan>) => ReadonlySet<string>;
119
+ /** The columns a plan has, for code that only needs to know which views exist. */
120
+ export interface StateColumns {
121
+ readonly archive: boolean;
122
+ readonly trash: boolean;
123
+ }
124
+ export declare const stateColumnsOf: (plan: EntityStatePlan) => StateColumns;
125
+ /** The views an entity with these columns has — `active` always, then one per column. */
126
+ export declare function viewsOf(columns: StateColumns): EntityStateName[];
127
+ /**
128
+ * The predicate selecting one view's rows, over bare column names or `alias.`-qualified ones.
129
+ *
130
+ * **Spelled one way, everywhere.** A partial index is used only when the query's `WHERE`
131
+ * contains the index's own terms, so the list index DDL and the list query both come from
132
+ * here — a reworded copy would plan a scan in production and pass every test.
133
+ *
134
+ * `columns` absent is an entity that declares no state: every row is active, so there is no
135
+ * predicate. A view the entity does not have is refused rather than answered empty — asking
136
+ * an entity with no trash for its trashed rows is a wiring mistake, and an empty page would
137
+ * read as an empty bin.
138
+ */
139
+ export declare function entityStateWhere(entityType: string, columns: StateColumns, view: EntityStateName, alias?: string): string;
140
+ export declare function entityStateWhere(entityType: string, columns: StateColumns | undefined, view: EntityStateName | undefined, alias?: string): string | undefined;
141
+ /**
142
+ * The plan for `entityType`, and the key one of its states is gated on — or `validation_failed`
143
+ * naming what the entity does not declare. Shared by the verbs and the trashed readers, so
144
+ * "declares no trash" is one sentence.
145
+ */
146
+ export declare function stateKeyOf(plans: ReadonlyMap<string, EntityStatePlan>, verb: string, entityType: string, which: 'archive' | 'trash'): {
147
+ plan: EntityStatePlan;
148
+ key: PermissionKey;
149
+ };
150
+ /** What the verbs need from the adapter. */
151
+ export interface EntityStateDeps {
152
+ /** RAW access inside the operation's own transaction — the guarded `ctx.sql` refuses these columns. */
153
+ sql: ScopedSql;
154
+ /** entity type → its plan, for every registered module. */
155
+ plans: ReadonlyMap<string, EntityStatePlan>;
156
+ /** The operation's instant — what each column is stamped with. */
157
+ now: Instant;
158
+ check: StateCheck;
159
+ /** `ctx.emit`'s kernel writer — stamps the actor, the authorization chain and the operation. */
160
+ emit: (event: DomainEventInput) => void;
161
+ /** K-42's read-only refusal, for the effecting verbs. */
162
+ assertWrites: (verb: string) => void;
163
+ }
164
+ /** The verbs, as `OperationContext` carries them. */
165
+ export type EntityStateVerbs = Pick<OperationContext, 'archive' | 'unarchive' | 'trash' | 'restore' | 'entityState'>;
166
+ export declare function createEntityStateVerbs(deps: EntityStateDeps): EntityStateVerbs;
167
+ /**
168
+ * After runtime DDL (#119, Codex r3): does every stateful table still carry what the kernel
169
+ * derived for it? The guard refuses the DDL known to take it away; this is the check that does not
170
+ * depend on having known. Read from `main`'s own catalogue, so a same-named temp object cannot
171
+ * stand in for the real table. Throws — the operation and its DDL roll back — rather than
172
+ * re-deriving: a schema the kernel did not expect is not one to repair silently.
173
+ *
174
+ * `indexes` names the derived list indexes each stateful table must keep, by table.
175
+ */
176
+ export declare function assertEntityStateIntact(sql: ScopedSql, plans: ReadonlyMap<string, EntityStatePlan>, indexes?: ReadonlyMap<string, readonly string[]>): void;
177
+ //# sourceMappingURL=entity-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-state.d.ts","sourceRoot":"","sources":["../src/entity-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,EASL,KAAK,gBAAgB,EAErB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,OAAO,EACZ,KAAK,aAAa,EACnB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGjF,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+CAA+C;IAC/C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,aAAa,CAAC;IAC3C,8CAA8C;IAC9C,QAAQ,CAAC,eAAe,CAAC,EAAE,aAAa,CAAC;CAC1C;AAED,0FAA0F;AAC1F,MAAM,MAAM,UAAU,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,SAAS,sBAAsB,EAAE,GAAG,SAAS,GAC1D,eAAe,EAAE,CA2BnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,SAAS,sBAAsB,EAAE,GAAG,SAAS,GAC1D,YAAY,EAAE,CAoBhB;AAOD,2FAA2F;AAC3F,eAAO,MAAM,2BAA2B,qBAAqB,CAAC;AAE9D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,0BAA0B,CAAC;AAChE,eAAO,MAAM,sBAAsB,2KAMlC,CAAC;AAKF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAkBnE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EACpC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,SAAS,sBAAsB,EAAE,GAAG,SAAS,EAC3D,YAAY,EAAE,SAAS;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,GAChD,IAAI,CAkBN;AAED,4GAA4G;AAC5G,eAAO,MAAM,gBAAgB,UAAW,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,KAAG,WAAW,CAAC,MAAM,CACjC,CAAC;AAEjE,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,cAAc,SAAU,eAAe,KAAG,YAGrD,CAAC;AAEH,yFAAyF;AACzF,wBAAgB,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,eAAe,EAAE,CAEhE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,eAAe,EACrB,KAAK,CAAC,EAAE,MAAM,GACb,MAAM,CAAC;AACV,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,YAAY,GAAG,SAAS,EACjC,IAAI,EAAE,eAAe,GAAG,SAAS,EACjC,KAAK,CAAC,EAAE,MAAM,GACb,MAAM,GAAG,SAAS,CAAC;AA4CtB;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,EAC3C,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,SAAS,GAAG,OAAO,GACzB;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,aAAa,CAAA;CAAE,CAS/C;AAED,4CAA4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,uGAAuG;IACvG,GAAG,EAAE,SAAS,CAAC;IACf,2DAA2D;IAC3D,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC5C,kEAAkE;IAClE,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,UAAU,CAAC;IAClB,gGAAgG;IAChG,IAAI,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACxC,yDAAyD;IACzD,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,qDAAqD;AACrD,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,aAAa,CAAC,CAAC;AAsBrH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,eAAe,GAAG,gBAAgB,CAgE9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,SAAS,EACd,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,EAC3C,OAAO,GAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAa,GAC1D,IAAI,CAgCN"}