@tokenoftrust/storefront-runner 2.4.40 → 2.4.42

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.
@@ -82,6 +82,18 @@ export const CATALOG_MODEL = {
82
82
  "unit?": "string",
83
83
  },
84
84
  },
85
+ CatalogSkuReference: {
86
+ union: [
87
+ { status: '"assigned" | "retired"', sku: "string", profileId: "string", profileVersion: "number",
88
+ acceptedRevisionId: "string", revisionStatus: '"current" | "historical"', itemId: "string",
89
+ provenance: '"catalog-reviewed-assignment"', findings: "string[]" },
90
+ { status: '"suggested"', sku: "string", profileId: "string", profileVersion: "number",
91
+ acceptedRevisionId: "string", itemId: "string", provenance: '"catalog-preview"', findings: "string[]" },
92
+ { status: '"review-needed"', sku: "null", profileId: "string | null", profileVersion: "number | null",
93
+ acceptedRevisionId: "string", itemId: "string", provenance: '"catalog-preview"', findings: "string[]" },
94
+ ],
95
+ },
96
+ MerchantSkuOrigin: { alias: '"tot-adopted" | "merchant-override"' },
85
97
  CatalogLink: {
86
98
  fields: {
87
99
  item_id: "string",
@@ -91,6 +103,7 @@ export const CATALOG_MODEL = {
91
103
  "locale?": "string",
92
104
  accepted_at: "string",
93
105
  facts: "CatalogFact[]",
106
+ "sku_reference?": "CatalogSkuReference",
94
107
  "setup_policy?": "CatalogSetupProvenance",
95
108
  },
96
109
  },
@@ -110,6 +123,7 @@ export const CATALOG_MODEL = {
110
123
  product_id: "string",
111
124
  source: "SourceRef",
112
125
  "sku?": "string",
126
+ "sku_origin?": "MerchantSkuOrigin",
113
127
  title: "string",
114
128
  price: "number",
115
129
  "compare_at_price?": "number",
@@ -288,6 +302,9 @@ function checkType(type, value, path, issues) {
288
302
  return;
289
303
  }
290
304
  switch (t) {
305
+ case "null":
306
+ if (value !== null) issues.push({ path, message: "expected null" });
307
+ return;
291
308
  case "unknown":
292
309
  return;
293
310
  case "string":
@@ -14,7 +14,7 @@
14
14
  "preview": "wrangler dev",
15
15
  "typecheck": "astro check && tsc --noEmit",
16
16
  "astro": "astro",
17
- "test": "NODE_OPTIONS=--experimental-sqlite vitest run && pnpm run test:pipeline && pnpm run test:pipeline-faults"
17
+ "test": "NODE_OPTIONS=--experimental-sqlite vitest run --exclude src/lib/apps/v84CleanCloneReplay.test.ts --exclude src/lib/apps/v84NonportableRouteDogfood.test.ts --exclude src/lib/apps/v84CloneLifecycleEvidence.test.ts && NODE_OPTIONS=--experimental-sqlite vitest run src/lib/apps/v84CleanCloneReplay.test.ts src/lib/apps/v84NonportableRouteDogfood.test.ts src/lib/apps/v84CloneLifecycleEvidence.test.ts --fileParallelism=false && pnpm run test:pipeline && pnpm run test:pipeline-faults"
18
18
  },
19
19
  "dependencies": {
20
20
  "@astrojs/cloudflare": "^14.0.1",
@@ -9,7 +9,6 @@ interface MethodOption { id: string; title: string; reason: string }
9
9
  interface SettingsResponse extends SettingsPayload {
10
10
  ok: true;
11
11
  pricingMethods: MethodOption[];
12
- skuMethods: MethodOption[];
13
12
  history: Array<{ revision: number; savedAt: string; actor: string }>;
14
13
  historyUnavailable: boolean;
15
14
  }
@@ -19,10 +18,8 @@ interface PreviewResponse {
19
18
  price: number | null; cost: number | null; profit: number | null; margin: number | null;
20
19
  calculation: string; issues: string[]; ruleExplanation: string;
21
20
  } };
22
- sku?: { status: string; value?: string };
23
21
  }
24
22
  type Child = Node | string | null | undefined | false;
25
- type Section = "pricing" | "sku";
26
23
  type RecordValue = Record<string, unknown>;
27
24
 
28
25
  function h<K extends keyof HTMLElementTagNameMap>(tag: K, attrs: Record<string, string | boolean | undefined> = {}, ...children: Child[]): HTMLElementTagNameMap[K] {
@@ -39,13 +36,12 @@ const METHOD_FIELDS: Record<string, [string, string, string]> = {
39
36
  };
40
37
  const METHOD_TITLES: Record<string, string> = {
41
38
  msrp: "Supplier MSRP", margin: "Target margin", markup: "Markup over cost", "msrp-adjustment": "Adjust MSRP", fixed: "Add a fixed amount",
42
- sequential: "Simple numbers", family: "Product families", hybrid: "Category + number", attributes: "Product attributes", supplier: "Supplier codes", random: "Short random codes", deterministic: "Stable identity codes",
43
39
  };
44
40
  const FIELD_LABELS: Record<string, string> = {
45
41
  targetMargin: "Target gross margin", markup: "Markup over cost", msrpAdjustment: "MSRP adjustment", fixedMarkup: "Amount added to cost",
46
42
  additionalCost: "Additional cost per item", additionalCostPercent: "Additional cost percentage", minimumPrice: "Minimum price", maximumPrice: "Maximum price", minimumMargin: "Minimum gross margin",
47
43
  costBasis: "Cost basis", rounding: "Price format", increment: "Rounding increment", ending: "Price ending", direction: "Round direction", missingData: "Missing price source", fallbackMethod: "Fallback pricing method", updatePolicy: "Update preference",
48
- startingNumber: "Starting number", prefix: "SKU prefix", separator: "SKU separator", method: "Method", productKey: "Product ID or SKU", selectedRuleId: "Matching rule",
44
+ method: "Method", productKey: "Product ID or SKU", selectedRuleId: "Matching rule",
49
45
  name: "Rule name", scope: "Match by", match: "Matching value", minimumCost: "Cost from", maximumCost: "Cost below", price: "Selling price",
50
46
  wholesale: "Wholesale cost", msrp: "Supplier MSRP", minimum: "Product minimum price", maximum: "Product maximum price",
51
47
  };
@@ -76,10 +72,8 @@ export function initCatalogSettingsPage(root: HTMLElement): void {
76
72
  let state: SettingsEditorState | null = null;
77
73
  let storeCurrency = root.dataset.currency ?? "";
78
74
  let pricingMethods: MethodOption[] = [];
79
- let skuMethods: MethodOption[] = [];
80
75
  let history: SettingsResponse["history"] = [];
81
76
  let historyUnavailable = false;
82
- let activeTab: Section = window.location.hash === "#sku" ? "sku" : "pricing";
83
77
  let saving = false;
84
78
  let loading = false;
85
79
  let feedback = "";
@@ -93,19 +87,19 @@ export function initCatalogSettingsPage(root: HTMLElement): void {
93
87
  const saveStatus = h("div", { class: "save-status", "aria-live": "polite" });
94
88
  const saveButton = h("button", { type: "button", class: "btn primary", "data-save": "true" }, "Save defaults");
95
89
  const discardButton = h("button", { type: "button", class: "btn secondary", "data-discard": "true" }, "Discard changes");
96
- const tabs = h("div", { class: "settings-tabs", role: "tablist", "aria-label": "Catalog defaults" });
97
90
  const money = (value: number, currency = state?.currency ?? storeCurrency) => {
98
91
  try { return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(value); }
99
92
  catch { return `${currency} ${value.toFixed(2)}`; }
100
93
  };
101
94
  const currencyMismatch = () => Boolean(state && storeCurrency && state.currency !== storeCurrency);
102
- const isConfigured = (section: Section) => state?.draft[section === "pricing" ? "pricingConfigured" : "skuConfigured"] === true;
103
- const methodTitle = (id: string) => METHOD_TITLES[id] ?? [...pricingMethods, ...skuMethods].find((method) => method.id === id)?.title ?? "Saved method";
95
+ const isConfigured = () => state?.draft.pricingConfigured === true;
96
+ const methodTitle = (id: string) => METHOD_TITLES[id] ?? pricingMethods.find((method) => method.id === id)?.title ?? "Saved method";
104
97
 
105
98
  function paintStatus(): void {
106
99
  if (!state) return;
107
100
  const dirty = draftIsDirty(state);
108
101
  const notices: Node[] = [];
102
+ if (state.savedEngineVersion && state.savedEngineVersion !== state.engineVersion) notices.push(h("p", { class: "notice", role: "status" }, "Saved pricing defaults use an older calculation engine. Review your pricing method before saving this revision under the current engine. Existing listings and merchant SKUs stay unchanged."));
109
103
  if (currencyMismatch()) notices.push(h("p", { class: "notice error", role: "alert" }, `Saved defaults use ${state.currency}; this store uses ${storeCurrency}. Saving and preview are paused. No currency conversion is applied.`));
110
104
  if (state.conflict) {
111
105
  const reload = h("button", { type: "button", class: "btn secondary", disabled: saving || loading, "data-reload": "true" }, "Load saved defaults");
@@ -163,45 +157,42 @@ export function initCatalogSettingsPage(root: HTMLElement): void {
163
157
  const field = METHOD_FIELDS[fieldString(record, "method")];
164
158
  return field ? numberField(record, field[0], field[1], touch, field[2], `${prefix}${field[0]}`) : null;
165
159
  }
166
- function examples(section: Section): Record<string, string> {
167
- return section === "pricing" ? {
168
- msrp: `${money(60)} MSRP → ${money(60)}`, margin: `${money(40)} cost · 40% → ${money(66.67)}`,
169
- markup: `${money(40)} cost · 50% → ${money(60)}`, "msrp-adjustment": `${money(60)} MSRP · −10% → ${money(54)}`, fixed: `${money(40)} cost + ${money(15)} → ${money(55)}`,
170
- } : { sequential: "SKU-004821", family: "P004821-V03", hybrid: "TSH-004821", attributes: "AC-TS42-BLK-M", supplier: "SUP7-AX204", random: "K7MX4R9T", deterministic: "P-7K3M9X2Q" };
171
- }
172
- function methodPicker(section: Section): HTMLElement {
160
+ function methodPicker(): HTMLElement {
173
161
  if (!state) return h("div");
174
- const record = state.draft[section];
175
- const configured = isConfigured(section);
176
- const all = section === "pricing" ? pricingMethods : skuMethods;
177
- const commonIds = section === "pricing" ? ["msrp", "margin", "markup"] : ["sequential", "family", "hybrid"];
178
- const common = all.filter((method) => commonIds.includes(method.id));
179
- const more = all.filter((method) => !commonIds.includes(method.id));
180
- const sampleExamples = examples(section);
162
+ const record = state.draft.pricing;
163
+ const configured = isConfigured();
164
+ const commonIds = ["msrp", "margin", "markup"];
165
+ const common = pricingMethods.filter((method) => commonIds.includes(method.id));
166
+ const more = pricingMethods.filter((method) => !commonIds.includes(method.id));
167
+ const sampleExamples: Record<string, string> = {
168
+ msrp: `${money(60)} MSRP → ${money(60)}`, margin: `${money(40)} cost · 40% → ${money(66.67)}`,
169
+ markup: `${money(40)} cost · 50% → ${money(60)}`, "msrp-adjustment": `${money(60)} MSRP · −10% → ${money(54)}`,
170
+ fixed: `${money(40)} cost + ${money(15)} → ${money(55)}`,
171
+ };
181
172
  const reasons: Record<string, string> = {
182
- msrp: "Start with the supplier’s suggested retail price.", margin: "Keep a consistent share of every sale.", markup: "Add a percentage to what you paid.", sequential: "A dependable format for any catalog.", family: "Keep related variants easy to recognize.", hybrid: "A readable prefix with a simple number.",
173
+ msrp: "Start with the supplier’s suggested retail price.", margin: "Keep a consistent share of every sale.", markup: "Add a percentage to what you paid.",
183
174
  };
184
175
  const card = (method: MethodOption) => {
185
176
  const selected = configured && record.method === method.id;
186
- const radio = h("input", { type: "radio", name: `${section}-method`, value: method.id, "data-focus": `${section}-${method.id}` });
177
+ const radio = h("input", { type: "radio", name: "pricing-method", value: method.id, "data-focus": `pricing-${method.id}` });
187
178
  radio.checked = selected;
188
- radio.addEventListener("change", () => { if (state) { selectDraftMethod(state, section, method.id); touch(); renderEditor(); } });
179
+ radio.addEventListener("change", () => { if (state) { selectDraftMethod(state, "pricing", method.id); touch(); renderEditor(); } });
189
180
  return h("label", { class: `method-card${selected ? " selected" : ""}` }, radio,
190
181
  h("span", { class: "method-title" }, methodTitle(method.id)), h("span", { class: "method-example" }, sampleExamples[method.id] ?? "Sample format"), h("span", { class: "method-reason" }, reasons[method.id] ?? method.reason));
191
182
  };
192
- const manual = h("button", { type: "button", class: "text-button", "data-focus": `${section}-manual`, "aria-pressed": String(!configured) }, section === "pricing" ? "Use manual prices instead" : "Enter SKUs manually instead");
183
+ const manual = h("button", { type: "button", class: "text-button", "data-focus": "pricing-manual", "aria-pressed": String(!configured) }, "Use manual prices instead");
193
184
  manual.addEventListener("click", () => {
194
185
  if (!state) return;
195
- state.draft[section === "pricing" ? "pricingConfigured" : "skuConfigured"] = false;
186
+ state.draft.pricingConfigured = false;
196
187
  touch(); renderEditor();
197
188
  });
198
- return h("section", { class: "editor-panel method-panel", "aria-labelledby": `${section}-method-heading` },
199
- h("div", { class: "section-heading" }, h("span", { class: "step-number", "aria-hidden": "true" }, "1"), h("h2", { id: `${section}-method-heading` }, section === "pricing" ? "Choose your pricing method" : "Choose your SKU format")),
200
- h("p", { class: "section-intro" }, section === "pricing" ? "One starting point for every new product." : "An item code that works for the way you sell."),
201
- !configured ? h("p", { class: "manual-note" }, section === "pricing" ? "Manual pricing is on. Choose a method to set a default." : "SKUs are entered manually. Choose a format to set a default.") : null,
189
+ return h("section", { class: "editor-panel method-panel", "aria-labelledby": "pricing-method-heading" },
190
+ h("div", { class: "section-heading" }, h("span", { class: "step-number", "aria-hidden": "true" }, "1"), h("h2", { id: "pricing-method-heading" }, "Choose your pricing method")),
191
+ h("p", { class: "section-intro" }, "One starting point for every new product."),
192
+ !configured ? h("p", { class: "manual-note" }, "Manual pricing is on. Choose a method to set a default.") : null,
202
193
  h("div", { class: "method-grid" }, ...common.map(card)),
203
- disclosure(`${section}-more`, "More methods", section === "pricing" ? "Adjust MSRP or add a fixed amount" : "Attributes, suppliers and generated codes", [h("div", { class: "method-grid more-grid" }, ...more.map(card))], configured && more.some((method) => method.id === record.method)),
204
- configured && section === "pricing" && METHOD_FIELDS[String(record.method)] ? h("div", { class: "method-parameter" }, parameter(record)) : null,
194
+ disclosure("pricing-more", "More methods", "Adjust MSRP or add a fixed amount", [h("div", { class: "method-grid more-grid" }, ...more.map(card))], configured && more.some((method) => method.id === record.method)),
195
+ configured && METHOD_FIELDS[String(record.method)] ? h("div", { class: "method-parameter" }, parameter(record)) : null,
205
196
  configured ? manual : null);
206
197
  }
207
198
 
@@ -225,7 +216,7 @@ export function initCatalogSettingsPage(root: HTMLElement): void {
225
216
  }));
226
217
  }
227
218
  function pricingControls(): HTMLElement[] {
228
- if (!state || !isConfigured("pricing")) return [];
219
+ if (!state || !isConfigured()) return [];
229
220
  const pricing = state.draft.pricing;
230
221
  const repaint = () => { touch(); renderEditor(); };
231
222
  const priceFormat = h("section", { class: "editor-panel" },
@@ -276,25 +267,6 @@ export function initCatalogSettingsPage(root: HTMLElement): void {
276
267
  ]);
277
268
  return [priceFormat, advanced, rules];
278
269
  }
279
- function skuControls(): HTMLElement[] {
280
- if (!state || !isConfigured("sku")) return [];
281
- const sku = state.draft.sku;
282
- const numbered = ["sequential", "family", "hybrid"].includes(String(sku.method));
283
- const separated = sku.method !== "random";
284
- const hasPrefix = !["attributes", "random"].includes(String(sku.method));
285
- return [h("section", { class: "editor-panel" },
286
- h("div", { class: "section-heading" }, h("span", { class: "step-number", "aria-hidden": "true" }, "2"), h("h2", {}, "Fine-tune the format")),
287
- h("div", { class: "fields-grid" },
288
- hasPrefix ? textField(sku, "prefix", sku.method === "supplier" ? "Supplier prefix" : sku.method === "hybrid" ? "Category prefix" : "Prefix", touch, { maxlength: "12", spellcheck: "false" }) : null,
289
- numbered ? numberField(sku, "startingNumber", "Starting number", touch, "A whole number. The preview pads it to six digits.") : null,
290
- separated ? select(sku, "separator", "Separator", [{ value: "-", label: "Hyphen (–)" }, { value: "_", label: "Underscore (_)" }, { value: "", label: "None" }]) : null),
291
- hasPrefix ? h("p", { class: "hint" }, "An optional prefix of up to 12 uppercase letters or digits, starting with a letter.") : null,
292
- !separated ? h("p", { class: "hint" }, "No format options needed. Use eight uppercase letters and digits.") : null,
293
- sku.method === "attributes" ? h("p", { class: "hint" }, "The sample illustrates brand, model, color and size abbreviations.") : null,
294
- sku.method === "family" ? h("p", { class: "hint" }, "V03 illustrates a variant within a numbered product family.") : null,
295
- sku.method === "supplier" ? h("p", { class: "hint" }, "AX204 illustrates a supplier’s item code.") : null,
296
- sku.method === "deterministic" ? h("p", { class: "hint" }, "The sample uses a fixed item identity. Generated codes still need a uniqueness check.") : null)];
297
- }
298
270
  function savedHistory(): HTMLElement {
299
271
  const entries: Node[] = [];
300
272
  if (historyUnavailable) entries.push(h("p", { class: "hint" }, "Saved history is unavailable."));
@@ -313,38 +285,16 @@ export function initCatalogSettingsPage(root: HTMLElement): void {
313
285
  const focusKey = focused && editor.contains(focused) ? focused.dataset.focus : undefined;
314
286
  const selection: [number | null, number | null] | null = focused?.tagName === "INPUT" && ["text", "search"].includes(focused.type) ? [focused.selectionStart, focused.selectionEnd] : null;
315
287
  editor.replaceChildren(
316
- h("div", { id: "settings-pricing-panel", role: "tabpanel", "aria-labelledby": "settings-pricing-tab", hidden: activeTab !== "pricing" }, methodPicker("pricing"), ...pricingControls()),
317
- h("div", { id: "settings-sku-panel", role: "tabpanel", "aria-labelledby": "settings-sku-tab", hidden: activeTab !== "sku" }, methodPicker("sku"), ...skuControls()),
318
- h("p", { class: "preserve-note" }, h("span", { "aria-hidden": "true" }, "✓"), " Saved listings keep their prices and SKUs. These defaults never overwrite them."), savedHistory());
288
+ h("div", { id: "settings-pricing-panel" }, methodPicker(), ...pricingControls()),
289
+ h("p", { class: "preserve-note" }, h("span", { "aria-hidden": "true" }, "✓"), " Saved listings keep their prices and merchant SKUs. Pricing defaults never overwrite them."), savedHistory());
319
290
  if (focusKey) {
320
291
  const control = Array.from(editor.querySelectorAll<HTMLElement>("[data-focus]")).find((node) => node.dataset.focus === focusKey);
321
292
  control?.focus({ preventScroll: true });
322
293
  if (selection && control instanceof HTMLInputElement) control.setSelectionRange(selection[0], selection[1]);
323
- if (!control && focusKey.endsWith("-manual")) editor.querySelector<HTMLInputElement>(`input[name="${activeTab}-method"]`)?.focus({ preventScroll: true });
294
+ if (!control && focusKey.endsWith("-manual")) editor.querySelector<HTMLInputElement>(`input[name="pricing-method"]`)?.focus({ preventScroll: true });
324
295
  }
325
296
  refreshRuleChoices();
326
297
  }
327
- function setTab(section: Section, focus = false, updateHash = false): void {
328
- activeTab = section;
329
- if (updateHash) window.history.replaceState(window.history.state, "", `#${section}`);
330
- for (const tab of tabs.querySelectorAll<HTMLButtonElement>("[data-tab]")) {
331
- const selected = tab.dataset.tab === section;
332
- tab.setAttribute("aria-selected", String(selected)); tab.tabIndex = selected ? 0 : -1;
333
- if (selected && focus) tab.focus();
334
- }
335
- for (const id of ["pricing", "sku"]) { const pane = editor.querySelector<HTMLElement>(`#settings-${id}-panel`); if (pane) pane.hidden = id !== section; }
336
- }
337
- for (const section of ["pricing", "sku"] as const) {
338
- const tab = h("button", { type: "button", role: "tab", id: `settings-${section}-tab`, "data-tab": section, "aria-controls": `settings-${section}-panel` }, section === "pricing" ? "Pricing" : "SKUs");
339
- tab.addEventListener("click", () => setTab(section, false, true));
340
- tab.addEventListener("keydown", (event) => {
341
- if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) {
342
- event.preventDefault(); setTab(event.key === "Home" ? "pricing" : event.key === "End" ? "sku" : activeTab === "pricing" ? "sku" : "pricing", true, true);
343
- }
344
- });
345
- tabs.append(tab);
346
- }
347
- window.addEventListener("hashchange", () => setTab(window.location.hash === "#sku" ? "sku" : "pricing"));
348
298
  const ruleSelect = select(sampleText, "selectedRuleId", "Matching rule", [{ value: "", label: "Automatically match" }, { value: "store", label: "Use store default" }], schedulePreview, "sample-selectedRuleId");
349
299
  function refreshRuleChoices(): void {
350
300
  const control = ruleSelect.querySelector("select");
@@ -363,7 +313,7 @@ export function initCatalogSettingsPage(root: HTMLElement): void {
363
313
  h("div", { class: "fields-grid" }, numberField(sample, "minimum", "Product minimum price", schedulePreview, "", "sample-minimum"), numberField(sample, "maximum", "Product maximum price", schedulePreview, "", "sample-maximum")),
364
314
  textField(sampleText, "productKey", "Product ID or SKU", schedulePreview, {}, "sample-productKey"),
365
315
  ...["assortment", "category", "brand"].map((key) => textField(sampleText, key, key.charAt(0).toUpperCase() + key.slice(1), schedulePreview, {}, `sample-${key}`)), ruleSelect]),
366
- h("p", { class: "preview-footnote" }, "A sample, not a catalog change. The SKU shown has not been reserved."));
316
+ h("p", { class: "preview-footnote" }, "A sample price; saving it does not change catalog references or existing listings."));
367
317
 
368
318
  function paintPreview(result: PreviewStatus<PreviewResponse>): void {
369
319
  previewOut.setAttribute("aria-busy", String(result.status === "waiting" || result.status === "loading"));
@@ -385,8 +335,7 @@ export function initCatalogSettingsPage(root: HTMLElement): void {
385
335
  previewOut.replaceChildren(
386
336
  h("div", { class: "preview-result" }, h("span", { class: "result-label" }, "Selling price"),
387
337
  h("strong", { class: `sample-price${!calculated ? " manual-result" : ""}` }, calculated ? calculated.price === null ? "Needs review" : money(calculated.price, body.currency) : "Manual price"),
388
- h("span", { class: "result-method" }, calculated ? "Your draft price" : "Choose a pricing method to calculate"), metrics,
389
- h("div", { class: "sku-result" }, h("span", { class: "result-label" }, "Sample SKU"), h("strong", {}, body.sku?.status === "ok" ? body.sku.value ?? "—" : "Entered manually"))),
338
+ h("span", { class: "result-method" }, calculated ? "Your draft price" : "Choose a pricing method to calculate"), metrics),
390
339
  ...(calculated?.calculation ? [h("p", { class: "calculation hint" }, calculated.calculation)] : []),
391
340
  ...(calculated?.ruleExplanation && calculated.ruleExplanation !== "Store default" ? [h("p", { class: "hint applied-rule" }, calculated.ruleExplanation)] : []),
392
341
  ...(calculated?.issues ?? []).map((issue) => h("p", { class: "notice review-note" }, issue)));
@@ -405,8 +354,8 @@ export function initCatalogSettingsPage(root: HTMLElement): void {
405
354
  preview.schedule({ ...draft, sample: { ...sample, ...Object.fromEntries(Object.entries(sampleText).filter(([, value]) => value !== "")) } });
406
355
  }
407
356
  function mount(): void {
408
- panel.replaceChildren(tabs, status, h("div", { class: "settings-layout" }, editor, previewPanel), h("div", { class: "save-bar" }, h("div", { class: "save-bar-inner" }, saveStatus, h("div", { class: "save-actions" }, discardButton, saveButton))));
409
- renderEditor(); setTab(activeTab); paintStatus(); schedulePreview();
357
+ panel.replaceChildren(status, h("div", { class: "settings-layout" }, editor, previewPanel), h("div", { class: "save-bar" }, h("div", { class: "save-bar-inner" }, saveStatus, h("div", { class: "save-actions" }, discardButton, saveButton))));
358
+ renderEditor(); paintStatus(); schedulePreview();
410
359
  }
411
360
  async function load(): Promise<void> {
412
361
  if (saving || loading) return;
@@ -416,7 +365,7 @@ export function initCatalogSettingsPage(root: HTMLElement): void {
416
365
  const response = await fetch(api, { headers: { accept: "application/json" } });
417
366
  const body = await response.json() as SettingsResponse & { message?: string };
418
367
  if (!response.ok || !body.ok) throw new Error(body.message || "Your defaults could not be loaded. Try again.");
419
- pricingMethods = body.pricingMethods; skuMethods = body.skuMethods; history = body.history; historyUnavailable = body.historyUnavailable;
368
+ pricingMethods = body.pricingMethods; history = body.history; historyUnavailable = body.historyUnavailable;
420
369
  state = editorFromPayload(body);
421
370
  if (typeof body.storeCurrency === "string") storeCurrency = body.storeCurrency;
422
371
  const badge = previewPanel.querySelector(".currency-badge"); if (badge) badge.textContent = state.currency;
@@ -14,6 +14,7 @@ export interface SettingsPayload {
14
14
  currency: string;
15
15
  storeCurrency?: string;
16
16
  engineVersion: string;
17
+ savedEngineVersion?: string | null;
17
18
  revision: number | null;
18
19
  pricing: SettingsSection | null;
19
20
  sku: SettingsSection | null;
@@ -33,6 +34,7 @@ export interface SettingsEditorState {
33
34
  savedRevision: number | null;
34
35
  currency: string;
35
36
  engineVersion: string;
37
+ savedEngineVersion: string | null;
36
38
  draft: CalculationDraft;
37
39
  saved: CalculationDraft | null;
38
40
  conflict: { revision: number | null; message: string } | null;
@@ -61,6 +63,7 @@ export function editorFromPayload(payload: SettingsPayload): SettingsEditorState
61
63
  savedRevision: payload.revision,
62
64
  currency: payload.currency,
63
65
  engineVersion: payload.engineVersion,
66
+ savedEngineVersion: payload.revision === null ? null : payload.savedEngineVersion ?? payload.engineVersion,
64
67
  draft,
65
68
  saved: structuredClone(draft),
66
69
  conflict: null,
@@ -114,6 +117,7 @@ export function applySaveResult(state: SettingsEditorState, result: SaveOutcome)
114
117
  contractRevision: state.contractRevision,
115
118
  currency: state.currency,
116
119
  engineVersion: state.engineVersion,
120
+ savedEngineVersion: state.engineVersion,
117
121
  revision: result.revision,
118
122
  pricing: result.pricing,
119
123
  sku: result.sku,
@@ -4,6 +4,16 @@
4
4
  * explicitly as a draft change. Catalog text is set as text, never parsed as HTML.
5
5
  */
6
6
 
7
+ /** The fields this review renders; the public runner does not bundle catalog setup. */
8
+ interface CatalogReferenceView {
9
+ status: "assigned" | "retired" | "suggested" | "review-needed";
10
+ sku: string | null;
11
+ profileId: string | null;
12
+ profileVersion: number | null;
13
+ revisionStatus?: "current" | "historical";
14
+ findings: string[];
15
+ }
16
+
7
17
  interface UpdatePlanProduct {
8
18
  handle: string;
9
19
  baseRevision: string;
@@ -14,6 +24,13 @@ interface UpdatesResponse {
14
24
  ok: boolean;
15
25
  message?: string;
16
26
  updates: Array<{ title: string; acceptedRevisionId: string; latestRevisionId: string }>;
27
+ referenceUpdates: Array<{
28
+ itemId: string;
29
+ title: string;
30
+ acceptedRevisionId: string;
31
+ saved: CatalogReferenceView | null;
32
+ incoming: CatalogReferenceView;
33
+ }>;
17
34
  unavailable?: Array<{ title: string; message: string }>;
18
35
  request?: Record<string, unknown>;
19
36
  plan: { products: UpdatePlanProduct[] } | null;
@@ -35,6 +52,15 @@ function el(tag: string, attrs: Record<string, string> = {}, ...children: Array<
35
52
  return node;
36
53
  }
37
54
 
55
+ function referenceLabel(reference: CatalogReferenceView | null): string {
56
+ if (!reference) return "none saved";
57
+ const code = reference.sku ?? "no approved code";
58
+ const status = reference.status === "suggested" ? "preview only" : reference.status.replace("-", " ");
59
+ const revision = reference.revisionStatus ? `, ${reference.revisionStatus} revision` : "";
60
+ const method = reference.profileId ? `, ${reference.profileId} v${reference.profileVersion}` : "";
61
+ return `${code} (${status}${revision}${method})`;
62
+ }
63
+
38
64
  export function initProductsTab(root: HTMLElement): void {
39
65
  const api = root.dataset.productsApi ?? "";
40
66
  const statusSelect = root.querySelector<HTMLSelectElement>("[data-products-status-select]");
@@ -81,21 +107,31 @@ export function initProductsTab(root: HTMLElement): void {
81
107
  return;
82
108
  }
83
109
  const nodes: Node[] = [];
110
+ const referenceUpdates = body.referenceUpdates ?? [];
84
111
  if (body.unavailable?.length) {
85
112
  nodes.push(el("p", { class: "subtle" }, `Not checked: ${body.unavailable.map((u) => `${u.title} (${u.message})`).join("; ")}.`));
86
113
  }
87
- if (body.updates.length === 0) {
88
- nodes.push(el("p", {}, "Every listed package is at the catalog's latest revision."));
114
+ if (body.updates.length === 0 && referenceUpdates.length === 0) {
115
+ nodes.push(el("p", {}, body.unavailable?.length
116
+ ? "No catalog update was found among the packages that could be checked."
117
+ : "Every listed package is at the catalog's latest revision."));
89
118
  box.replaceChildren(...nodes);
90
119
  return;
91
120
  }
92
- nodes.push(
121
+ if (body.updates.length) nodes.push(
93
122
  el("strong", {}, `${body.updates.length} package(s) have a newer catalog revision`),
94
- el(
95
- "ul",
96
- { class: "update-list" },
97
- ...body.updates.map((u) => el("li", {}, `${u.title}: revision ${u.acceptedRevisionId.slice(0, 8)} → ${u.latestRevisionId.slice(0, 8)}`)),
98
- ),
123
+ el("ul", { class: "update-list" },
124
+ ...body.updates.map((u) => el("li", {}, `${u.title}: revision ${u.acceptedRevisionId.slice(0, 8)} → ${u.latestRevisionId.slice(0, 8)}`))),
125
+ );
126
+ if (referenceUpdates.length) nodes.push(
127
+ el("strong", {}, `${referenceUpdates.length} catalog reference ${referenceUpdates.length === 1 ? "update" : "updates"} to review`),
128
+ el("ul", { class: "update-list" }, ...referenceUpdates.map((update) => el("li", {},
129
+ `${update.title}: ${referenceLabel(update.saved)} → ${referenceLabel(update.incoming)}. `,
130
+ `Exact package revision ${update.acceptedRevisionId.slice(0, 8)}.`,
131
+ ...(update.incoming.findings.length
132
+ ? [el("ul", {}, ...update.incoming.findings.map((finding) => el("li", {}, finding)))]
133
+ : []),
134
+ ))),
99
135
  );
100
136
  const plan = body.plan;
101
137
  if (!plan || !body.request) {
@@ -116,7 +152,8 @@ export function initProductsTab(root: HTMLElement): void {
116
152
  );
117
153
  }
118
154
  nodes.push(el("p", { class: "subtle" }, "Your SKUs, prices and the fields you set are kept. Accepting opens a draft change; nothing is published."));
119
- const accept = el("button", { type: "button", class: "btn primary" }, "Accept update as a draft") as HTMLButtonElement;
155
+ const accept = el("button", { type: "button", class: "btn primary" }, referenceUpdates.length && !body.updates.length
156
+ ? "Accept catalog reference update as a draft" : "Accept update as a draft") as HTMLButtonElement;
120
157
  accept.addEventListener("click", async () => {
121
158
  accept.disabled = true;
122
159
  const expected = Object.fromEntries(plan.products.map((p) => [p.handle, p.baseRevision]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.4.40",
3
+ "version": "2.4.42",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -158,6 +158,19 @@ export interface CatalogFact {
158
158
  * accepted. One variant lists exactly one packaged item. The accepted facts are stored here
159
159
  * so the product previews and publishes with the catalog unavailable.
160
160
  */
161
+ /** Server-verified shared catalog code at the exact accepted item revision. */
162
+ export type CatalogSkuReference =
163
+ | { status: "assigned" | "retired"; sku: string; profileId: string; profileVersion: number;
164
+ acceptedRevisionId: string; revisionStatus: "current" | "historical"; itemId: string;
165
+ provenance: "catalog-reviewed-assignment"; findings: string[] }
166
+ | { status: "suggested"; sku: string; profileId: string; profileVersion: number;
167
+ acceptedRevisionId: string; itemId: string; provenance: "catalog-preview"; findings: string[] }
168
+ | { status: "review-needed"; sku: null; profileId: string | null; profileVersion: number | null;
169
+ acceptedRevisionId: string; itemId: string; provenance: "catalog-preview"; findings: string[] };
170
+
171
+ /** Merchant's operational choice; it never changes the shared catalog reference. */
172
+ export type MerchantSkuOrigin = "tot-adopted" | "merchant-override";
173
+
161
174
  export interface CatalogLink {
162
175
  item_id: string;
163
176
  accepted_revision_id: string;
@@ -168,6 +181,8 @@ export interface CatalogLink {
168
181
  /** ISO 8601. */
169
182
  accepted_at: string;
170
183
  facts: CatalogFact[];
184
+ /** Independent of editable merchant SKU and revocable setup-policy acceptance. */
185
+ sku_reference?: CatalogSkuReference;
171
186
  /** Server-verified defaults adopted by the merchant; replayable without the current policy. */
172
187
  setup_policy?: CatalogSetupProvenance;
173
188
  }
@@ -204,6 +219,7 @@ export interface CatalogVariant {
204
219
  product_id: string;
205
220
  source: SourceRef;
206
221
  sku?: string;
222
+ sku_origin?: MerchantSkuOrigin;
207
223
  title: string;
208
224
  price: number;
209
225
  compare_at_price?: number;