Skip to content
This repository was archived by the owner on Oct 14, 2026. It is now read-only.

Commit 0acce60

Browse files
tb-adbeTanase Butcaru
andauthored
feat(utils): add semantic lookup source/entity type registries (LLMO-7445) (#1959)
<!-- mysticat-pr-skill --> ## 1. Abstract Adds the semantic lookup type lists to `@adobe/spacecat-shared-utils` constants: `OPPORTUNITY_SEMANTIC_SOURCE_TYPES` / `OPPORTUNITY_SEMANTIC_ENTITY_TYPES`, which today live in data-access, and the upcoming `SUGGESTION_SEMANTIC_SOURCE_TYPES` / `SUGGESTION_SEMANTIC_ENTITY_TYPES`. They're exported from both the main entry and the `/constants` subpath. ## 2. Reasoning The allowed `sourceTypes` / `entityTypes` for the semantic lookup (`POST /sites/:siteId/opportunities/by-topics`, and later the suggestion endpoints) are defined in `spacecat-shared-data-access`. The UI (project-elmo-ui) needs the same values for its SDK types, but it can't import data-access in a browser bundle: that package has no browser entry, and its entry point pulls in the S3, PostgREST, `@adobe/fetch` and X-Ray clients plus Node's `crypto`. The utils `/constants` subpath is dependency-free and already consumed by the UI, so moving the lists here gives the writer, the reader and the UI one source instead of hand-copied lists. ## 3. High-level overview of the changes - Four frozen lists in `constants.js`: - opportunity source types: `topic`; - opportunity entity types: `cited-analysis`, `reddit-analysis`, `youtube-analysis`, built from `OPPORTUNITY_TYPES`; - suggestion source types: `topic`, `title`; - suggestion entity types: the same three opportunity types. - They're exported from `@adobe/spacecat-shared-utils` and `@adobe/spacecat-shared-utils/constants`, with explicit literal types in `constants.d.ts`, so TypeScript consumers can derive exact unions (e.g. `typeof X[keyof typeof X]`). - The suggestion lists are added now, with the values settled in the suggestion lookup plan, so the upcoming suggestion work doesn't need another utils release. - Additive only; no existing export changes. data-access keeps its own copies until a follow-up PR imports and re-exports these (no API change for its consumers). - Found while doing this: the existing `OPPORTUNITY_TYPES` typing resolves to `any`, because `index.d.ts` and `constants.d.ts` re-export it from each other. That's why the new lists spell out their literal types instead of referring to `OPPORTUNITY_TYPES`. The existing typing is left as is here, since giving it real types could break consumers that rely on `any`. ## 4. Required information - Jira / issue: [LLMO-7445](https://jira.corp.adobe.com/browse/LLMO-7445) - ADR(s): [ADR 0002 — embedding client and semantic index utils](https://github.com/adobe/spacecat-shared/blob/main/docs/adr/0002-embedding-client-and-semantic-index-utils.md) - Other: the lists currently live in `packages/spacecat-shared-data-access/src/util/semantic-index.utils.js`, added in #1950 ## 6. Affected / used mysticat-workspace projects - `spacecat-shared-data-access` (consumed): the follow-up PR bumps its utils version (currently fixed at 1.105.0), imports these lists, and re-exports them under the same names. - `project-elmo-ui` (consumed): the by-topics SDK derives its `sourceTypes` / `entityTypes` types from `@adobe/spacecat-shared-utils/constants`. - `spacecat-api-service`, `spacecat-audit-worker` (unchanged): they keep importing the lists from data-access, which re-exports the same values. ## 7. Additional information outside the code - Type check in a scratch TypeScript project (`moduleResolution: bundler`, `strict`) against this branch: `OPPORTUNITY_SEMANTIC_ENTITY_TYPES` derives the exact `'cited-analysis' | 'reddit-analysis' | 'youtube-analysis'` union through both the main entry and the `/constants` subpath, and assigning `'wikipedia-analysis'` to it is a type error. The same probe against `OPPORTUNITY_TYPES` accepted any value, which confirmed its typing is `any`. ## 8. Test plan - (a) Local: the scratch TypeScript check above. - (b) After release: in project-elmo-ui, bump `@adobe/spacecat-shared-utils`, import the entity/source lists from `@adobe/spacecat-shared-utils/constants`, and confirm `npm run build` succeeds (browser bundle) with the derived types. In data-access, confirm its unit and integration tests pass after switching to the imported lists. ## 9. Deployment & merge order - This PR merges and releases first (utils minor version). - Then a data-access PR (depends on this release): bump the utils version, import and re-export the lists. - Then project-elmo-ui (depends on this release only): replace the hand-written unions in the by-topics SDK with types derived from utils. Order: this PR → data-access PR → project-elmo-ui PR (the last two can go in parallel once this is released). 🤖 Generated with [review-kit](https://github.com/adobe/experience-success-skills) --------- Co-authored-by: Tanase Butcaru <tbutcaru@adobe.com>
1 parent fc75b50 commit 0acce60

7 files changed

Lines changed: 158 additions & 1 deletion

File tree

‎packages/spacecat-shared-utils/src/constants.d.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,32 @@
1111
*/
1212

1313
export { OPPORTUNITY_TYPES, DEFAULT_CPC_VALUE } from './index.js';
14+
15+
// test/constants.test.js fails if these registries drift from constants.js.
16+
export const OPPORTUNITY_SEMANTIC_SOURCE_TYPES: Readonly<{
17+
TOPIC: 'topic';
18+
}>;
19+
20+
export const OPPORTUNITY_SEMANTIC_ENTITY_TYPES: Readonly<{
21+
CITED_ANALYSIS: 'cited-analysis';
22+
REDDIT_ANALYSIS: 'reddit-analysis';
23+
YOUTUBE_ANALYSIS: 'youtube-analysis';
24+
}>;
25+
26+
export const SUGGESTION_SEMANTIC_SOURCE_TYPES: Readonly<{
27+
TOPIC: 'topic';
28+
TITLE: 'title';
29+
}>;
30+
31+
export const SUGGESTION_SEMANTIC_ENTITY_TYPES: Readonly<{
32+
CITED_ANALYSIS: 'cited-analysis';
33+
REDDIT_ANALYSIS: 'reddit-analysis';
34+
YOUTUBE_ANALYSIS: 'youtube-analysis';
35+
}>;
36+
37+
type ValueOf<T> = T[keyof T];
38+
39+
export type OpportunitySemanticSourceType = ValueOf<typeof OPPORTUNITY_SEMANTIC_SOURCE_TYPES>;
40+
export type OpportunitySemanticEntityType = ValueOf<typeof OPPORTUNITY_SEMANTIC_ENTITY_TYPES>;
41+
export type SuggestionSemanticSourceType = ValueOf<typeof SUGGESTION_SEMANTIC_SOURCE_TYPES>;
42+
export type SuggestionSemanticEntityType = ValueOf<typeof SUGGESTION_SEMANTIC_ENTITY_TYPES>;

‎packages/spacecat-shared-utils/src/constants.js‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,43 @@ export const OPPORTUNITY_TYPES = /** @type {const} */ ({
7777
});
7878

7979
export const DEFAULT_CPC_VALUE = 1.5;
80+
81+
/**
82+
* Kinds of source text in `opportunity_semantic_embedding.source_type`. The data-access semantic
83+
* index writer and reader reject any other value (they use their own copy until they import this
84+
* one); add a kind here before indexing or searching it.
85+
*/
86+
export const OPPORTUNITY_SEMANTIC_SOURCE_TYPES = Object.freeze({
87+
TOPIC: 'topic',
88+
});
89+
90+
/**
91+
* Opportunity types allowed in `opportunity_semantic_embedding.entity_type`. Same enforcement as
92+
* `OPPORTUNITY_SEMANTIC_SOURCE_TYPES`; add a type here before indexing or filtering by it.
93+
*/
94+
export const OPPORTUNITY_SEMANTIC_ENTITY_TYPES = Object.freeze({
95+
CITED_ANALYSIS: OPPORTUNITY_TYPES.CITED_ANALYSIS,
96+
REDDIT_ANALYSIS: OPPORTUNITY_TYPES.REDDIT_ANALYSIS,
97+
YOUTUBE_ANALYSIS: OPPORTUNITY_TYPES.YOUTUBE_ANALYSIS,
98+
});
99+
100+
/**
101+
* Kinds of source text in `suggestion_semantic_embedding.source_type`: the suggestion's derived
102+
* topics and its title. Provisional: nothing enforces it until the suggestion writer and reader
103+
* land.
104+
*/
105+
export const SUGGESTION_SEMANTIC_SOURCE_TYPES = Object.freeze({
106+
TOPIC: 'topic',
107+
TITLE: 'title',
108+
});
109+
110+
/**
111+
* Parent opportunity types allowed in `suggestion_semantic_embedding.entity_type`. Provisional,
112+
* like `SUGGESTION_SEMANTIC_SOURCE_TYPES`. Separate from the opportunity list on purpose, so the
113+
* two can diverge.
114+
*/
115+
export const SUGGESTION_SEMANTIC_ENTITY_TYPES = Object.freeze({
116+
CITED_ANALYSIS: OPPORTUNITY_TYPES.CITED_ANALYSIS,
117+
REDDIT_ANALYSIS: OPPORTUNITY_TYPES.REDDIT_ANALYSIS,
118+
YOUTUBE_ANALYSIS: OPPORTUNITY_TYPES.YOUTUBE_ANALYSIS,
119+
});

‎packages/spacecat-shared-utils/src/index.d.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ export { AUTHORING_TYPES, DELIVERY_TYPES } from './aem.js';
1717

1818
export { OPPORTUNITY_TYPES } from './constants.js';
1919

20+
export {
21+
OPPORTUNITY_SEMANTIC_SOURCE_TYPES,
22+
OPPORTUNITY_SEMANTIC_ENTITY_TYPES,
23+
SUGGESTION_SEMANTIC_SOURCE_TYPES,
24+
SUGGESTION_SEMANTIC_ENTITY_TYPES,
25+
OpportunitySemanticSourceType,
26+
OpportunitySemanticEntityType,
27+
SuggestionSemanticSourceType,
28+
SuggestionSemanticEntityType,
29+
} from './constants.js';
30+
2031
export const DEFAULT_CPC_VALUE: number;
2132

2233
/** UTILITY FUNCTIONS */

‎packages/spacecat-shared-utils/src/index.js‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,14 @@ export { getStoredMetrics, storeMetrics, calculateCPCValue } from './metrics-sto
9292

9393
export { s3Wrapper, getObjectFromKey } from './s3.js';
9494

95-
export { OPPORTUNITY_TYPES, DEFAULT_CPC_VALUE } from './constants.js';
95+
export {
96+
OPPORTUNITY_TYPES,
97+
DEFAULT_CPC_VALUE,
98+
OPPORTUNITY_SEMANTIC_SOURCE_TYPES,
99+
OPPORTUNITY_SEMANTIC_ENTITY_TYPES,
100+
SUGGESTION_SEMANTIC_SOURCE_TYPES,
101+
SUGGESTION_SEMANTIC_ENTITY_TYPES,
102+
} from './constants.js';
96103

97104
export { fetch, resetFetchContext, clearFetchCache } from './adobe-fetch.js';
98105
export { tracingFetch, SPACECAT_USER_AGENT } from './tracing-fetch.js';
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import { readFileSync } from 'fs';
14+
import { expect } from 'chai';
15+
import * as constants from '../src/constants.js';
16+
import {
17+
OPPORTUNITY_SEMANTIC_SOURCE_TYPES,
18+
OPPORTUNITY_SEMANTIC_ENTITY_TYPES,
19+
SUGGESTION_SEMANTIC_SOURCE_TYPES,
20+
SUGGESTION_SEMANTIC_ENTITY_TYPES,
21+
} from '../src/constants.js';
22+
23+
describe('semantic lookup type registries', () => {
24+
it('lists the opportunity source and entity types', () => {
25+
expect(OPPORTUNITY_SEMANTIC_SOURCE_TYPES).to.deep.equal({ TOPIC: 'topic' });
26+
expect(OPPORTUNITY_SEMANTIC_ENTITY_TYPES).to.deep.equal({
27+
CITED_ANALYSIS: 'cited-analysis',
28+
REDDIT_ANALYSIS: 'reddit-analysis',
29+
YOUTUBE_ANALYSIS: 'youtube-analysis',
30+
});
31+
});
32+
33+
it('lists the suggestion source and entity types', () => {
34+
expect(SUGGESTION_SEMANTIC_SOURCE_TYPES).to.deep.equal({ TOPIC: 'topic', TITLE: 'title' });
35+
expect(SUGGESTION_SEMANTIC_ENTITY_TYPES).to.deep.equal({
36+
CITED_ANALYSIS: 'cited-analysis',
37+
REDDIT_ANALYSIS: 'reddit-analysis',
38+
YOUTUBE_ANALYSIS: 'youtube-analysis',
39+
});
40+
});
41+
42+
it('freezes every registry', () => {
43+
Object.entries({
44+
OPPORTUNITY_SEMANTIC_SOURCE_TYPES,
45+
OPPORTUNITY_SEMANTIC_ENTITY_TYPES,
46+
SUGGESTION_SEMANTIC_SOURCE_TYPES,
47+
SUGGESTION_SEMANTIC_ENTITY_TYPES,
48+
}).forEach(([name, registry]) => expect(Object.isFrozen(registry), name).to.equal(true));
49+
});
50+
51+
it('declares every frozen registry in constants.d.ts with the same values', () => {
52+
const dts = readFileSync(new URL('../src/constants.d.ts', import.meta.url), 'utf8');
53+
const declared = Object.fromEntries(
54+
[...dts.matchAll(/export const (\w+): Readonly<\{([^}]*)\}>;/g)].map(([, name, body]) => [
55+
name,
56+
Object.fromEntries([...body.matchAll(/(\w+): '([^']*)';/g)].map(([, key, value]) => [key, value])),
57+
]),
58+
);
59+
const frozen = Object.fromEntries(
60+
Object.entries(constants).filter(([, value]) => typeof value === 'object' && Object.isFrozen(value)),
61+
);
62+
expect(declared).to.deep.equal(frozen);
63+
});
64+
});

‎packages/spacecat-shared-utils/test/index.test.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ describe('Index Exports', () => {
110110
'logWrapper',
111111
'OPPORTUNITY_DEPENDENCY_MAP',
112112
'OPPORTUNITY_GRANT_CONFIG',
113+
'OPPORTUNITY_SEMANTIC_ENTITY_TYPES',
114+
'OPPORTUNITY_SEMANTIC_SOURCE_TYPES',
113115
'OPPORTUNITY_TITLES',
114116
'OPPORTUNITY_TYPES',
115117
'prependSchema',
@@ -130,6 +132,8 @@ describe('Index Exports', () => {
130132
'stripTrailingDot',
131133
'stripTrailingSlash',
132134
'stripWWW',
135+
'SUGGESTION_SEMANTIC_ENTITY_TYPES',
136+
'SUGGESTION_SEMANTIC_SOURCE_TYPES',
133137
'TOKEN_GRANT_CONFIG',
134138
'toBoolean',
135139
'filterBySiteScope',

‎packages/spacecat-shared-utils/test/subpaths.test.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ const EXPECTED_SCHEMAS_EXPORTS = [
5757

5858
const EXPECTED_CONSTANTS_EXPORTS = [
5959
'DEFAULT_CPC_VALUE', 'OPPORTUNITY_TYPES',
60+
'OPPORTUNITY_SEMANTIC_SOURCE_TYPES', 'OPPORTUNITY_SEMANTIC_ENTITY_TYPES',
61+
'SUGGESTION_SEMANTIC_SOURCE_TYPES', 'SUGGESTION_SEMANTIC_ENTITY_TYPES',
6062
];
6163

6264
const pkgDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');

0 commit comments

Comments
 (0)