@rudra-js/core 0.5.0 → 0.6.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 (64) hide show
  1. package/README.md +51 -4
  2. package/dist/claim-screening.d.ts +33 -0
  3. package/dist/claim-screening.d.ts.map +1 -0
  4. package/dist/claim-screening.js +226 -0
  5. package/dist/claim-screening.js.map +1 -0
  6. package/dist/component-generator.d.ts.map +1 -1
  7. package/dist/component-generator.js +32 -98
  8. package/dist/component-generator.js.map +1 -1
  9. package/dist/component-spec.d.ts +2 -49
  10. package/dist/component-spec.d.ts.map +1 -1
  11. package/dist/component-spec.js +3 -17
  12. package/dist/component-spec.js.map +1 -1
  13. package/dist/fallback-component.d.ts +3 -0
  14. package/dist/fallback-component.d.ts.map +1 -1
  15. package/dist/fallback-component.js +3 -13
  16. package/dist/fallback-component.js.map +1 -1
  17. package/dist/fit-to-shopper.d.ts +5 -1
  18. package/dist/fit-to-shopper.d.ts.map +1 -1
  19. package/dist/fit-to-shopper.js +4 -3
  20. package/dist/fit-to-shopper.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/model-prompt.d.ts +1 -1
  25. package/dist/model-prompt.d.ts.map +1 -1
  26. package/dist/model-prompt.js +38 -18
  27. package/dist/model-prompt.js.map +1 -1
  28. package/dist/product-selection.d.ts +0 -6
  29. package/dist/product-selection.d.ts.map +1 -1
  30. package/dist/product-selection.js +1 -3
  31. package/dist/product-selection.js.map +1 -1
  32. package/dist/provider.d.ts +1 -8
  33. package/dist/provider.d.ts.map +1 -1
  34. package/dist/provider.js +1 -8
  35. package/dist/provider.js.map +1 -1
  36. package/dist/reconciliation.d.ts +10 -22
  37. package/dist/reconciliation.d.ts.map +1 -1
  38. package/dist/reconciliation.js +116 -399
  39. package/dist/reconciliation.js.map +1 -1
  40. package/dist/signal-digest.d.ts +10 -0
  41. package/dist/signal-digest.d.ts.map +1 -1
  42. package/dist/signal-digest.js +38 -28
  43. package/dist/signal-digest.js.map +1 -1
  44. package/dist/spec-cache.d.ts +14 -2
  45. package/dist/spec-cache.d.ts.map +1 -1
  46. package/dist/spec-cache.js +14 -24
  47. package/dist/spec-cache.js.map +1 -1
  48. package/dist/tracking-input.d.ts.map +1 -1
  49. package/dist/tracking-input.js +15 -13
  50. package/dist/tracking-input.js.map +1 -1
  51. package/package.json +4 -4
  52. package/src/claim-screening.ts +242 -0
  53. package/src/component-generator.ts +37 -123
  54. package/src/component-spec.ts +5 -18
  55. package/src/fallback-component.ts +3 -14
  56. package/src/fit-to-shopper.ts +9 -4
  57. package/src/index.ts +1 -0
  58. package/src/model-prompt.ts +42 -20
  59. package/src/product-selection.ts +1 -9
  60. package/src/provider.ts +1 -8
  61. package/src/reconciliation.ts +135 -430
  62. package/src/signal-digest.ts +41 -31
  63. package/src/spec-cache.ts +27 -24
  64. package/src/tracking-input.ts +16 -13
