@rudra-js/core 0.5.0 → 0.7.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 (67) hide show
  1. package/README.md +64 -16
  2. package/dist/claim-screening.d.ts +10 -0
  3. package/dist/claim-screening.d.ts.map +1 -0
  4. package/dist/claim-screening.js +157 -0
  5. package/dist/claim-screening.js.map +1 -0
  6. package/dist/component-generator.d.ts +0 -70
  7. package/dist/component-generator.d.ts.map +1 -1
  8. package/dist/component-generator.js +27 -135
  9. package/dist/component-generator.js.map +1 -1
  10. package/dist/component-spec.d.ts +4 -159
  11. package/dist/component-spec.d.ts.map +1 -1
  12. package/dist/component-spec.js +0 -92
  13. package/dist/component-spec.js.map +1 -1
  14. package/dist/fallback-component.d.ts +0 -6
  15. package/dist/fallback-component.d.ts.map +1 -1
  16. package/dist/fallback-component.js +0 -21
  17. package/dist/fallback-component.js.map +1 -1
  18. package/dist/fit-to-shopper.d.ts +5 -1
  19. package/dist/fit-to-shopper.d.ts.map +1 -1
  20. package/dist/fit-to-shopper.js +5 -14
  21. package/dist/fit-to-shopper.js.map +1 -1
  22. package/dist/index.d.ts +1 -8
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +0 -7
  25. package/dist/index.js.map +1 -1
  26. package/dist/model-prompt.d.ts +1 -30
  27. package/dist/model-prompt.d.ts.map +1 -1
  28. package/dist/model-prompt.js +19 -78
  29. package/dist/model-prompt.js.map +1 -1
  30. package/dist/product-selection.d.ts +0 -29
  31. package/dist/product-selection.d.ts.map +1 -1
  32. package/dist/product-selection.js +1 -22
  33. package/dist/product-selection.js.map +1 -1
  34. package/dist/provider.d.ts +0 -55
  35. package/dist/provider.d.ts.map +1 -1
  36. package/dist/provider.js +0 -12
  37. package/dist/provider.js.map +1 -1
  38. package/dist/reconciliation.d.ts +6 -61
  39. package/dist/reconciliation.d.ts.map +1 -1
  40. package/dist/reconciliation.js +76 -466
  41. package/dist/reconciliation.js.map +1 -1
  42. package/dist/signal-digest.d.ts +1 -18
  43. package/dist/signal-digest.d.ts.map +1 -1
  44. package/dist/signal-digest.js +27 -66
  45. package/dist/signal-digest.js.map +1 -1
  46. package/dist/spec-cache.d.ts +8 -70
  47. package/dist/spec-cache.d.ts.map +1 -1
  48. package/dist/spec-cache.js +5 -68
  49. package/dist/spec-cache.js.map +1 -1
  50. package/dist/tracking-input.d.ts +0 -49
  51. package/dist/tracking-input.d.ts.map +1 -1
  52. package/dist/tracking-input.js +5 -105
  53. package/dist/tracking-input.js.map +1 -1
  54. package/package.json +7 -6
  55. package/src/claim-screening.ts +190 -0
  56. package/src/component-generator.ts +45 -240
  57. package/src/component-spec.ts +17 -118
  58. package/src/fallback-component.ts +0 -22
  59. package/src/fit-to-shopper.ts +12 -16
  60. package/src/index.ts +1 -8
  61. package/src/model-prompt.ts +25 -84
  62. package/src/product-selection.ts +6 -52
  63. package/src/provider.ts +4 -60
  64. package/src/reconciliation.ts +104 -499
  65. package/src/signal-digest.ts +34 -83
  66. package/src/spec-cache.ts +10 -97
  67. package/src/tracking-input.ts +17 -111
package/README.md CHANGED
@@ -6,20 +6,21 @@ component specification.
6
6
  ## Install
7
7
 
8
8
  ```sh
9
- npm install @rudra-js/core @rudra-js/attested zod@^4
9
+ npm install @rudra-js/core @rudra-js/attested
10
10
  ```
11
11
 
