Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
* governing permissions and limitations under the License.
*/

export const AUDIT_TYPE = 'semantic-value-visibility';
export const GUIDANCE_TYPE = 'guidance:semantic-value-visibility';
export const OPPORTUNITY_TYPE = 'semantic-value-visibility';
export const AUDIT_TYPE = 'image-enrichment';
export const GUIDANCE_TYPE = 'guidance:image-enrichment';
export const OPPORTUNITY_TYPE = 'image-enrichment';
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { createOpportunityData } from './opportunity-data-mapper.js';
import { OPPORTUNITY_TYPE } from './constants.js';

/**
* Guidance handler for semantic value visibility.
* Guidance handler for image enrichment.
*
* Receives Mystique's response with marketing image suggestions and:
* - Creates/updates an opportunity for the site
Expand All @@ -35,7 +35,7 @@ export default async function handler(message, context) {
// Validate siteId
const site = await Site.findById(siteId);
if (!site) {
log.error(`[semantic-value-visibility] Site not found for siteId: ${siteId}`);
log.error(`[image-enrichment] Site not found for siteId: ${siteId}`);
return notFound('Site not found');
}

Expand All @@ -47,23 +47,23 @@ export default async function handler(message, context) {
// Note: semanticHtml contains untrusted LLM-generated content from Mystique.
// Downstream consumers must sanitize before rendering.
if (!Array.isArray(suggestions)) {
log.error(`[semantic-value-visibility] Invalid suggestions format for siteId: ${siteId}`);
log.error(`[image-enrichment] Invalid suggestions format for siteId: ${siteId}`);
return badRequest('Suggestions must be an array');
}

const validSuggestions = suggestions.filter((s) => {
const hasRequiredFields = s?.data?.imageUrl && s?.data?.semanticHtml;
if (!hasRequiredFields) {
log.warn('[semantic-value-visibility] Skipping suggestion with missing imageUrl or semanticHtml');
log.warn('[image-enrichment] Skipping suggestion with missing imageUrl or semanticHtml');
}
return hasRequiredFields;
});

log.info(`[semantic-value-visibility] Guidance handler received ${validSuggestions.length} valid suggestions for siteId: ${siteId}`);
log.info(`[image-enrichment] Guidance handler received ${validSuggestions.length} valid suggestions for siteId: ${siteId}`);

// No valid suggestions — handle stale opportunity
if (validSuggestions.length === 0) {
log.info(`[semantic-value-visibility] No marketing images found for siteId: ${siteId}`);
log.info(`[image-enrichment] No marketing images found for siteId: ${siteId}`);

// Check if there's an existing opportunity to clean up
const existing = await Opportunity.allBySiteIdAndStatus(siteId, 'NEW');
Expand All @@ -72,7 +72,7 @@ export default async function handler(message, context) {
);

if (staleOpportunity) {
log.info(`[semantic-value-visibility] Removing stale opportunity ${staleOpportunity.getId()} for siteId: ${siteId}`);
log.info(`[image-enrichment] Removing stale opportunity ${staleOpportunity.getId()} for siteId: ${siteId}`);
staleOpportunity.setStatus('RESOLVED');
staleOpportunity.setUpdatedBy('system');
await staleOpportunity.save();
Expand All @@ -94,11 +94,11 @@ export default async function handler(message, context) {
);

if (!opportunity) {
log.error(`[semantic-value-visibility] Failed to create opportunity for siteId: ${siteId}`);
log.error(`[image-enrichment] Failed to create opportunity for siteId: ${siteId}`);
return badRequest('Failed to create opportunity');
}

log.info(`[semantic-value-visibility] Opportunity ${opportunity.getId()} ready for siteId: ${siteId}`);
log.info(`[image-enrichment] Opportunity ${opportunity.getId()} ready for siteId: ${siteId}`);

// Sync suggestions — adds new ones, marks outdated ones
await syncSuggestions({
Expand All @@ -114,7 +114,7 @@ export default async function handler(message, context) {
context,
});

log.info(`[semantic-value-visibility] Synced ${validSuggestions.length} suggestions for opportunity ${opportunity.getId()}`);
log.info(`[image-enrichment] Synced ${validSuggestions.length} suggestions for opportunity ${opportunity.getId()}`);

return ok();
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export async function sendToMystique(auditUrl, auditData, context, site) {
data: { url: auditUrl },
});

log.info('[semantic-value-visibility] Request sent to Mystique');
log.info('[image-enrichment] Request sent to Mystique');
return auditData;
}

Expand Down
11 changes: 7 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ import youtubeAnalysisGuidance from './youtube-analysis/guidance-handler.js';
import citedAnalysis from './cited-analysis/handler.js';
import citedAnalysisGuidance from './cited-analysis/guidance-handler.js';
import frescopaDataGeneration from './frescopa-data-generation/handler.js';
import semanticValueVisibility from './semantic-value-visibility/handler.js';
import semanticValueVisibilityGuidance from './semantic-value-visibility/guidance-handler.js';
import imageEnrichment from './image-enrichment/handler.js';
import imageEnrichmentGuidance from './image-enrichment/guidance-handler.js';
import drsPromptGeneration from './drs-prompt-generation/handler.js';
import offsiteBrandPresence from './offsite-brand-presence/handler.js';

Expand Down Expand Up @@ -223,8 +223,11 @@ const HANDLERS = {
'cited-analysis': citedAnalysis,
'guidance:cited-analysis': citedAnalysisGuidance,
'frescopa-data-generation': frescopaDataGeneration,
'semantic-value-visibility': semanticValueVisibility,
'guidance:semantic-value-visibility': semanticValueVisibilityGuidance,
'image-enrichment': imageEnrichment,
'guidance:image-enrichment': imageEnrichmentGuidance,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: keep the old keys as aliases for one release.

Removing 'semantic-value-visibility' and 'guidance:semantic-value-visibility' from HANDLERS means any in-flight SQS message with the old type hits the if (!handler) branch in src/index.js#run, logs a warn, and returns notFound(). SQS treats that as success - the message is acked and the audit/guidance is silently dropped. Producers (scheduled crons in spacecat-jobs-dispatcher, in-flight mystique guidance responses) are not guaranteed to redeploy in lock-step with this PR.

Fix:

'image-enrichment': imageEnrichment,
'guidance:image-enrichment': imageEnrichmentGuidance,
// Deprecated aliases - remove after jobs-dispatcher cron + mystique PR 1704 reach prod
'semantic-value-visibility': imageEnrichment,
'guidance:semantic-value-visibility': imageEnrichmentGuidance,

Four in-scope lines. Converts a strict 4-PR merge ordering into a tolerant rollout.

// @deprecated remove after jobs-dispatcher + mystique PR 1704 in prod
'semantic-value-visibility': imageEnrichment,
'guidance:semantic-value-visibility': imageEnrichmentGuidance,
'drs:prompt_generation_base_url': drsPromptGeneration,
'offsite-brand-presence': offsiteBrandPresence,
dummy: (message) => ok(message),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ use(sinonChai);

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const fixturesPath = join(__dirname, '../../fixtures/semantic-value-visibility');
const fixturesPath = join(__dirname, '../../fixtures/image-enrichment');

// Load Krisshop fixture (new format with top-level guidance)
const krisshopFixture = JSON.parse(readFileSync(join(fixturesPath, 'Krisshop.json'), 'utf8'));

describe('Semantic Value Visibility Guidance Handler', () => {
describe('Image Enrichment Guidance Handler', () => {
let context;
let log;
let Opportunity;
Expand All @@ -46,7 +46,7 @@ describe('Semantic Value Visibility Guidance Handler', () => {

// Load handler with mocked dependencies
const mockedHandler = await esmock(
'../../../src/semantic-value-visibility/guidance-handler.js',
'../../../src/image-enrichment/guidance-handler.js',
{
'../../../src/utils/data-access.js': {
syncSuggestions: syncSuggestionsStub,
Expand Down Expand Up @@ -316,7 +316,7 @@ describe('Semantic Value Visibility Guidance Handler', () => {
it('should mark stale opportunity as RESOLVED when no new suggestions', async () => {
const staleOpportunity = {
getId: sinon.stub().returns('stale-oppty-123'),
getType: sinon.stub().returns('semantic-value-visibility'),
getType: sinon.stub().returns('image-enrichment'),
setStatus: sinon.stub(),
setUpdatedBy: sinon.stub(),
save: sinon.stub().resolves(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
import { expect, use } from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { auditRunner, sendToMystique } from '../../../src/semantic-value-visibility/handler.js';
import { auditRunner, sendToMystique } from '../../../src/image-enrichment/handler.js';

use(sinonChai);

describe('Semantic Value Visibility Handler', () => {
describe('Image Enrichment Handler', () => {
let sandbox;
let site;
let context;
Expand Down Expand Up @@ -83,7 +83,7 @@ describe('Semantic Value Visibility Handler', () => {

const message = sqsStub.sendMessage.getCall(0).args[1];
expect(message).to.deep.include({
type: 'guidance:semantic-value-visibility',
type: 'guidance:image-enrichment',
siteId,
auditId: 'audit-456',
url: auditUrl,
Expand All @@ -107,7 +107,7 @@ describe('Semantic Value Visibility Handler', () => {
await sendToMystique(auditUrl, {}, context, site);

expect(logStub.info).to.have.been.calledWith(
'[semantic-value-visibility] Request sent to Mystique',
'[image-enrichment] Request sent to Mystique',
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import { expect } from 'chai';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { createOpportunityData } from '../../../src/semantic-value-visibility/opportunity-data-mapper.js';
import { createOpportunityData } from '../../../src/image-enrichment/opportunity-data-mapper.js';
import { DATA_SOURCES } from '../../../src/common/constants.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const fixturesPath = join(__dirname, '../../fixtures/semantic-value-visibility');
const fixturesPath = join(__dirname, '../../fixtures/image-enrichment');
const krisshopFixture = JSON.parse(readFileSync(join(fixturesPath, 'Krisshop.json'), 'utf8'));

describe('Semantic Value Visibility Opportunity Data Mapper', () => {
describe('Image Enrichment Opportunity Data Mapper', () => {
describe('createOpportunityData', () => {
it('should create opportunity data with all required fields', () => {
const result = createOpportunityData({ guidance: krisshopFixture.guidance });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"url": "https://www.krisshop.com/",
"opportunityType": "semantic-value-visibility",
"opportunityType": "image-enrichment",
"guidance": {
"insight": "Marketing images on this page contain promotional text (headlines, offers, CTAs) that is not represented in the page's semantic HTML structure.",
"rationale": "Text embedded in images is invisible to LLMs and search engines. Without machine-readable HTML equivalents, key marketing messages cannot be understood, indexed, or surfaced in AI-powered search results.",
Expand Down
Loading