@adobe/spacecat-shared-utils 1.53.0 → 1.55.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [@adobe/spacecat-shared-utils-v1.55.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.54.0...@adobe/spacecat-shared-utils-v1.55.0) (2025-10-03)
2
+
3
+
4
+ ### Features
5
+
6
+ * add topics with nested prompts + make categories and prompts top level entities ([#999](https://github.com/adobe/spacecat-shared/issues/999)) ([1b43970](https://github.com/adobe/spacecat-shared/commit/1b43970912478361b2eb4eaf1e7f173e77ad80e9))
7
+
8
+ # [@adobe/spacecat-shared-utils-v1.54.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.53.0...@adobe/spacecat-shared-utils-v1.54.0) (2025-10-02)
9
+
10
+
11
+ ### Features
12
+
13
+ * remove topic from brand alias and make regions mandatory in categories + make regions mandatory for categories ([#997](https://github.com/adobe/spacecat-shared/issues/997)) ([78e609a](https://github.com/adobe/spacecat-shared/commit/78e609a336cb2a2645199d94d4464c4843bd8c4a))
14
+
1
15
  # [@adobe/spacecat-shared-utils-v1.53.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.52.0...@adobe/spacecat-shared-utils-v1.53.0) (2025-10-02)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-utils",
3
- "version": "1.53.0",
3
+ "version": "1.55.0",
4
4
  "description": "Shared modules of the Spacecat Services - utils",
5
5
  "type": "module",
6
6
  "engines": {
@@ -41,6 +41,8 @@ export function llmoConfigPath(siteId) {
41
41
  export function defaultConfig() {
42
42
  return {
43
43
  entities: {},
44
+ categories: {},
45
+ topics: {},
44
46
  brands: {
45
47
  aliases: [],
46
48
  },
package/src/schemas.js CHANGED
@@ -38,21 +38,39 @@ const nonEmptyString = z.string().min(1);
38
38
 
39
39
  const region = z.string().length(2).regex(/^[a-z][a-z]$/i);
40
40
 
41
- const entity = z.union([
42
- z.object({ type: z.literal('category'), name: nonEmptyString, region: z.union([region, z.array(region)]).optional() }),
43
- z.object({ type: z.literal('topic'), name: nonEmptyString }),
44
- z.object({ type: nonEmptyString }),
45
- ]);
41
+ const prompt = z.object({
42
+ prompt: nonEmptyString,
43
+ regions: z.array(region),
44
+ origin: z.union([z.literal('human'), z.literal('ai'), z.string()]),
45
+ source: z.union([z.literal('config'), z.literal('api'), z.string()]),
46
+ });
47
+
48
+ const entity = z.object({
49
+ type: nonEmptyString,
50
+ name: nonEmptyString,
51
+ });
52
+
53
+ const category = z.object({
54
+ name: nonEmptyString,
55
+ region: z.union([region, z.array(region)]),
56
+ });
57
+
58
+ const topic = z.object({
59
+ name: nonEmptyString,
60
+ prompts: z.array(prompt).min(1),
61
+ category: z.union([z.uuid(), nonEmptyString]),
62
+ });
46
63
 
47
64
  export const llmoConfig = z.object({
48
65
  entities: z.record(z.uuid(), entity),
66
+ categories: z.record(z.uuid(), category),
67
+ topics: z.record(z.uuid(), topic),
49
68
  brands: z.object({
50
69
  aliases: z.array(
51
70
  z.object({
52
71
  aliases: z.array(nonEmptyString),
53
72
  category: z.uuid(),
54
73
  region: z.union([region, z.array(region)]),
55
- topic: z.uuid(),
56
74
  }),
57
75
  ),
58
76
  }),
@@ -68,75 +86,73 @@ export const llmoConfig = z.object({
68
86
  ),
69
87
  }),
70
88
  }).superRefine((value, ctx) => {
71
- const { entities, brands, competitors } = value;
89
+ const {
90
+ categories, topics, brands, competitors,
91
+ } = value;
72
92
 
73
93
  brands.aliases.forEach((alias, index) => {
74
- ensureEntityType(entities, ctx, alias.category, 'category', ['brands', 'aliases', index, 'category'], 'category');
75
- ensureEntityType(entities, ctx, alias.topic, 'topic', ['brands', 'aliases', index, 'topic'], 'topic');
76
- ensureRegionCompatibility(entities, ctx, alias.category, alias.region, ['brands', 'aliases', index, 'region'], 'brand alias');
94
+ ensureCategoryExists(categories, ctx, alias.category, ['brands', 'aliases', index, 'category']);
95
+ ensureRegionCompatibility(categories, ctx, alias.category, alias.region, ['brands', 'aliases', index, 'region'], 'brand alias');
77
96
  });
78
97
 
79
98
  competitors.competitors.forEach((competitor, index) => {
80
- ensureEntityType(entities, ctx, competitor.category, 'category', ['competitors', 'competitors', index, 'category'], 'category');
81
- ensureRegionCompatibility(entities, ctx, competitor.category, competitor.region, ['competitors', 'competitors', index, 'region'], 'competitor');
99
+ ensureCategoryExists(categories, ctx, competitor.category, ['competitors', 'competitors', index, 'category']);
100
+ ensureRegionCompatibility(categories, ctx, competitor.category, competitor.region, ['competitors', 'competitors', index, 'region'], 'competitor');
101
+ });
102
+
103
+ // Validate topic prompts regions against their category
104
+ Object.entries(topics).forEach(([topicId, topicEntity]) => {
105
+ if (topicEntity.prompts && topicEntity.category) {
106
+ // If category is a UUID, validate against the referenced category entity
107
+ if (topicEntity.category.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
108
+ topicEntity.prompts.forEach((promptItem, promptIndex) => {
109
+ ensureRegionCompatibility(
110
+ categories,
111
+ ctx,
112
+ topicEntity.category,
113
+ promptItem.regions,
114
+ ['topics', topicId, 'prompts', promptIndex, 'regions'],
115
+ 'topic prompt',
116
+ );
117
+ });
118
+ }
119
+ }
82
120
  });
83
121
  });
84
122
 
85
123
  /**
86
- * @param {LLMOConfig['entities']} entities
124
+ * @param {LLMOConfig['categories']} categories
87
125
  * @param {z.RefinementCtx} ctx
88
126
  * @param {string} id
89
- * @param {string} expectedType
90
127
  * @param {Array<number | string>} path
91
- * @param {string} refLabel
92
128
  */
93
- function ensureEntityType(entities, ctx, id, expectedType, path, refLabel) {
94
- const entityValue = entities[id];
95
- if (!entityValue) {
129
+ function ensureCategoryExists(categories, ctx, id, path) {
130
+ if (!categories[id]) {
96
131
  ctx.addIssue({
97
132
  code: 'custom',
98
133
  path,
99
- message: `Unknown ${refLabel} entity: ${id}`,
100
- });
101
- return;
102
- }
103
-
104
- if (entityValue.type !== expectedType) {
105
- ctx.addIssue({
106
- code: 'custom',
107
- path,
108
- message: `Entity ${id} referenced as ${refLabel} must have type "${expectedType}" but was "${entityValue.type}"`,
134
+ message: `Category ${id} does not exist`,
109
135
  });
110
136
  }
111
137
  }
112
138
 
113
139
  /**
114
- * @param {LLMOConfig['entities']} entities
140
+ * @param {LLMOConfig['categories']} categories
115
141
  * @param {z.RefinementCtx} ctx
116
142
  * @param {string} categoryId
117
143
  * @param {string | string[]} itemRegion
118
144
  * @param {Array<number | string>} path
119
145
  * @param {string} itemLabel
120
146
  */
121
- function ensureRegionCompatibility(entities, ctx, categoryId, itemRegion, path, itemLabel) {
122
- const categoryEntity = entities[categoryId];
147
+ function ensureRegionCompatibility(categories, ctx, categoryId, itemRegion, path, itemLabel) {
148
+ const categoryEntity = categories[categoryId];
123
149
  if (!categoryEntity) {
124
- // Category validation is handled by ensureEntityType
150
+ // Category validation is handled by ensureCategoryExists
125
151
  return;
126
152
  }
127
153
 
128
154
  const categoryRegions = categoryEntity.region;
129
155
 
130
- // If category has no regions defined, item should not have regions
131
- if (!categoryRegions) {
132
- ctx.addIssue({
133
- code: 'custom',
134
- path,
135
- message: `${itemLabel} cannot have regions when the referenced category has no regions defined`,
136
- });
137
- return;
138
- }
139
-
140
156
  // Normalize regions to arrays for comparison
141
157
  const categoryRegionArray = Array.isArray(categoryRegions) ? categoryRegions : [categoryRegions];
142
158
  const itemRegionArray = Array.isArray(itemRegion) ? itemRegion : [itemRegion];