12
- Two peer dependencies, for two different reasons.
13
-
14
- `zod` is a peer because the public API of this package _is_ zod schemas, so your
15
- app and the package have to resolve the same copy of zod. **zod 4.5 or later is
16
- required**, which is what the peer range asks for. The schemas use zod 4 APIs,
17
- and installing into a zod 3 app fails with `ERESOLVE`, which isn't the most
18
- helpful error you'll ever read. The floor is 4.5 rather than 4.0 because 4.5
19
- changed how a nullable field is written into the tool schema we send the model.
20
-
21
- `@rudra-js/attested` is a peer for the opposite reason: none of its types cross
22
- this package's public surface, and you never have to import it. It's a peer so
12
+ `zod` is a dependency, so you don't need it yourself. The public API of this
13
+ package _is_ zod schemas, and an app on any zod 4 shares one copy with it. An
14
+ app on zod 3 gets its own copy alongside ours, and the two don't mix: build on
15
+ the exported schemas with their own methods (`productSchema.array()`) rather
16
+ than your zod's (`z.array(productSchema)`), and read `error.issues` rather than
17
+ checking `instanceof ZodError`. Whichever copy core ends up with writes the tool
18
+ schema we send the model, so its wording shifts a little between releases:
19
+ before 4.5, a nullable field is written as `anyOf` rather than a type array. It
20
+ means the same thing either way.
21
+
22
+ `@rudra-js/attested` is a peer dependency, even though none of its types cross
23
+ this package's public surface and you never have to import it. It's a peer so
23
24
  there is exactly one denylist in your tree. As a real dependency, an app that
24
25
  pins its own copy gets two — we checked, and npm 10.9.4 installs the app's
25
26
  version at the top and quietly nests ours under `@rudra-js/core` — and the two
@@ -120,10 +121,23 @@ before anything renders. A product you left out doesn't reach the page.
120
121
  `bundles` is optional. These are the sets you sell together, each with your own
121
122
  price for the set, the currency that price is in, and, if you want one, your own
122
123
  name for it. Every product in a set has to be a candidate as well. That's what
123
- lets the same checks that pass a single product pass a whole set, and what lets
124
+ lets the checks that pass a single product run over a whole set, and what lets
124
125
  the renderer look the members up in the catalog it already has. Ids must be
125
126
  unique, and one set must not name the same product twice.
126
127
 
128
+ A set is placed whole, so one check runs differently: a set may hold something
129
+ the shopper already bought or has in the basket, where a grid or a carousel
130
+ would drop it. A thumbs-down on a member still blocks the whole set, and so does
131
+ a member that is out of stock, already on the page, or the product being looked
132
+ at right now.
133
+
134
+ That last one has a cost. A "frequently bought together, including this item"
135
+ set names the product whose page it sits on, so it is refused on exactly the
136
+ page you wrote it for. Leave the item out and sell the companions as the set —
137
+ that rejection goes away, and the rest of the checks still apply. And when a
138
+ bundle block is the only block the model asked for, refusing the set leaves
139
+ nothing to render, so the deterministic component takes over.
140
+
127
141
  The model _never_ picks a set and is never told a price. All it does is ask for a
128
142
  bundle block and write the words around it. We pick which set when the page is
129
143
  served, going on what the shopper has in their basket, has looked at, or is
@@ -244,6 +258,30 @@ everyone looking at it shares them. If you pick candidates per shopper you'll ge
244
258
  smaller cohorts. That's the honest outcome, because your prompt really is
245
259
  personal.
246
260
 
261
+ What goes in the key is the _set_ of candidate SKUs. Not the order you sent them
262
+ in, not which ones were in stock, and not where the 60-product cut fell. So two
263
+ requests carrying the same SKUs in a different order share one component, and the
264
+ one generated first is the one both get. If your ranking is personal, order your
265
+ candidates and the products in the copy won't be the ones you put at the top.
266
+ Prices, titles and stock are always read from your catalog at render time, so
267
+ nothing stale is ever shown — a product that sold out is dropped as the page is
268
+ served, and any sentence written about it stays.
269
+
270
+ #### Who can create a cohort
271
+
272
+ Every field above is one you supply, and each distinct value is a cohort of its
273
+ own — a fresh model call, and a slot in the cache. If a visitor can choose one of
274
+ them, a visitor can choose how many calls you pay for. A `locale` copied straight
275
+ out of `Accept-Language`, a `currentCategory` read off a URL slug and a `segment`
276
+ built from a query parameter are the three that go wrong. The shipped cache holds
277
+ 10,000 entries and drops the oldest, so enough made-up values also evict the
278
+ cohorts your real shoppers were being served from.
279
+
280
+ Draw those fields from sets you control: your own locale list, your own category
281
+ ids, your own segment labels. Nothing leaks either way — a cohort prompt carries
282
+ no shopper text at all — the cost is the bill and the model round trip on a page
283
+ that was being served from cache.
284
+
247
285
  Pass `generation: 'per-shopper'` to generate for the individual instead. The