package/src/index.ts CHANGED
@@ -61,6 +61,7 @@ export {
61
61
  type ComponentSpec,
62
62
  type CopyBlock,
63
63
  type GeneratedSpec,
64
+ type GeneratedSpecResult,
64
65
  type GridBlock,
65
66
  type HeroBlock,
66
67
  type ProductReference,
@@ -1,6 +1,6 @@
1
1
  import { BANNER_TONES, EMPHASIS, RECOMMENDATION_BASES, TONES } from './component-spec.js';
2
2
  import type { SignalDigest } from './signal-digest.js';
3
- import type { Product, TrackingInput } from './tracking-input.js';
3
+ import { FIELD_LIMITS, type Product, type TrackingInput } from './tracking-input.js';
4
4
 
5
5
  /**
6
6
  * What actually reaches the model.
@@ -140,34 +140,57 @@ on.`;
140
140
  * text exactly the same way, and it missed U+0085, U+061C and U+00AD as well.
141
141
  * Properties cover the ones nobody has thought of yet.
142
142
  *
143
- * - Cc, control. Includes U+0085, a mandatory line break that is not U+000A.
144
- * - Cf, format. Zero-width characters, the bidirectional overrides and
145
- * isolates, and the tag block, which mirrors all of ASCII invisibly.
146
- * - Zl and Zp, the line and paragraph separators.
147
- * - Cn and Co, unassigned and private use — undefined rendering by definition.
148
- * - The variation selectors supplement, which is assigned and therefore not
149
- * caught by Cn, and is invisible.
143
+ * The general categories alone do not, though. Half of what renders as nothing
144
+ * is a mark or a letter rather than a format character \u2014 the Mongolian free
145
+ * variation selectors, U+034F, the Hangul fillers, and the sixteen variation
146
+ * selectors at U+FE00 \u2014 so `Default_Ignorable_Code_Point` is in the class too.
147
+ * It is the property that means "takes up no room", which is the thing that
148
+ * makes a character able to carry text nobody sees.
150
149
  *
151
- * The zero-width joiner is the one exception. It is a format character, but it
152
- * is also how a family emoji is spelled, so escaping it mangles ordinary
153
- * product titles. Emoji presentation selectors (U+FE00-U+FE0F) are excluded for
154
- * the same reason.
150
+ * Three are left out. The zero-width joiner is a format character, but it is
151
+ * also how a family emoji is spelled, and U+FE0E and U+FE0F are what make a
152
+ * character render as an emoji rather than as text. Escaping those mangles
153
+ * ordinary product titles. The other thirteen selectors at U+FE00 spell
154
+ * nothing and are escaped like the rest.
155
155
  */
156
- const UNPRINTABLE = /(?!\u200D)[\p{Cc}\p{Cf}\p{Cn}\p{Co}\p{Zl}\p{Zp}\u{E0100}-\u{E01EF}]/gu;
156
+ const UNPRINTABLE =
157
+ /(?!\u{200D}|\u{FE0E}|\u{FE0F})[\p{Cc}\p{Cf}\p{Cn}\p{Co}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/gu;
157
158
 
158
159
  /**
159
- * Host-supplied text, written so it cannot introduce structure of its own.
160
+ * How long one quoted value may be once escaped.
160
161
  *
161
- * Quoting handles the obvious half. Escaping the characters above handles the
162
- * half that looks identical to a reader: after this, a value occupies exactly
163
- * one line, reads in one direction, and contains nothing a log cannot show.
162
+ * The contract caps the string a host may send, and this caps what that string
163
+ * turns into. They are not the same number: an escape writes up to eight
164
+ * characters for one, so a field on its cap can still buy eight times the
165
+ * prompt \u2014 and the bill \u2014 that the cap implies. Ordinary text escapes nothing
166
+ * and never comes near this.
164
167
  */
165
- const quote = (value: string) =>
166
- JSON.stringify(value).replace(UNPRINTABLE, (character) => {
168
+ const MAX_QUOTED = FIELD_LIMITS.shortText * 2;
169
+
170
+ const escapeUnprintable = (text: string) =>
171
+ text.replace(UNPRINTABLE, (character) => {
167
172
  const codePoint = character.codePointAt(0)!;
168
173
  return `\\u{${codePoint.toString(16).toUpperCase()}}`;
169
174
  });
170
175
 
176
+ /** Host-supplied text, written so it cannot introduce structure of its own. */
177
+ const quote = (value: string) => {
178
+ const escaped = escapeUnprintable(JSON.stringify(value));
179
+ if (escaped.length <= MAX_QUOTED) return escaped;
180
+
181
+ // Cut whole characters, so the result cannot end inside an escape sequence.
182
+ let length = 2;
183
+ let kept = '';
184
+ for (const character of value) {
185
+ const cost = escapeUnprintable(JSON.stringify(character)).length - 2;
186
+ if (length + cost > MAX_QUOTED) break;
187
+ length += cost;
188
+ kept += character;
189
+ }
190
+
191
+ return escapeUnprintable(JSON.stringify(kept));
192
+ };
193
+
171
194
  function section(heading: string, body: string | undefined): string | null {
172
195
  if (!body || body.length === 0) return null;
173
196
  return `${heading}: ${body}`;
@@ -207,7 +230,6 @@ function describeShopper(digest: SignalDigest): string {
207
230
  return lines.filter((line): line is string => line !== null).join('\n');
208
231
  }
209
232
 
210
- /** One candidate per line. Facts the model must not restate are left out. */
211
233
  function describeCandidate(product: Product): string {
212
234
  const parts = [quote(product.sku), quote(product.title), quote(product.category)];
213
235
  if (product.rating !== undefined) parts.push(`rated ${product.rating}`);
@@ -42,10 +42,8 @@ const SCORE_WEIGHTS = {
42
42
  /** Beyond this many shared tags, more overlap says nothing new. */
43
43
  const MAX_TAG_OVERLAP = 3;
44
44
 
45
- /** Assumed rating for a product the catalog does not rate. */
46
45
  const UNRATED = 3.5;
47
46
 
48
- /** Tags on the products this shopper has actually engaged with. */
49
47
  function engagedTags(input: TrackingInput, digest: SignalDigest): Set<string> {
50
48
  const engagedSkus = new Set([
51
49
  ...digest.likedSkus,
@@ -98,12 +96,6 @@ function basisFor(
98
96
  return { basis: 'popular', reason: `Popular in ${product.category}` };
99
97
  }
100
98
 
101
- /**
102
- * Scores every eligible candidate and returns them best first.
103
- *
104
- * Ties break on SKU so the order is total: two runs over the same payload
105
- * produce the same list, which is what makes the control arm reproducible.
106
- */
107
99
  /** How the picks are ordered once the unplaceable ones are gone. */
108
100
  export type RankOrder = 'signals' | 'given';
109
101
 
@@ -129,7 +121,7 @@ export function selectProducts(
129
121
  // same thing whether a shopper has two signals or two hundred.
130
122
  const strongestAffinity = Math.max(1, ...affinityByCategory.values());
131
123
  const tags = engagedTags(input, digest);
132
- const excluded = neverRecommend(digest);
124
+ const excluded = neverRecommend(input);
133
125
  const viewsBySku = new Map(digest.topViewed.map((viewed) => [viewed.sku, viewed.views]));
134
126
 
135
127
  const picks: ProductPick[] = [];
package/src/provider.ts CHANGED
@@ -74,14 +74,7 @@ export interface ComponentProvider {
74
74
  generate(request: ProviderRequest): Promise<ProviderResult>;
75
75
  }
76
76
 
77
- /**
78
- * A provider that returns a fixed spec and never touches the network.
79
- *
80
- * This is the benchmark's control: it isolates the cost of the framework from
81
- * the latency of a model, which is the only way to report what server-side
82
- * rendering itself costs. It is also what the generator's tests run against,
83
- * since a test that needs an API key is a test nobody runs.
84
- */
77
+ /** A provider that returns a fixed spec and never touches the network. */
85
78
  export function createFixedSpecProvider(spec: GeneratedSpec): ComponentProvider {
86
79
  return {
87
80
  name: 'fixed',