@adobe/spacecat-shared-data-access 3.79.0 → 3.80.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,15 @@
1
+ ## [@adobe/spacecat-shared-data-access-v3.80.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.79.1...@adobe/spacecat-shared-data-access-v3.80.0) (2026-07-02)
2
+
3
+ ### Features
4
+
5
+ * **data-access:** add feedback review constants + verdict/signal helpers (SITES-43974) ([#1696](https://github.com/adobe/spacecat-shared/issues/1696)) ([2cabb05](https://github.com/adobe/spacecat-shared/commit/2cabb0596b27dcdefaf66c7d46531403ae005f2c))
6
+
7
+ ## [@adobe/spacecat-shared-data-access-v3.79.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.79.0...@adobe/spacecat-shared-data-access-v3.79.1) (2026-06-22)
8
+
9
+ ### Bug Fixes
10
+
11
+ * data structure for Sitemap "error" suggestions ([#1697](https://github.com/adobe/spacecat-shared/issues/1697)) ([a9b550c](https://github.com/adobe/spacecat-shared/commit/a9b550c8bf6c3411e312fe1c305f38b5a90dfdde))
12
+
1
13
  ## [@adobe/spacecat-shared-data-access-v3.79.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.78.0...@adobe/spacecat-shared-data-access-v3.79.0) (2026-06-19)
2
14
 
3
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-data-access",
3
- "version": "3.79.0",
3
+ "version": "3.80.0",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -0,0 +1,249 @@
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
+ /**
14
+ * @fileoverview Shared constants + helpers for the human-review feedback loop
15
+ * (SITES-43974). The `feedback_event` table has a client-supplied `event_id`
16
+ * primary key and is append-only, so it is NOT modelled as a data-access entity
17
+ * (the entity framework forces an auto-generated `id` PK). Producers and
18
+ * consumers — `spacecat-api-service` (capture + ?include=reviews composition)
19
+ * and `spacecat-jobs-dispatcher` (the daily JSONL exporter) — talk to the table
20
+ * via the raw `postgrestClient`. These constants + the verdict<->signal
21
+ * translation live here so the enum values and the JSONL contract never drift
22
+ * across those repos.
23
+ */
24
+
25
+ /**
26
+ * Capture surface. Derived server-side from the API route (never trusted from
27
+ * the request body — FR-10). Only `backoffice` is wired up for v1; `aso_ui` and
28
+ * `aemy_pr` are reserved.
29
+ */
30
+ export const REVIEW_SOURCES = Object.freeze({
31
+ BACKOFFICE: 'backoffice',
32
+ ASO_UI: 'aso_ui',
33
+ AEMY_PR: 'aemy_pr',
34
+ });
35
+
36
+ /**
37
+ * App-layer verdict — what the reviewer expresses in the UI / request body.
38
+ */
39
+ export const REVIEW_VERDICTS = Object.freeze({
40
+ UP: 'up',
41
+ DOWN: 'down',
42
+ });
43
+
44
+ /**
45
+ * Persisted training label on `feedback_event.signal`. Translated once from the
46
+ * verdict at capture time (see {@link verdictToSignal}).
47
+ */
48
+ export const REVIEW_SIGNALS = Object.freeze({
49
+ POSITIVE: 'positive',
50
+ NEGATIVE: 'negative',
51
+ });
52
+
53
+ /**
54
+ * Optional ESE category on a reject. `product_bug` rows are filtered OUT of the
55
+ * Learning Agent export (routed to Jira); `bad_recommendation` / `other` / NULL
56
+ * are exported.
57
+ */
58
+ export const REJECTION_CATEGORIES = Object.freeze({
59
+ PRODUCT_BUG: 'product_bug',
60
+ BAD_RECOMMENDATION: 'bad_recommendation',
61
+ OTHER: 'other',
62
+ });
63
+
64
+ /**
65
+ * Commerce tier of the reviewed site at review time.
66
+ */
67
+ export const FEEDBACK_TIERS = Object.freeze({
68
+ PAID: 'paid',
69
+ FREE: 'free',
70
+ });
71
+
72
+ /**
73
+ * Rejection categories whose rows must NOT be exported to the Learning Agent
74
+ * training corpus (they describe product/detection bugs the LA cannot learn
75
+ * from). The exporter filters these out; NULL-category rows are still exported.
76
+ */
77
+ export const EXPORT_EXCLUDED_REJECTION_CATEGORIES = Object.freeze([
78
+ REJECTION_CATEGORIES.PRODUCT_BUG,
79
+ ]);
80
+
81
+ /**
82
+ * Customer-derived fields stripped from the JSONL export for organizations with
83
+ * `training_opt_in = false`. Verdict / signal / category / identity metadata
84
+ * still ship. snake_case to match the JSONL row shape (and the feedback_event
85
+ * DB columns) — {@link toJsonlRow} uses this list to do the stripping, so the
86
+ * opt-out boundary is single-sourced here.
87
+ */
88
+ export const OPT_OUT_STRIPPED_FIELDS = Object.freeze([
89
+ 'previous_fix',
90
+ 'edited_fix',
91
+ 'detail_markdown',
92
+ ]);
93
+
94
+ /**
95
+ * Current JSONL row schema version (the `schema_version` field stamped on every
96
+ * exported line). DMAI ignores unknown fields within a known version and SHOULD
97
+ * reject unknown versions; bump this in one place on a breaking change.
98
+ */
99
+ export const SCHEMA_VERSION = 1;
100
+
101
+ /**
102
+ * Translate the app-layer verdict to the persisted training signal. Single
103
+ * source of truth shared by the capture handler and any consumer.
104
+ *
105
+ * @param {string} verdict - one of {@link REVIEW_VERDICTS}.
106
+ * @returns {string} the corresponding {@link REVIEW_SIGNALS} value.
107
+ * @throws {Error} if the verdict is not a recognised value.
108
+ */
109
+ export function verdictToSignal(verdict) {
110
+ if (verdict === REVIEW_VERDICTS.UP) {
111
+ return REVIEW_SIGNALS.POSITIVE;
112
+ }
113
+ if (verdict === REVIEW_VERDICTS.DOWN) {
114
+ return REVIEW_SIGNALS.NEGATIVE;
115
+ }
116
+ throw new Error(`Invalid review verdict: ${verdict}`);
117
+ }
118
+
119
+ /**
120
+ * Inverse of {@link verdictToSignal} — translate a persisted signal back to the
121
+ * app-layer verdict (used when composing reviews into an API response).
122
+ *
123
+ * @param {string} signal - one of {@link REVIEW_SIGNALS}.
124
+ * @returns {string} the corresponding {@link REVIEW_VERDICTS} value.
125
+ * @throws {Error} if the signal is not a recognised value.
126
+ */
127
+ export function signalToVerdict(signal) {
128
+ if (signal === REVIEW_SIGNALS.POSITIVE) {
129
+ return REVIEW_VERDICTS.UP;
130
+ }
131
+ if (signal === REVIEW_SIGNALS.NEGATIVE) {
132
+ return REVIEW_VERDICTS.DOWN;
133
+ }
134
+ throw new Error(`Invalid review signal: ${signal}`);
135
+ }
136
+
137
+ /**
138
+ * Map a raw `feedback_event` row (snake_case, as returned by PostgREST) to the
139
+ * **API review view** (camelCase, verdict-flavoured) for HTTP responses — e.g.
140
+ * the `?include=reviews` composition in spacecat-api-service. This is NOT the
141
+ * JSONL export shape; use {@link toJsonlRow} for the Learning Agent export. The
142
+ * heavy patch fields (`previous_fix` / `edited_fix`) are omitted unless
143
+ * `includePatches` is set.
144
+ *
145
+ * @param {object} row - a raw feedback_event row from PostgREST.
146
+ * @param {object} [options]
147
+ * @param {boolean} [options.includePatches=false] - include previous/edited fix.
148
+ * @returns {object|null} the review view object, or null when no row is given.
149
+ */
150
+ export function toReviewView(row, { includePatches = false } = {}) {
151
+ if (!row) {
152
+ return null;
153
+ }
154
+
155
+ const view = {
156
+ eventId: row.event_id,
157
+ eventTime: row.event_time,
158
+ source: row.source,
159
+ verdict: signalToVerdict(row.signal),
160
+ signal: row.signal,
161
+ reviewerId: row.reviewer_id ?? null,
162
+ detailMarkdown: row.detail_markdown ?? null,
163
+ rejectionCategory: row.rejection_category ?? null,
164
+ stateTransition: row.state_transition ?? null,
165
+ tier: row.tier,
166
+ };
167
+
168
+ if (includePatches) {
169
+ view.previousFix = row.previous_fix ?? null;
170
+ view.editedFix = row.edited_fix ?? null;
171
+ }
172
+
173
+ return view;
174
+ }
175
+
176
+ /**
177
+ * Whether a raw `feedback_event` row should be exported to the Learning Agent
178
+ * corpus. `product_bug` rejections are excluded (routed to Jira); NULL-category
179
+ * rows ARE exported. See {@link EXPORT_EXCLUDED_REJECTION_CATEGORIES}.
180
+ *
181
+ * @param {object} row - a raw feedback_event row from PostgREST.
182
+ * @returns {boolean} true if the row should be exported.
183
+ */
184
+ export function shouldExport(row) {
185
+ return !EXPORT_EXCLUDED_REJECTION_CATEGORIES.includes(row.rejection_category);
186
+ }
187
+
188
+ /**
189
+ * Map a raw `feedback_event` row (snake_case, from PostgREST) to one JSONL line
190
+ * object — the canonical Learning Agent export contract. The row mirrors the
191
+ * feedback_event columns (snake_case), stamped with {@link SCHEMA_VERSION} and
192
+ * the derived `verdict`. For organizations that have NOT opted into training,
193
+ * the customer-derived fields in {@link OPT_OUT_STRIPPED_FIELDS} are nulled;
194
+ * verdict / signal / category / identity metadata still ship (§10.5.7).
195
+ *
196
+ * This is the single source of truth for the JSONL row shape — the exporter
197
+ * (spacecat-jobs-dispatcher) calls it rather than re-deriving the contract.
198
+ *
199
+ * @param {object} row - a raw feedback_event row from PostgREST.
200
+ * @param {object} [options]
201
+ * @param {boolean} [options.optedIn=false] - the org's training_opt_in.
202
+ * @returns {object} a single JSONL line object (snake_case).
203
+ */
204
+ export function toJsonlRow(row, { optedIn = false } = {}) {
205
+ const jsonlRow = {
206
+ schema_version: SCHEMA_VERSION,
207
+ event_id: row.event_id,
208
+ event_time: row.event_time,
209
+ organization_id: row.organization_id,
210
+ site_id: row.site_id,
211
+ suggestion_id: row.suggestion_id,
212
+ opportunity_type: row.opportunity_type,
213
+ source: row.source,
214
+ signal: row.signal,
215
+ verdict: signalToVerdict(row.signal),
216
+ reviewer_id: row.reviewer_id ?? null,
217
+ rejection_category: row.rejection_category ?? null,
218
+ state_transition: row.state_transition ?? null,
219
+ tier: row.tier,
220
+ detail_markdown: row.detail_markdown ?? null,
221
+ previous_fix: row.previous_fix ?? null,
222
+ edited_fix: row.edited_fix ?? null,
223
+ };
224
+
225
+ if (!optedIn) {
226
+ OPT_OUT_STRIPPED_FIELDS.forEach((field) => {
227
+ jsonlRow[field] = null;
228
+ });
229
+ }
230
+
231
+ return jsonlRow;
232
+ }
233
+
234
+ /**
235
+ * Build the JSONL (newline-delimited JSON) export body from raw feedback_event
236
+ * rows: filters out non-exportable rows ({@link shouldExport}) and maps the rest
237
+ * through {@link toJsonlRow}.
238
+ *
239
+ * @param {Array<object>} rows - raw feedback_event rows from PostgREST.
240
+ * @param {object} [options]
241
+ * @param {boolean} [options.optedIn=false] - the org's training_opt_in.
242
+ * @returns {string} the JSONL body (no trailing newline).
243
+ */
244
+ export function buildJsonl(rows, { optedIn = false } = {}) {
245
+ return rows
246
+ .filter(shouldExport)
247
+ .map((row) => JSON.stringify(toJsonlRow(row, { optedIn })))
248
+ .join('\n');
249
+ }
@@ -0,0 +1,85 @@
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
+ export const REVIEW_SOURCES: Readonly<{
14
+ BACKOFFICE: 'backoffice';
15
+ ASO_UI: 'aso_ui';
16
+ AEMY_PR: 'aemy_pr';
17
+ }>;
18
+
19
+ export const REVIEW_VERDICTS: Readonly<{ UP: 'up'; DOWN: 'down' }>;
20
+
21
+ export const REVIEW_SIGNALS: Readonly<{ POSITIVE: 'positive'; NEGATIVE: 'negative' }>;
22
+
23
+ export const REJECTION_CATEGORIES: Readonly<{
24
+ PRODUCT_BUG: 'product_bug';
25
+ BAD_RECOMMENDATION: 'bad_recommendation';
26
+ OTHER: 'other';
27
+ }>;
28
+
29
+ export const FEEDBACK_TIERS: Readonly<{ PAID: 'paid'; FREE: 'free' }>;
30
+
31
+ export const EXPORT_EXCLUDED_REJECTION_CATEGORIES: ReadonlyArray<string>;
32
+
33
+ /** snake_case JSONL/DB field names stripped from the export for opted-out orgs. */
34
+ export const OPT_OUT_STRIPPED_FIELDS: ReadonlyArray<string>;
35
+
36
+ /** Current JSONL row schema version stamped on every exported line. */
37
+ export const SCHEMA_VERSION: number;
38
+
39
+ /** Translate the app-layer verdict (`up`/`down`) to the persisted signal. */
40
+ export function verdictToSignal(verdict: string): 'positive' | 'negative';
41
+
42
+ /** Translate a persisted signal (`positive`/`negative`) back to the verdict. */
43
+ export function signalToVerdict(signal: string): 'up' | 'down';
44
+
45
+ export interface ReviewView {
46
+ eventId: string;
47
+ eventTime: string;
48
+ source: string;
49
+ verdict: 'up' | 'down';
50
+ signal: 'positive' | 'negative';
51
+ reviewerId: string | null;
52
+ detailMarkdown: string | null;
53
+ rejectionCategory: string | null;
54
+ stateTransition: string | null;
55
+ tier: string;
56
+ previousFix?: unknown;
57
+ editedFix?: unknown;
58
+ }
59
+
60
+ /**
61
+ * Map a raw PostgREST `feedback_event` row to the API review view (HTTP
62
+ * responses). Returns null when no row is given. NOT for JSONL export.
63
+ */
64
+ export function toReviewView(
65
+ row: Record<string, unknown>,
66
+ options?: { includePatches?: boolean },
67
+ ): ReviewView | null;
68
+
69
+ /** Whether a raw feedback_event row should be exported to the LA corpus. */
70
+ export function shouldExport(row: Record<string, unknown>): boolean;
71
+
72
+ /**
73
+ * Map a raw PostgREST `feedback_event` row to one JSONL export line (snake_case,
74
+ * schema_version-stamped). Customer-derived fields are nulled when optedIn=false.
75
+ */
76
+ export function toJsonlRow(
77
+ row: Record<string, unknown>,
78
+ options?: { optedIn?: boolean },
79
+ ): Record<string, unknown>;
80
+
81
+ /** Build the JSONL export body (filtered + mapped) from raw feedback_event rows. */
82
+ export function buildJsonl(
83
+ rows: Array<Record<string, unknown>>,
84
+ options?: { optedIn?: boolean },
85
+ ): string;
@@ -0,0 +1,34 @@
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
+ // feedback_event has a client-supplied PK and is append-only, so it is NOT a
14
+ // data-access entity (no model/collection/schema/registry). This barrel only
15
+ // re-exports the shared constants + helpers consumed by spacecat-api-service
16
+ // (capture + ?include=reviews) and spacecat-jobs-dispatcher (the JSONL exporter):
17
+ // the enum vocabulary, the verdict<->signal translation, the API review view,
18
+ // and the single-sourced JSONL row contract.
19
+ export {
20
+ REVIEW_SOURCES,
21
+ REVIEW_VERDICTS,
22
+ REVIEW_SIGNALS,
23
+ REJECTION_CATEGORIES,
24
+ FEEDBACK_TIERS,
25
+ EXPORT_EXCLUDED_REJECTION_CATEGORIES,
26
+ OPT_OUT_STRIPPED_FIELDS,
27
+ SCHEMA_VERSION,
28
+ verdictToSignal,
29
+ signalToVerdict,
30
+ toReviewView,
31
+ shouldExport,
32
+ toJsonlRow,
33
+ buildJsonl,
34
+ } from './feedback-event.constants.js';
@@ -26,6 +26,7 @@ export * from './geo-experiment/index.js';
26
26
  export * from './fix-entity/index.js';
27
27
  export * from './fix-entity-suggestion/index.js';
28
28
  export * from './experiment/index.js';
29
+ export * from './feedback-event/index.js';
29
30
  export * from './import-job/index.js';
30
31
  export * from './import-url/index.js';
31
32
  export * from './key-event/index.js';
@@ -436,15 +436,30 @@ export const DATA_SCHEMAS = {
436
436
  },
437
437
  },
438
438
  },
439
+ // Sitemap has two data shapes from the audit worker:
440
+ // 1. URL-type (type='url'): sitemapUrl + pageUrl identify a probed page issue
441
+ // 2. Error-type (type='error'): site/sitemap infrastructure failures; sitemapUrl may be empty
439
442
  [OPPORTUNITY_TYPES.SITEMAP]: {
440
443
  schema: Joi.object({
441
- sitemapUrl: Joi.string().uri().required(),
442
- pageUrl: Joi.string().uri().required(),
443
444
  type: Joi.string().valid('url', 'error').optional(),
445
+ error: Joi.when('type', {
446
+ is: 'error',
447
+ then: Joi.string().required(),
448
+ otherwise: Joi.string().optional(),
449
+ }),
450
+ sitemapUrl: Joi.when('type', {
451
+ is: 'error',
452
+ then: Joi.string().uri().allow('').optional(),
453
+ otherwise: Joi.string().uri().required(),
454
+ }),
455
+ pageUrl: Joi.when('type', {
456
+ is: 'error',
457
+ then: Joi.string().uri().allow('').optional(),
458
+ otherwise: Joi.string().uri().required(),
459
+ }),
444
460
  statusCode: Joi.number().optional(),
445
- urlsSuggested: Joi.string().uri().optional(),
446
- recommendedAction: Joi.string().optional(),
447
- error: Joi.string().optional(),
461
+ urlsSuggested: Joi.string().uri().allow('').optional(),
462
+ recommendedAction: Joi.string().allow('').optional(),
448
463
  aggregationKey: Joi.string().allow(null).optional(),
449
464
  }).unknown(true),
450
465
  projections: {