248
286
  model then chooses the products too, and every shopper pays for their own call.
249
287
 
@@ -311,9 +349,19 @@ const generator = createComponentGenerator({ provider, rank: 'given' });
311
349
 
312
350
  You keep the rest either way. We still drop anything the shopper shouldn't be
313
351
  shown, whether it's out of stock, already bought, in the basket, disliked, or
314
- the product they're looking at right now. Every product still carries a basis
315
- we check against their real signals, and everything the model writes is still
316
- screened.
352
+ the product they're looking at right now — read from the payload you sent, not
353
+ from the shortened history the model was shown, so a shopper with a long order
354
+ book is covered too. Every product still carries a basis we check against their
355
+ real signals, and everything the model writes is still screened.
356
+
357
+ How much each basis proves is worth knowing, because two of the six prove less
358
+ than they read. `most_viewed` is checked against the SKUs they viewed,
359
+ `similar_to_current` against the category being browsed, `liked_category`
360
+ against the categories they bought, liked, carted or viewed in — standing on a
361
+ category page is not one of those — and `popular` claims nothing.
362
+ `complements_cart` and `complements_purchase` check only that the basket, or the
363
+ order history, isn't empty: nothing here can know that one product goes with
364
+ another. A basis that fails takes the sentence and the badge stating it with it.
317
365
 
318
366
  Each candidate can carry its own `reason`, the phrase shown under the product.
319
367
  Reach for it when your ranking knows something the signals don't:
@@ -0,0 +1,10 @@
1
+ import type { Product, TrackingInput } from './tracking-input.js';
2
+ export declare const ALLOWED_PHRASES: string[];
3
+ export declare function productFacts(product: Product): string[];
4
+ export interface HostFacts {
5
+ pooled: string[];
6
+ bySku: Map<string, string[]>;
7
+ }
8
+ export declare function hostFacts(input: TrackingInput): HostFacts;
9
+ export declare function claimIn(text: string): string | null;
10
+ //# sourceMappingURL=claim-screening.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claim-screening.d.ts","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AA2IlE,eAAO,MAAM,eAAe,UAAkE,CAAC;AAc/F,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAOvD;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC9B;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,CAWzD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQnD"}
@@ -0,0 +1,157 @@
1
+ import { offeredCandidates } from './model-prompt.js';
2
+ const CLAIM_PATTERNS = [
3
+ {
4
+ kind: 'rating',
5
+ patterns: [
6
+ /\breviews?\b|\breviewed\b/,
7
+ /\b(?:\d+(?:\.\d+)?|three|four|five)[\s-]?stars?\b/,
8
+ /\bstars?[\s-]?ratings?\b/,
9
+ /\b(?:customer|shopper|buyer|user|average|overall)[\s-]ratings?\b/,
10
+ /\bratings?\s+of\s+[0-5]\.\d\b/,
11
+ /\b[0-5](?:\.\d)?\s+out of\s+(?:5|five)\b/,
12
+ /\b(?:highly|top|best|well|poorly|five|four)[\s-]rated\b/,
13
+ /\brated\s+(?:[0-5]\.\d|(?:[0-5]|three|four|five)\s+(?:stars?|out of))\b/,
14
+ /\bbest[\s-]?sell(?:er|ers|ing)\b/,
15
+ /(?:\bnumber one|\bno\.? ?1|#\s?1)[\s-]?sell(?:er|ers|ing)\b/,
16
+ /\bloved by (?:thousands|hundreds|millions|\d)/,
17
+ ],
18
+ },
19
+ {
20
+ kind: 'price',
21
+ patterns: [
22
+ /[$£€¥₹]/,
23
+ /\b\d+(?:\.\d+)?\s?(?:usd|eur|gbp|dollars?|pounds?|euros?)\b/,
24
+ /\bdollars?\b|\beuros?\b/,
25
+ /\b(?:usd|eur|gbp)\s?\d/,
26
+ /\bkr\s?\d|\d\s?kr\b/,
27
+ /\bpric(?:e|es|ed|ing)\b/,
28
+ /\bwas\s+[$£€¥]?\s?\d[\d,.]*\s*[,;–—-]?\s*now\s+[$£€¥]?\s?\d/,
29
+ /\bcheap(?:er|est)\b/,
30
+ /\baffordable\b|\bbargain\b/,
31
+ /\bcosts?\s+(?:less|more|only|just|about|around|[$£€¥]?\d)/,
32
+ /\blow(?:er)?[\s-]cost\b/,
33
+ /\bsaves?\s+(?:you\s+)?(?:money|cash|[$£€¥]\s?\d)/,
34
+ ],
35
+ },
36
+ {
37
+ kind: 'discount',
38
+ patterns: [
39
+ /\d+(?:\.\d+)?\s?(?:%|percent)\s*(?:off\b|discount|reduction|less\b)/,
40
+ /\b(?:save|saving|savings|off|discount|extra|up to)\s+(?:up to\s+)?\d+(?:\.\d+)?\s?(?:%|percent)/,
41
+ /\bdiscount(?:s|ed)?\b/,
42
+ /\bsale\b|\bmarked down\b|\bdeal of\b/,
43
+ /\bhalf[\s-]?(?:price|off)\b/,
44
+ /\bsaves?\s+\d[\d.,]*\s+off\b/,
45
+ /\bclearance\s+(?:sale|price|event|deal)\b|\bon clearance\b/,
46
+ /\bprice reduced\b|\breduced price\b|\breduced by \d+\s?(?:%|percent)|\breduced\s+(?:this|next|last)\s+week\b/,
47
+ ],
48
+ },
49
+ {
50
+ kind: 'delivery',
51
+ patterns: [
52
+ /\bdelivery\b|\bshipping\b/,
53
+ /\bnext[\s-]day\b|\bsame[\s-]day\b|\bovernight\b/,
54
+ /\b(?:ships?|arrives?|arriving|delivered|by|before|in time for)\s+(?:on\s+)?(?:today|tomorrow|tonight|this week|next week|the weekend|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/,
55
+ /\b(?:ships?|arrives?|arriving)\s+(?:in|within)\s+\d/,
56
+ /\bin time for\b/,
57
+ ],
58
+ },
59
+ {
60
+ kind: 'stock',
61
+ patterns: [
62
+ /\b(?:in|out of|low on) stock\b/,
63
+ /\blimited stock\b/,
64
+ /\brestocked?\b|\bsold out\b/,
65
+ /\b(?:only\s+)?(?:\d+|a few|a handful|a couple|one|few)\s+(?:left|remain(?:s|ing)?)\b/,
66
+ /\bselling fast\b|\b(?:almost|nearly) gone\b|\bwhile stocks last\b/,
67
+ ],
68
+ },
69
+ ];
70
+ const CONFUSABLES = {
71
+ а: 'a',
72
+ в: 'b',
73
+ е: 'e',
74
+ к: 'k',
75
+ м: 'm',
76
+ н: 'h',
77
+ о: 'o',
78
+ р: 'p',
79
+ с: 'c',
80
+ т: 't',
81
+ х: 'x',
82
+ у: 'y',
83
+ ѕ: 's',
84
+ і: 'i',
85
+ ј: 'j',
86
+ ԁ: 'd',
87
+ һ: 'h',
88
+ ӏ: 'l',
89
+ α: 'a',
90
+ ε: 'e',
91
+ ι: 'i',
92
+ κ: 'k',
93
+ ν: 'v',
94
+ ο: 'o',
95
+ ρ: 'p',
96
+ τ: 't',
97
+ υ: 'u',
98
+ χ: 'x',
99
+ ϲ: 'c',
100
+ };
101
+ const INVISIBLE = /[\p{Cf}\p{Default_Ignorable_Code_Point}\p{Cc}]/gu;
102
+ const SPACING = /[\t\n\v\f\r\u0085]/;
103
+ const MARKS = /[\p{Mn}\p{Me}]/gu;
104
+ function normaliseForClaims(text) {
105
+ const lowered = text
106
+ .replace(INVISIBLE, (char) => (SPACING.test(char) ? char : ''))
107
+ .normalize('NFKC')
108
+ .toLowerCase()
109
+ .normalize('NFC')
110
+ .replace(MARKS, '');
111
+ let folded = '';
112
+ for (const char of lowered)
113
+ folded += CONFUSABLES[char] ?? char;
114
+ return folded;
115
+ }
116
+ export const ALLOWED_PHRASES = ['does not feel cheap', "doesn't feel cheap", 'last few miles'];
117
+ const DIGIT = /\p{Nd}/u;
118
+ const BARE_EXPONENT = /^[+-]?\d+(?:\.\d+)?[eE]([+-]?\d+)$/;
119
+ const MAX_EXPONENT = 1000;
120
+ function canLayOut(fact) {
121
+ const match = BARE_EXPONENT.exec(fact.trim());
122
+ if (match === null)
123
+ return true;
124
+ return Math.abs(Number(match[1] ?? '')) <= MAX_EXPONENT;
125
+ }
126
+ export function productFacts(product) {
127
+ const facts = [];
128
+ if (DIGIT.test(product.category) && canLayOut(product.category))
129
+ facts.push(product.category);
130
+ for (const tag of product.tags) {
131
+ if (DIGIT.test(tag) && canLayOut(tag))
132
+ facts.push(tag);
133
+ }
134
+ return facts;
135
+ }
136
+ export function hostFacts(input) {
137
+ const pooled = new Set();
138
+ const bySku = new Map();
139
+ for (const product of offeredCandidates(input)) {
140
+ const own = productFacts(product);
141
+ bySku.set(product.sku, own);
142
+ for (const fact of own)
143
+ pooled.add(fact);
144
+ }
145
+ return { pooled: [...pooled], bySku };
146
+ }
147
+ export function claimIn(text) {
148
+ const normalised = normaliseForClaims(text);
149
+ for (const claim of CLAIM_PATTERNS) {
150
+ for (const pattern of claim.patterns) {
151
+ if (pattern.test(normalised))
152
+ return claim.kind;
153
+ }
154
+ }
155
+ return null;
156
+ }
157
+ //# sourceMappingURL=claim-screening.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claim-screening.js","sourceRoot":"","sources":["../src/claim-screening.ts"],"names":[],"mappings":"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"}
@@ -3,51 +3,11 @@ import type { ComponentProvider, TokenUsage } from './provider.js';
3
3
  import { type RankOrder } from './product-selection.js';
4
4
  import { type SpecCache } from './spec-cache.js';
5
5
  import { type TrackingInputDraft } from './tracking-input.js';
6
- /**
7
- * Turns one tracking payload into one renderable component.
8
- *
9
- * Everything else in this package is a piece of that sentence; this module is
10
- * the order they go in. It is deliberately the only place that knows the whole
11
- * sequence, and it is written as a straight line so the sequence is readable:
12
- *
13
- * validate → digest → cache → generate → reconcile → render
14
- *
15
- * The single promise it makes to a caller is that `generate` always returns
16
- * something renderable. A model that is slow, refusing, erroring, rate-limited
17
- * or simply not configured produces the deterministic component instead. The
18
- * only way it rejects is a malformed payload, which is a caller bug and should
19
- * be loud.
20
- */
21
- /**
22
- * Reported exactly once per call to `generate`, whatever happened.
23
- *
24
- * One flat shape rather than a variant per outcome, because the numbers the
25
- * evaluation needs are ratios over all calls — hit rate, fallback share, model
26
- * calls and cost per thousand views. A variant that some callers do not emit
27
- * makes every one of those ratios wrong by however many it skipped, which is
28
- * what happened when requests that joined an in-flight generation reported
29
- * nothing at all.
30
- */
31
6
  export interface GenerationEvent {
32
- /** Null when no key was computed, which means no provider was configured. */
33
7
  key: string | null;
34
8
  source: SpecSource;
35
- /** Wall-clock milliseconds for the whole call. */
36
9
  elapsedMs: number;
37
- /**
38
- * True for the caller that sent the request, on every outcome — including a
39
- * call that timed out, errored or came back unparseable. Requests that joined
40
- * an in-flight generation share its answer and its usage figures, so cost
41
- * must be summed over this flag rather than over every event.
42
- *
43
- * It counts requests sent, which is an upper bound on requests billed: an
44
- * adapter that throws before it reaches the vendor looks the same from here
45
- * as one that throws after. An upper bound is the useful direction — the
46
- * calls that produce nothing are the ones worth seeing, and reporting them as
47
- * no call at all hides them completely.
48
- */
49
10
  calledModel: boolean;
50
- /** What reconciliation removed. Absent when no spec was reconciled. */
51
11
  violations?: string[];
52
12
  usage?: TokenUsage;
53
13
  degradedReason?: DegradedReason;
@@ -55,46 +15,16 @@ export interface GenerationEvent {
55
15
  cache?: 'hit' | 'miss' | 'error' | 'timeout';
56
16
  }
57
17
  export interface ComponentGeneratorOptions {
58
- /**
59
- * Omit to run without a model. That is a supported configuration rather than
60
- * a stub: it is the control arm of the benchmark, and the right setting for
61
- * anyone who has not yet decided on a provider.
62
- */
63
18
  provider?: ComponentProvider | null;
64
- /** Defaults to an in-process cache. Pass `createNullSpecCache()` to disable. */
65
19
  cache?: SpecCache;
66
- /**
67
- * How long the model gets. Past this the deterministic component renders and
68
- * the request is aborted. Defaults to 1500ms.
69
- */
70
20
  modelTimeoutMs?: number;
71
- /**
72
- * How long the cache gets. The shipped caches cannot exceed it, but the store
73
- * is a port a host implements — a hung Redis read on the render path would
74
- * hold the page open, which is exactly what this module exists to prevent.
75
- */
76
21
  cacheTimeoutMs?: number;
77
- /**
78
- * 'cohort' shares one generated component between shoppers who look alike and
79
- * fills in each shopper's own products. 'per-shopper' generates for the
80
- * individual, which is what the benchmark compares against. Defaults to
81
- * 'cohort'.
82
- */
83
22
  generation?: 'cohort' | 'per-shopper';
84
- /**
85
- * How the products are ordered. 'signals' scores each candidate from this
86
- * shopper's signals. 'given' keeps the order you sent, for a shop whose own
87
- * ranking is better than four weights. Either way the exclusions and the
88
- * stock check still apply, and each product still carries a basis
89
- * reconciliation can verify. Defaults to 'signals'.
90
- */
91
23
  rank?: RankOrder;
92
- /** Observability. Never allowed to break a render. */
93
24
  onEvent?: (event: GenerationEvent) => void;
94
25
  }
95
26
  export interface ComponentGenerator {
96
27
  generate(input: TrackingInputDraft): Promise<ComponentSpec>;
97
- /** The deterministic component, without consulting a model or a cache. */
98
28
  generateDeterministic(input: TrackingInputDraft): ComponentSpec;
99
29
  }
100
30
  export declare class TimeoutError extends Error {
@@ -1 +1 @@
1
- {"version":3,"file":"component-generator.d.ts","sourceRoot":"","sources":["../src/component-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,cAAc,EAEnB,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAOnE,OAAO,EAAkB,KAAK,SAAS,EAAoB,MAAM,wBAAwB,CAAC;AAG1F,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,MAAM,EAAE,UAAU,CAAC;IACnB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;OAWG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;CAC9C;AAED,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACpC,gFAAgF;IAChF,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IACtC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,sDAAsD;IACtD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAC5C;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5D,0EAA0E;IAC1E,qBAAqB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAAC;CACjE;AAED,qBAAa,YAAa,SAAQ,KAAK;IACrC,YAAY,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAG9C;CACF;AAgKD,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAA8B,GACtC,kBAAkB,CA8PpB"}
1
+ {"version":3,"file":"component-generator.d.ts","sourceRoot":"","sources":["../src/component-generator.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,cAAc,EAEnB,KAAK,UAAU,EAChB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEnE,OAAO,EAAkB,KAAK,SAAS,EAAoB,MAAM,wBAAwB,CAAC;AAG1F,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,MAAM,EAAE,UAAU,CAAC;IAEnB,SAAS,EAAE,MAAM,CAAC;IAElB,WAAW,EAAE,OAAO,CAAC;IAErB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;CAC9C;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAEpC,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,UAAU,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IAEtC,IAAI,CAAC,EAAE,SAAS,CAAC;IAEjB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAC5C;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAE5D,qBAAqB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAAC;CACjE;AAED,qBAAa,YAAa,SAAQ,KAAK;IACrC,YAAY,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAG9C;CACF;AAkGD,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAA8B,GACtC,kBAAkB,CA2LpB"}