← back to Designer Wallcoverings
fix(TK-11357/TK-11403/TK-11539): create DRAFT then gate before ACTIVE (activate-then-price reorder)
624ad3ff931e0c7e85763ca33a1c237d85a0b0de · 2026-09-14 15:26:18 -0700 · Steve
createProductWithMetafield no longer decides ACTIVE from a pre-gate price guess
(which let Shopify auto-create a $0 sellable variant that stayed ACTIVE +
published on a gate block / infra error — the TK-11357 recurrence). It now
always creates DRAFT and returns baseline eligibility flags; addSampleVariant
runs the price-integrity gate BEFORE the price write and returns {priced,
gateBlocked}; createOrGetProduct promotes to ACTIVE + publishes ONLY when
eligibleForActive && priced && !gateBlocked, else keeps DRAFT + Needs-Price-Review.
The catch path keeps the $0-orderable check fail-CLOSED even on gate infra error.
Includes the shared gate library it depends on (price-integrity-gate/cost/record,
weight-guard) and its negative tests (inject bad input -> gate goes red -> green).
KNOWN-OPEN (ticket stays open, see review clippings): gate returns ok:true on an
empty variants array (fail-open in the shared fn), and the bulk-import batch route
still bypasses the gate/weight/sample. Do not mark TK-11357/TK-11414 closed yet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.tsA DW-Programming/ImportNewSkufromURL/__tests__/shopify-activation-order.test.tsA DW-Programming/ImportNewSkufromURL/__tests__/weight-guard.test.tsA DW-Programming/ImportNewSkufromURL/gate-negtest.tsA DW-Programming/ImportNewSkufromURL/lib/price-integrity-cost.tsA DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.tsA DW-Programming/ImportNewSkufromURL/lib/price-integrity-record.tsM DW-Programming/ImportNewSkufromURL/lib/shopify.tsA DW-Programming/ImportNewSkufromURL/lib/weight-guard.ts
Diff
commit 624ad3ff931e0c7e85763ca33a1c237d85a0b0de
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Sep 14 15:26:18 2026 -0700
fix(TK-11357/TK-11403/TK-11539): create DRAFT then gate before ACTIVE (activate-then-price reorder)
createProductWithMetafield no longer decides ACTIVE from a pre-gate price guess
(which let Shopify auto-create a $0 sellable variant that stayed ACTIVE +
published on a gate block / infra error — the TK-11357 recurrence). It now
always creates DRAFT and returns baseline eligibility flags; addSampleVariant
runs the price-integrity gate BEFORE the price write and returns {priced,
gateBlocked}; createOrGetProduct promotes to ACTIVE + publishes ONLY when
eligibleForActive && priced && !gateBlocked, else keeps DRAFT + Needs-Price-Review.
The catch path keeps the $0-orderable check fail-CLOSED even on gate infra error.
Includes the shared gate library it depends on (price-integrity-gate/cost/record,
weight-guard) and its negative tests (inject bad input -> gate goes red -> green).
KNOWN-OPEN (ticket stays open, see review clippings): gate returns ok:true on an
empty variants array (fail-open in the shared fn), and the bulk-import batch route
still bypasses the gate/weight/sample. Do not mark TK-11357/TK-11414 closed yet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
.../__tests__/price-integrity-gate.test.ts | 226 ++++++++++
.../__tests__/shopify-activation-order.test.ts | 436 +++++++++++++++++++
.../__tests__/weight-guard.test.ts | 96 +++++
DW-Programming/ImportNewSkufromURL/gate-negtest.ts | 47 ++
.../lib/price-integrity-cost.ts | 84 ++++
.../lib/price-integrity-gate.ts | 476 +++++++++++++++++++++
.../lib/price-integrity-record.ts | 57 +++
DW-Programming/ImportNewSkufromURL/lib/shopify.ts | 53 ++-
.../ImportNewSkufromURL/lib/weight-guard.ts | 74 ++++
9 files changed, 1541 insertions(+), 8 deletions(-)
diff --git a/DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.ts b/DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.ts
new file mode 100644
index 00000000..cb1b8ce1
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.ts
@@ -0,0 +1,226 @@
+/**
+ * price-integrity-gate.test.ts - TK-11403 / TK-11431 rule 3 proof suite.
+ *
+ * Written for JEST (describe/it/expect) so it runs under the repo's real test
+ * command (npm test -> jest; jest.config.js testMatch covers this file):
+ * npx jest __tests__/price-integrity-gate.test.ts
+ *
+ * Imports ONLY the pure gate (no DB, no cost adapter). Every bug class has a
+ * NEGATIVE test that proves the gate goes RED on an injected fault, plus the
+ * headline sample/default-price LEAK cases (the bug this gate exists to stop).
+ */
+import { describe, it, expect } from '@jest/globals';
+import {
+ assertPriceIntegrity,
+ enforcePriceIntegrity,
+ PriceIntegrityError,
+ type PriceIntegrityInput,
+} from '../lib/price-integrity-gate';
+
+const clean: PriceIntegrityInput = {
+ dwSku: 'DWX-1',
+ netCost: 30,
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: 59, orderable: true },
+ ],
+};
+
+describe('assertPriceIntegrity', () => {
+ // ---- 1. POSITIVE clean ----
+ it('1. clean input → ok:true, 0 violations', () => {
+ const r = assertPriceIntegrity(clean);
+ expect(r.ok).toBe(true);
+ expect(r.violations).toHaveLength(0);
+ });
+
+ // ---- 2. NEGATIVE class A (sample mis-price) ----
+ // TK-11447: the old per-variant `sample === 4.25` equality was REPLACED by the
+ // product-level OUTCOME test, so the code changed (`sample-not-expected` →
+ // `no-sample-in-band`). The INTENT is unchanged: a $59 "sample" is outside the
+ // acceptable band [4.25, 50], so no customer can buy a memo sample at a sample
+ // price, and the product must still be BLOCKED.
+ it('2. sample priced 59 → ok:false, class A no-sample-in-band', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-1',
+ netCost: 30,
+ variants: [
+ { role: 'sample', price: 59, orderable: false },
+ { role: 'sellable', price: 59, orderable: true },
+ ],
+ });
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.class === 'A' && v.code === 'no-sample-in-band')).toBe(true);
+ });
+
+ // ---- 3. NEGATIVE class B (sellable at cost, below 1.5x) ----
+ it('3. sellable 30 at netCost 30 (1.0x) → ok:false, class B below-markup-floor', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-1',
+ netCost: 30,
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: 30, orderable: true },
+ ],
+ });
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.class === 'B' && v.code === 'below-markup-floor')).toBe(true);
+ });
+
+ // ---- 4. A-with-teeth: cost UNKNOWN + plausible price WARNs, does NOT block ----
+ // (updated from the old fail-closed 'cost-unknown' block per the
+ // 2026-09-11 DTD verdict — blocking on unknown cost would halt ~97 vendors.)
+ it('4. sellable 59 with netCost:null → ok:true AND warnings has cost-unverified (no block)', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-1',
+ netCost: null,
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: 59, orderable: true },
+ ],
+ });
+ expect(r.ok).toBe(true);
+ expect(r.violations).toHaveLength(0);
+ expect(r.warnings.some(w => w.class === 'B' && w.code === 'cost-unverified' && w.severity === 'warn')).toBe(true);
+ });
+
+ // ---- 5. NEGATIVE class C ($0 orderable) ----
+ it('5. sellable 0 orderable:true → ok:false, class C zero-price-orderable', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-1',
+ netCost: 30,
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: 0, orderable: true },
+ ],
+ });
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.class === 'C' && v.code === 'zero-price-orderable')).toBe(true);
+ });
+
+ // ---- 6. HEADLINE LEAK (numeric backstop): $4.25 sample price leaks as the
+ // sellable/roll price. This is the exact bug the gate exists to stop,
+ // and it used to pass clean when 4.25 >= 1.5x cost. ----
+ it('6. sellable 4.25 == sample price (netCost 2.50) → ok:false, class A sellable-equals-sample', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-1',
+ netCost: 2.5, // 1.5x = 3.75, so 4.25 clears Class B — magnitude alone misses it
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: 4.25, orderable: true },
+ ],
+ });
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.class === 'A' && v.code === 'sellable-equals-sample')).toBe(true);
+ });
+
+ // ---- 7. HEADLINE LEAK (provenance): a sellable whose price was DEFAULTED. ----
+ it("7. sellable priceSource:'defaulted' → ok:false, class A sellable-defaulted-price", () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-1',
+ netCost: 30,
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: 4.25, orderable: true, priceSource: 'defaulted' },
+ ],
+ });
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.class === 'A' && v.code === 'sellable-defaulted-price')).toBe(true);
+ });
+
+ // ---- 8. NEGATIVE class C (negative price gets its own honest code) ----
+ it('8. sellable -5 orderable:true → ok:false, class C negative-price (not zero-price)', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-1',
+ netCost: 30,
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: -5, orderable: true },
+ ],
+ });
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.class === 'C' && v.code === 'negative-price')).toBe(true);
+ expect(r.violations.some(v => v.code === 'zero-price-orderable')).toBe(false);
+ });
+
+ // ---- 9. POSITIVE: a legitimately cheap-ish product still passes ----
+ // (guards against the backstop over-blocking low-priced real goods.)
+ it('9. cheap-but-legit sellable 10 at netCost 2.50 (scraped) → ok:true', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-1',
+ netCost: 2.5,
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: 10, orderable: true, priceSource: 'scraped' },
+ ],
+ });
+ expect(r.ok).toBe(true);
+ expect(r.violations).toHaveLength(0);
+ });
+
+ // ---- 11. cost-FREE absolute-dollar floor: $2 orderable with UNKNOWN cost blocks ----
+ it('11. cost-unknown + $2 orderable → ok:false, block B below-absolute-floor', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-1',
+ netCost: null,
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: 2, orderable: true },
+ ],
+ });
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.class === 'B' && v.code === 'below-absolute-floor')).toBe(true);
+ });
+
+ // ---- 12. a $4 orderable with cost unknown is blocked by the floor (default 5.00) ----
+ it('12. cost-unknown + $4 orderable → blocked by absolute floor', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-1',
+ netCost: null,
+ variants: [{ role: 'sellable', price: 4, orderable: true }],
+ });
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.code === 'below-absolute-floor')).toBe(true);
+ });
+
+ // ---- 13. cost KNOWN, sellable at cost → still the markup-floor BLOCK (unchanged) ----
+ it('13. netCost 30 + sellable 30 → still ok:false below-markup-floor', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-1',
+ netCost: 30,
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: 30, orderable: true },
+ ],
+ });
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.class === 'B' && v.code === 'below-markup-floor')).toBe(true);
+ });
+
+ // ---- 14. clean input carries no warnings (shape guard) ----
+ it('14. clean input → warnings is empty', () => {
+ const r = assertPriceIntegrity(clean);
+ expect(r.warnings).toHaveLength(0);
+ });
+});
+
+describe('enforcePriceIntegrity', () => {
+ // ---- 10. throws on fail, not on clean ----
+ it('10. throws PriceIntegrityError (with .violations) on failing input, not on clean', () => {
+ expect(() => enforcePriceIntegrity(clean)).not.toThrow();
+
+ const failing: PriceIntegrityInput = {
+ dwSku: 'DWX-1',
+ netCost: 30,
+ variants: [{ role: 'sellable', price: 0, orderable: true }],
+ };
+ let caught: unknown;
+ try {
+ enforcePriceIntegrity(failing);
+ } catch (e) {
+ caught = e;
+ }
+ expect(caught).toBeInstanceOf(PriceIntegrityError);
+ expect((caught as PriceIntegrityError).violations.length).toBeGreaterThan(0);
+ });
+});
diff --git a/DW-Programming/ImportNewSkufromURL/__tests__/shopify-activation-order.test.ts b/DW-Programming/ImportNewSkufromURL/__tests__/shopify-activation-order.test.ts
new file mode 100644
index 00000000..4cf7dbb0
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/__tests__/shopify-activation-order.test.ts
@@ -0,0 +1,436 @@
+/**
+ * shopify-activation-order.test.ts
+ * ============================================================================
+ * Regression coverage for the activate-then-price ordering bug (2026-09-14 fix).
+ *
+ * BEFORE this fix: createProductWithMetafield() decided ACTIVE from a PRE-gate
+ * price guess (`numericUnitPrice > 4.25`) and productCreate() shipped NO priced
+ * variants, so Shopify auto-created the sellable variant at $0. The REAL price
+ * (after the price-integrity gate) was only written later in addSampleVariant(),
+ * which sat AFTER `if (gateBlocked) return;` and returned void — so a gate
+ * BLOCK, a gate infra error, or a failed price write all left an ACTIVE,
+ * all-channels-published $0 product live (TK-11357 recurrence).
+ *
+ * AFTER the fix: createProductWithMetafield() ALWAYS creates DRAFT.
+ * createOrGetProduct() promotes to ACTIVE + publishes ONLY when BOTH:
+ * (1) the baseline eligibility gate passed (hasImage && hasWidth && hasPrice)
+ * (2) addSampleVariant()'s result confirms priced:true AND gateBlocked:false
+ * Otherwise the product stays DRAFT, gets tagged 'Needs-Price-Review', and
+ * publishToAllChannels() is never called.
+ *
+ * MOCKING STRATEGY (call-site level, no real network / DB):
+ * - global.fetch is replaced with a single mock that inspects each GraphQL
+ * request's `query` text and returns a canned response per mutation/query
+ * name. This lets createOrGetProduct()'s REAL code run end-to-end (the
+ * exact code path that had the bug) while making zero real Shopify calls.
+ * - lib/dw-sku-registry, lib/sku-counter, lib/sku-history, and
+ * lib/price-integrity-record are jest.mock()'d — they are DB/file-writing
+ * adapters unrelated to the bug under test, and touching real Postgres or
+ * the shared price-integrity telemetry JSONL from a unit test would be
+ * unsafe/non-hermetic.
+ * - lib/price-integrity-gate and lib/price-integrity-cost are left REAL —
+ * the whole point of this suite is to prove the gate's real outcome is
+ * what decides ACTIVE vs DRAFT, not a mock of it. resolveNetCost() is
+ * pointed (via PRICE_FINDER_VERIFY_SCRIPT) at a nonexistent script so it
+ * fails fast/closed to null (a legitimate, already-covered fail-safe path
+ * in the gate: unknown cost = non-blocking WARN, never a block) instead of
+ * shelling out to the real price-finder skill against production data.
+ *
+ * LIMITATION (noted per the task's instruction to be explicit rather than ship
+ * a vacuous test): addProductImages()/uploadImageToShopify() — which runs
+ * AFTER the activation decision this suite covers — is exercised for real
+ * (dto.images is non-empty, to legitimately satisfy `hasImage`), but its
+ * network calls are NOT modeled beyond "return 404" for any non-GraphQL fetch.
+ * That is sufficient because (a) it only affects logging/best-effort image
+ * attachment, not the ACTIVE/DRAFT/publish decision under test, and (b) it
+ * already fails gracefully (try/catch, never throws) by design. It is not
+ * asserted on directly in this suite.
+ */
+import { describe, it, expect, beforeAll, beforeEach, jest } from '@jest/globals';
+import type { ProductDTO } from '../lib/types';
+
+// ---- jest.mock() calls are hoisted above imports/requires by Jest's babel
+// plugin, so these register before lib/shopify.ts (which imports these same
+// modules) is ever loaded — regardless of textual position in this file. ----
+jest.mock('../lib/dw-sku-registry', () => ({
+ assignDwSku: jest.fn(async (_vendorName: string, _mfrSku: string | null) => ({
+ dwSku: 'DWXX-999999',
+ vendorPrefix: 'DWXX',
+ isNew: true,
+ })),
+ registerShopifyInfo: jest.fn(async () => {}),
+}));
+
+jest.mock('../lib/sku-counter', () => ({
+ getNextSKU: jest.fn(async () => 'DWWC-500000'),
+ getCurrentSKU: jest.fn(async () => 'DWWC-500000'),
+}));
+
+jest.mock('../lib/sku-history', () => ({
+ addToHistory: jest.fn(async () => {}),
+ getHistory: jest.fn(async () => []),
+}));
+
+jest.mock('../lib/price-integrity-record', () => ({
+ recordOutcome: jest.fn(),
+}));
+
+const GRAPHQL_URL = 'https://test-store.myshopify.com/admin/api/2024-07/graphql.json';
+
+function graphqlOk(data: unknown) {
+ return {
+ ok: true,
+ status: 200,
+ json: async () => ({ data }),
+ };
+}
+
+let productCounter = 0;
+// (review 2026-09-14) When true, the productVariantsBulkCreate branch (the Sample
+// variant create) returns a real userError so the sample-create-failure path can be
+// exercised — proving a failed Sample keeps the product DRAFT (sampleOk fix).
+let FAIL_SAMPLE_CREATE = false;
+
+/** Builds a fresh fetch mock. One instance per test so call logs don't bleed across tests. */
+function makeFetchMock() {
+ return jest.fn(async (url: any, opts: any = {}) => {
+ if (url === GRAPHQL_URL) {
+ const body = JSON.parse(opts.body);
+ const q: string = body.query || '';
+ const vars = body.variables || {};
+
+ if (q.includes('mutation ProductCreate')) {
+ productCounter++;
+ return graphqlOk({
+ productCreate: {
+ product: {
+ id: `gid://shopify/Product/${productCounter}`,
+ handle: `test-handle-${productCounter}`,
+ status: 'DRAFT',
+ },
+ userErrors: [],
+ },
+ });
+ }
+ if (q.includes('query getProductVariants')) {
+ // A single existing variant that never matches unitLabel by name, so the
+ // real code's `findBySize(unitLabel) || existingVariants[0]` fallback
+ // exercises the same path regardless of the computed unit label.
+ return graphqlOk({
+ product: {
+ variants: {
+ edges: [
+ {
+ node: {
+ id: 'gid://shopify/ProductVariant/1',
+ title: 'Default Title',
+ selectedOptions: [{ name: 'Size', value: '__no_match__' }],
+ },
+ },
+ ],
+ },
+ },
+ });
+ }
+ if (q.includes('productVariantsBulkUpdate')) {
+ return graphqlOk({
+ productVariantsBulkUpdate: {
+ productVariants: [
+ {
+ id: vars.variants?.[0]?.id || 'gid://shopify/ProductVariant/1',
+ title: 'Roll',
+ price: vars.variants?.[0]?.price,
+ inventoryItem: {
+ id: 'gid://shopify/InventoryItem/1',
+ sku: vars.variants?.[0]?.inventoryItem?.sku,
+ },
+ },
+ ],
+ userErrors: [],
+ },
+ });
+ }
+ if (q.includes('productVariantsBulkCreate')) {
+ if (FAIL_SAMPLE_CREATE) {
+ // A real (non-"already exists") userError — the Sample variant was NOT created.
+ return graphqlOk({
+ productVariantsBulkCreate: {
+ productVariants: [],
+ userErrors: [{ field: ['variants'], message: 'Something went wrong creating the variant' }],
+ },
+ });
+ }
+ return graphqlOk({
+ productVariantsBulkCreate: {
+ productVariants: [
+ {
+ id: 'gid://shopify/ProductVariant/2',
+ title: 'Sample',
+ inventoryItem: {
+ id: 'gid://shopify/InventoryItem/2',
+ sku: vars.variants?.[0]?.inventoryItem?.sku,
+ },
+ },
+ ],
+ userErrors: [],
+ },
+ });
+ }
+ if (q.includes('getInventoryLevels')) {
+ return graphqlOk({
+ inventoryItem: {
+ id: vars.id,
+ inventoryLevels: {
+ edges: [
+ {
+ node: {
+ id: 'gid://shopify/InventoryLevel/1',
+ location: { id: 'gid://shopify/Location/1', name: 'Shop' },
+ },
+ },
+ ],
+ },
+ },
+ });
+ }
+ if (q.includes('inventoryAdjustQuantities')) {
+ return graphqlOk({
+ inventoryAdjustQuantities: {
+ inventoryAdjustmentGroup: { createdAt: new Date().toISOString(), reason: 'correction', changes: [] },
+ userErrors: [],
+ },
+ });
+ }
+ if (q.includes('mutation productUpdateStatus')) {
+ return graphqlOk({
+ productUpdate: {
+ product: { id: vars.input.id, status: vars.input.status },
+ userErrors: [],
+ },
+ });
+ }
+ if (q.includes('mutation tagsAdd')) {
+ return graphqlOk({
+ tagsAdd: { node: { id: vars.id }, userErrors: [] },
+ });
+ }
+ if (q.includes('publications(')) {
+ return graphqlOk({
+ publications: { edges: [{ node: { id: 'gid://shopify/Publication/1', name: 'Online Store' } }] },
+ });
+ }
+ if (q.includes('mutation productPublish')) {
+ return graphqlOk({
+ productPublish: { product: { id: vars.input.id }, userErrors: [] },
+ });
+ }
+ // Unknown GraphQL op — never throw, just return empty data.
+ return graphqlOk({});
+ }
+ // Non-GraphQL fetch (image download in addProductImages) — fail gracefully.
+ // addProductImages runs AFTER the activation decision under test and is
+ // already best-effort (try/catch, never throws) by design; see LIMITATION.
+ return { ok: false, status: 404, statusText: 'Not Found' };
+ });
+}
+
+/** Filters a fetch mock's calls to those whose GraphQL `query` contains `substr`. */
+function callsMatching(fetchMock: ReturnType<typeof makeFetchMock>, substr: string) {
+ return (fetchMock.mock.calls as any[]).filter(([url, opts]) => {
+ if (url !== GRAPHQL_URL) return false;
+ try {
+ return JSON.parse((opts as any).body).query.includes(substr);
+ } catch {
+ return false;
+ }
+ });
+}
+
+function baseDto(overrides: Partial<ProductDTO> = {}): ProductDTO {
+ return {
+ source_url: 'https://example.com/products/test-pattern',
+ fingerprint: `fp-${Math.random().toString(36).slice(2)}`,
+ title: 'Test Pattern Blue',
+ bodyHtml: '<p>A lovely wallcovering.</p>',
+ tags: ['blue', 'geometric'],
+ images: ['https://cdn.example.com/img1.jpg'],
+ specs: {
+ sku: 'TP-001',
+ brand: 'Test Vendor',
+ width: '27 Inches',
+ length: '27 Feet',
+ },
+ price: '54.00',
+ vendor: 'Test Vendor',
+ ...overrides,
+ } as ProductDTO;
+}
+
+let createOrGetProduct: (dto: ProductDTO) => Promise<any>;
+
+beforeAll(() => {
+ // Set BEFORE requiring lib/shopify.ts — it reads these at module top-level.
+ process.env.SHOPIFY_STORE_DOMAIN = 'test-store.myshopify.com';
+ process.env.SHOPIFY_ADMIN_ACCESS_TOKEN = 'test-token';
+ process.env.SHOPIFY_ADMIN_API_VERSION = '2024-07';
+ // Force resolveNetCost() to fail fast/closed to null (spawn ENOENT) instead
+ // of shelling out to the real price-finder skill against production data.
+ process.env.PRICE_FINDER_VERIFY_SCRIPT = '/nonexistent/verify-price-for-tests.js';
+
+ // Require (not import) so it loads AFTER the env vars above are set and
+ // AFTER the jest.mock() calls (hoisted) have registered.
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ createOrGetProduct = require('../lib/shopify').createOrGetProduct;
+});
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ FAIL_SAMPLE_CREATE = false;
+});
+
+describe('activate-then-price ordering fix (2026-09-14)', () => {
+ it('gate BLOCK: sub-$5-absolute-floor sellable never goes ACTIVE, never publishes, tagged for review', async () => {
+ // $4.50 clears the OLD pre-gate guess (`> 4.25`) so hasPrice=true and the
+ // baseline eligibility gate passes — but the REAL price-integrity gate's
+ // absolute-dollar floor ($5.00 default) blocks it. This is exactly the
+ // ordering bug: eligible-looking, but the real gate says no.
+ const fetchMock = makeFetchMock();
+ global.fetch = fetchMock as any;
+
+ const dto = baseDto({ price: '4.50', vendor: 'Test Vendor' });
+ const result = await createOrGetProduct(dto);
+
+ expect(result.product).toBeDefined();
+
+ // The sellable price write must NEVER have been attempted — the gate
+ // blocked before addSampleVariant's write step.
+ expect(callsMatching(fetchMock, 'productVariantsBulkUpdate')).toHaveLength(0);
+ expect(callsMatching(fetchMock, 'productVariantsBulkCreate')).toHaveLength(0);
+
+ // The product must NEVER be promoted to ACTIVE...
+ const statusCalls = callsMatching(fetchMock, 'mutation productUpdateStatus');
+ expect(statusCalls.every(([, opts]: any) => JSON.parse(opts.body).variables.input.status !== 'ACTIVE')).toBe(
+ true
+ );
+
+ // ...and must NEVER be published to any sales channel.
+ expect(callsMatching(fetchMock, 'mutation productPublish')).toHaveLength(0);
+
+ // It must be tagged for manual review.
+ const tagCalls = callsMatching(fetchMock, 'mutation tagsAdd');
+ expect(tagCalls.length).toBeGreaterThan(0);
+ const taggedWith = tagCalls.map(([, opts]: any) => JSON.parse(opts.body).variables.tags).flat();
+ expect(taggedWith).toContain('Needs-Price-Review');
+ }, 15000);
+
+ it('happy path: well-priced product with image+width promotes to ACTIVE + publishes with the real price written', async () => {
+ const fetchMock = makeFetchMock();
+ global.fetch = fetchMock as any;
+
+ const dto = baseDto({ price: '54.00', vendor: 'Test Vendor' });
+ const result = await createOrGetProduct(dto);
+
+ expect(result.product).toBeDefined();
+
+ // The real sellable price must have been written.
+ const priceWriteCalls = callsMatching(fetchMock, 'productVariantsBulkUpdate');
+ expect(priceWriteCalls).toHaveLength(1);
+ expect(JSON.parse(priceWriteCalls[0][1].body).variables.variants[0].price).toBe('54.00');
+
+ // The product must be promoted to ACTIVE...
+ const statusCalls = callsMatching(fetchMock, 'mutation productUpdateStatus');
+ expect(statusCalls).toHaveLength(1);
+ expect(JSON.parse(statusCalls[0][1].body).variables.input.status).toBe('ACTIVE');
+
+ // ...and published to all channels.
+ expect(callsMatching(fetchMock, 'mutation productPublish')).toHaveLength(1);
+
+ // No review tag on the happy path.
+ const tagCalls = callsMatching(fetchMock, 'mutation tagsAdd');
+ const taggedWith = tagCalls.map(([, opts]: any) => JSON.parse(opts.body).variables.tags).flat();
+ expect(taggedWith).not.toContain('Needs-Price-Review');
+ }, 15000);
+
+ it("declared-floor vendor ('DW Bespoke Studio'): sample is written at the declared $12 floor and passes", async () => {
+ const fetchMock = makeFetchMock();
+ global.fetch = fetchMock as any;
+
+ const dto = baseDto({ price: '60.00', vendor: 'DW Bespoke Studio' });
+ const result = await createOrGetProduct(dto);
+
+ expect(result.product).toBeDefined();
+
+ // resolveSampleFloor('DW Bespoke Studio') === 12 (VENDOR_DECLARED_SAMPLE_PRICE),
+ // and addSampleVariant writes the sample at String(sampleFloor) — not the
+ // generic $4.25 — so the sample-create call must carry price '12'.
+ const sampleCreateCalls = callsMatching(fetchMock, 'productVariantsBulkCreate');
+ expect(sampleCreateCalls).toHaveLength(1);
+ expect(JSON.parse(sampleCreateCalls[0][1].body).variables.variants[0].price).toBe('12');
+
+ // A sample correctly priced at its own declared floor passes the gate —
+ // eligible + gate-passed ⇒ promoted to ACTIVE + published.
+ const statusCalls = callsMatching(fetchMock, 'mutation productUpdateStatus');
+ expect(statusCalls).toHaveLength(1);
+ expect(JSON.parse(statusCalls[0][1].body).variables.input.status).toBe('ACTIVE');
+ expect(callsMatching(fetchMock, 'mutation productPublish')).toHaveLength(1);
+ }, 15000);
+
+ it("declared-floor vendor ('DW Bespoke Studio'): a $4.25 sample (the generic default) would BLOCK the gate", async () => {
+ // This proves the WIRING matters: addSampleVariant() always writes+checks the
+ // sample at String(sampleFloor) — never a hardcoded $4.25 — for a declared
+ // vendor. Called directly against the real, pure gate (no network/DB) to
+ // demonstrate what protects against a future regression that reintroduces a
+ // hardcoded $4.25 sample write for a declared-floor vendor.
+ const { assertPriceIntegrity, resolveSampleFloor } = require('../lib/price-integrity-gate');
+ const sampleFloor = resolveSampleFloor('DW Bespoke Studio');
+ expect(sampleFloor).toBe(12.0);
+
+ const result = assertPriceIntegrity({
+ dwSku: 'DWXX-999999',
+ netCost: null,
+ sampleFloor,
+ variants: [
+ { role: 'sellable', price: 60, orderable: true, sku: 'DWXX-999999-Roll', priceSource: 'scraped' },
+ { role: 'sample', price: 4.25, orderable: false, sku: 'DWXX-999999-Sample' },
+ ],
+ });
+
+ expect(result.ok).toBe(false);
+ expect(
+ result.violations.some(
+ (v: any) => v.class === 'A' && (v.code === 'no-sample-in-band' || v.code === 'sample-below-vendor-floor')
+ )
+ ).toBe(true);
+ });
+
+ // NEGATIVE TEST (CLAUDE.md TK-11431 rule 3) for the sampleOk fix (review 2026-09-14):
+ // a well-priced, eligible product whose Sample-variant CREATE fails must NOT be
+ // promoted to ACTIVE (DW hard rule: every product has a memo Sample) — it stays DRAFT
+ // and is tagged Needs-Sample. Without the fix, priced+gate-passed alone promoted it.
+ it('sample-create failure: product with a good sellable but a failed Sample create stays DRAFT + Needs-Sample', async () => {
+ FAIL_SAMPLE_CREATE = true;
+ const fetchMock = makeFetchMock();
+ global.fetch = fetchMock as any;
+
+ const dto = baseDto({ price: '54.00', vendor: 'Test Vendor' });
+ const result = await createOrGetProduct(dto);
+ expect(result.product).toBeDefined();
+
+ // The sellable price DID write (this is not a price failure)...
+ expect(callsMatching(fetchMock, 'productVariantsBulkUpdate')).toHaveLength(1);
+ // ...and the Sample create WAS attempted and failed.
+ expect(callsMatching(fetchMock, 'productVariantsBulkCreate')).toHaveLength(1);
+
+ // Must NEVER be promoted to ACTIVE and NEVER published.
+ const statusCalls = callsMatching(fetchMock, 'mutation productUpdateStatus');
+ expect(
+ statusCalls.every(([, opts]: any) => JSON.parse(opts.body).variables.input.status !== 'ACTIVE')
+ ).toBe(true);
+ expect(callsMatching(fetchMock, 'mutation productPublish')).toHaveLength(0);
+
+ // Must be tagged Needs-Sample.
+ const tagCalls = callsMatching(fetchMock, 'mutation tagsAdd');
+ const taggedWith = tagCalls.map(([, opts]: any) => JSON.parse(opts.body).variables.tags).flat();
+ expect(taggedWith).toContain('Needs-Sample');
+ }, 15000);
+});
diff --git a/DW-Programming/ImportNewSkufromURL/__tests__/weight-guard.test.ts b/DW-Programming/ImportNewSkufromURL/__tests__/weight-guard.test.ts
new file mode 100644
index 00000000..0c6254b6
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/__tests__/weight-guard.test.ts
@@ -0,0 +1,96 @@
+// weight-guard.test.ts — TK-11539 negative test (CLAUDE.md TK-11431 amendment 3:
+// "a check ships with a negative test proving it goes red on an injected fault").
+//
+// The guard's job: no variant the shared import engine creates may carry a zero/missing
+// shipping weight. hasPositiveWeight() is the invariant; the tests prove it is RED on the
+// pre-fix (un-guarded) payload and GREEN on the guarded payload, and that the guard resolves
+// sane, positive, per-product-type weights.
+
+import {
+ inventoryItemWithWeight,
+ hasPositiveWeight,
+ sellableWeightLb,
+ sampleWeightLb,
+ SAMPLE_WEIGHT_LB,
+ FALLBACK_LB,
+ TYPE_DEFAULT_LB,
+} from '../lib/weight-guard';
+
+describe('TK-11539 weight guard — weight resolution', () => {
+ it('sellable Wallcovering resolves to the TK-11414 backfill default (3.0 lb)', () => {
+ expect(sellableWeightLb('Wallcovering')).toBe(TYPE_DEFAULT_LB['Wallcovering']);
+ expect(sellableWeightLb('Wallcovering')).toBe(3.0);
+ });
+
+ it('sellable resolution is case-insensitive', () => {
+ expect(sellableWeightLb('wallcovering')).toBe(3.0);
+ expect(sellableWeightLb(' FABRIC ')).toBe(1.0);
+ });
+
+ it('unknown/blank product_type falls back to FALLBACK_LB (never zero)', () => {
+ expect(sellableWeightLb('Nonexistent Type')).toBe(FALLBACK_LB);
+ expect(sellableWeightLb('')).toBe(FALLBACK_LB);
+ expect(sellableWeightLb(undefined)).toBe(FALLBACK_LB);
+ expect(sellableWeightLb(null)).toBe(FALLBACK_LB);
+ expect(FALLBACK_LB).toBeGreaterThan(0);
+ });
+
+ it('sample weight is the approved 0.25 lb', () => {
+ expect(sampleWeightLb()).toBe(SAMPLE_WEIGHT_LB);
+ expect(sampleWeightLb()).toBe(0.25);
+ });
+});
+
+describe('TK-11539 weight guard — inventoryItemWithWeight builder', () => {
+ it('sellable payload keeps base fields AND carries a positive weight in POUNDS', () => {
+ const out = inventoryItemWithWeight(
+ { sku: 'DWXX-1-Roll', tracked: true },
+ { role: 'sellable', productType: 'Wallcovering' }
+ );
+ expect(out.sku).toBe('DWXX-1-Roll');
+ expect(out.tracked).toBe(true);
+ expect(out.measurement.weight.unit).toBe('POUNDS');
+ expect(out.measurement.weight.value).toBe(3.0);
+ expect(hasPositiveWeight(out)).toBe(true);
+ });
+
+ it('sample payload keeps base fields AND carries 0.25 lb', () => {
+ const out = inventoryItemWithWeight(
+ { sku: 'DWXX-1-Sample', tracked: false },
+ { role: 'sample', productType: 'Wallcovering' }
+ );
+ expect(out.sku).toBe('DWXX-1-Sample');
+ expect(out.tracked).toBe(false);
+ expect(out.measurement.weight.value).toBe(0.25);
+ expect(hasPositiveWeight(out)).toBe(true);
+ });
+
+ it('guarantees a positive weight even for an unknown product_type', () => {
+ const out = inventoryItemWithWeight(
+ { sku: 'DWXX-2-Roll', tracked: true },
+ { role: 'sellable', productType: 'Totally Unknown' }
+ );
+ expect(out.measurement.weight.value).toBe(FALLBACK_LB);
+ expect(hasPositiveWeight(out)).toBe(true);
+ });
+});
+
+describe('TK-11539 weight guard — NEGATIVE test (proves the invariant has teeth)', () => {
+ it('REDDENS on the PRE-FIX payload (the exact zero-weight fault this guard removes)', () => {
+ // This is precisely what addSampleVariant wrote before TK-11539: no measurement.
+ const preFixUnguardedPayload = { sku: 'DWXX-1-Roll', tracked: true };
+ expect(hasPositiveWeight(preFixUnguardedPayload)).toBe(false);
+ });
+
+ it('REDDENS on an injected zero / negative / non-finite weight', () => {
+ expect(hasPositiveWeight({ sku: 'x', measurement: { weight: { value: 0, unit: 'POUNDS' } } })).toBe(false);
+ expect(hasPositiveWeight({ sku: 'x', measurement: { weight: { value: -3, unit: 'POUNDS' } } })).toBe(false);
+ expect(hasPositiveWeight({ sku: 'x', measurement: { weight: { value: NaN, unit: 'POUNDS' } } })).toBe(false);
+ expect(hasPositiveWeight({ sku: 'x', measurement: { weight: {} } })).toBe(false);
+ });
+
+ it('GREENS on the guarded payload — closing the loop against the pre-fix fault', () => {
+ const guarded = inventoryItemWithWeight({ sku: 'DWXX-1-Roll', tracked: true }, { role: 'sellable', productType: 'Wallcovering' });
+ expect(hasPositiveWeight(guarded)).toBe(true);
+ });
+});
diff --git a/DW-Programming/ImportNewSkufromURL/gate-negtest.ts b/DW-Programming/ImportNewSkufromURL/gate-negtest.ts
new file mode 100644
index 00000000..eb4f8b0e
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/gate-negtest.ts
@@ -0,0 +1,47 @@
+// TK-11447 negative tests: prove the reworked Class A goes RED on injected faults
+// AND green on the vendor prices that the old equality wrongly blocked.
+import { assertPriceIntegrity, resolveSampleFloor } from './lib/price-integrity-gate';
+const R:string[]=[]; let pass=0, fail=0;
+const t=(name:string, cond:boolean)=>{ (cond?pass++:fail++); R.push((cond?' PASS ':' FAIL ')+name); };
+const G=(vendor:string, variants:any[], netCost:number|null=null)=>assertPriceIntegrity({
+ dwSku:'T', netCost, sampleFloor:resolveSampleFloor(vendor), variants});
+const codes=(r:any)=>r.violations.map((v:any)=>v.code);
+
+// --- GREEN: the legitimate vendor sample prices the equality rule wrongly blocked
+for (const [vendor, p] of [['DW Bespoke Studio',12.00],['Sister Parish',8.24],['Fentucci',9.95],['Artmura',5.00]] as [string,number][]) {
+ const r=G(vendor,[{role:'sample',price:p,orderable:true},{role:'sellable',price:195,orderable:true,priceSource:'scraped'}]);
+ t(`LEGIT ${vendor} sample $${p} passes (equality rule would have BLOCKED)`, r.ok);
+}
+// house baseline still fine
+t('LEGIT house $4.25 sample passes', G('Thibaut',[{role:'sample',price:4.25,orderable:true},{role:'sellable',price:174.71,orderable:true,priceSource:'scraped'}]).ok);
+
+// --- RED: injected faults
+const f1=G('Thibaut',[{role:'sample',price:161.27,orderable:true},{role:'sellable',price:174.71,orderable:true,priceSource:'scraped'}]);
+t('FAULT sample priced as a roll ($161.27) BLOCKS', !f1.ok && codes(f1).includes('no-sample-in-band'));
+
+const f2=G('Thibaut',[{role:'sample',price:4.25,orderable:true},{role:'sellable',price:4.25,orderable:true,priceSource:'scraped'}]);
+t('FAULT sellable equals sample BLOCKS', !f2.ok && codes(f2).includes('sellable-equals-sample'));
+
+// the leak a GLOBAL 4.25 constant would have MISSED: vendor floor is 12.00
+const f3=G('DW Bespoke Studio',[{role:'sample',price:12.00,orderable:true},{role:'sellable',price:12.00,orderable:true,priceSource:'scraped'}]);
+t('FAULT sellable equals a NON-4.25 vendor sample ($12) BLOCKS (global-constant blind spot)', !f3.ok && codes(f3).includes('sellable-equals-sample'));
+
+const f4=G('Thibaut',[{role:'sample',price:4.25,orderable:true},{role:'sellable',price:0,orderable:true,priceSource:'scraped'}]);
+t('FAULT $0 orderable sellable BLOCKS', !f4.ok && codes(f4).includes('zero-price-orderable'));
+
+const f5=G('Innovations',[{role:'sample',price:3.50,orderable:true},{role:'sellable',price:195,orderable:true,priceSource:'scraped'}]);
+t('FAULT sample BELOW vendor floor ($3.50 vs $4.25) BLOCKS', !f5.ok && codes(f5).includes('sample-below-vendor-floor'));
+
+const f6=G('Thibaut',[{role:'sample',price:4.25,orderable:true},{role:'sellable',price:50,orderable:true,priceSource:'defaulted'}]);
+t('FAULT defaulted-source sellable BLOCKS', !f6.ok && codes(f6).includes('sellable-defaulted-price'));
+
+// --- the DTD verdict E behaviour: cost-unknown must WARN, never BLOCK
+const w=G('SomeVendorWithNoCostFeed',[{role:'sample',price:4.25,orderable:true},{role:'sellable',price:195,orderable:true,priceSource:'scraped'}], null);
+t('VERDICT-E cost-unknown WARNS and does NOT block', w.ok && w.warnings.map((x:any)=>x.code).includes('cost-unverified'));
+// and a KNOWN cost below markup still blocks
+const b=G('Thibaut',[{role:'sample',price:4.25,orderable:true},{role:'sellable',price:60,orderable:true,priceSource:'scraped'}], 100);
+t('cost KNOWN + below markup floor still BLOCKS', !b.ok && codes(b).includes('below-markup-floor'));
+
+console.log(R.join('\n'));
+console.log(`\n${pass} passed, ${fail} failed`);
+process.exit(fail?1:0);
diff --git a/DW-Programming/ImportNewSkufromURL/lib/price-integrity-cost.ts b/DW-Programming/ImportNewSkufromURL/lib/price-integrity-cost.ts
new file mode 100644
index 00000000..29020b3d
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/lib/price-integrity-cost.ts
@@ -0,0 +1,84 @@
+/**
+ * price-integrity-cost.ts — impure COST ADAPTER for the price-integrity gate.
+ * ============================================================================
+ * Ticket: TK-11403 (see lib/price-integrity-gate.ts header).
+ *
+ * The pure gate (lib/price-integrity-gate.ts) is DB-free and deterministic and
+ * MUST NOT import this file. This adapter is the ONLY place that resolves OUR
+ * net cost by shelling the price-finder authority:
+ *
+ * node <price-finder>/scripts/verify-price.js --vendor <V> --dw-sku <S> --json
+ *
+ * That emits JSON with a `sample: [{ dw_sku, net_cost, our_price, ... }]` array.
+ * net_cost is OUR net cost in dollars. We return sample[0].net_cost as a number,
+ * or null on ANY error / missing value. Under the A-with-teeth contract
+ * (2026-09-11) a null cost makes the gate emit a NON-BLOCKING `cost-unverified`
+ * WARNING (not a block) — so a broken cost lookup does not halt the ~97 vendors
+ * with no cost feed, while the gate's cost-free absolute-dollar floor still
+ * blocks an obviously-mispriced (sub-$5 orderable) publish.
+ */
+
+import { execFile } from 'node:child_process';
+import { homedir } from 'node:os';
+import { join } from 'node:path';
+
+/** Location of the price-finder authority. Overridable via env for tests/CI. */
+const VERIFY_PRICE_SCRIPT =
+ process.env.PRICE_FINDER_VERIFY_SCRIPT ||
+ join(homedir(), '.claude', 'skills', 'price-finder', 'scripts', 'verify-price.js');
+
+interface VerifyPriceReport {
+ sample?: Array<{ dw_sku?: string; net_cost?: number | string | null }>;
+}
+
+/**
+ * Resolve OUR net cost (dollars) for a (vendor, dwSku) pair, or null on any
+ * failure/missing — fail-closed by design.
+ */
+export async function resolveNetCost(
+ vendor: string,
+ dwSku: string,
+ timeoutMs: number = 60_000,
+): Promise<number | null> {
+ if (!vendor || !dwSku) return null;
+
+ let stdout: string;
+ try {
+ stdout = await runVerifyPrice(vendor, dwSku, timeoutMs);
+ } catch {
+ return null; // spawn error, nonzero exit, timeout, etc. → fail-closed
+ }
+
+ let report: VerifyPriceReport;
+ try {
+ report = JSON.parse(stdout) as VerifyPriceReport;
+ } catch {
+ return null; // unparseable stdout → fail-closed
+ }
+
+ const first = report?.sample?.[0];
+ if (!first) return null;
+
+ const cost = typeof first.net_cost === 'string' ? Number(first.net_cost) : first.net_cost;
+ if (typeof cost !== 'number' || !isFinite(cost) || cost <= 0) return null;
+
+ return cost;
+}
+
+function runVerifyPrice(vendor: string, dwSku: string, timeoutMs: number = 60_000): Promise<string> {
+ // Clamp to a sane range: a 0/negative/NaN timeout would disable the kill-timer
+ // (execFile treats 0 as "no timeout"), which is exactly the unbounded-hang risk
+ // the batch path passes a short timeout to avoid.
+ const t = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, 60_000) : 60_000;
+ return new Promise((resolve, reject) => {
+ execFile(
+ process.execPath, // node
+ [VERIFY_PRICE_SCRIPT, '--vendor', vendor, '--dw-sku', dwSku, '--json'],
+ { timeout: t, maxBuffer: 8 * 1024 * 1024 },
+ (err, out) => {
+ if (err) return reject(err);
+ resolve(out ? out.toString() : '');
+ }
+ );
+ });
+}
diff --git a/DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.ts b/DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.ts
new file mode 100644
index 00000000..d579997a
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.ts
@@ -0,0 +1,476 @@
+/**
+ * price-integrity-gate.ts — SHARED WRITE-TIME PRICE-INTEGRITY GATE
+ * ============================================================================
+ * Ticket: TK-11403 (root-cause prevention memo 2026-09-10-ROOTCAUSE-pricing-
+ * integrity-recurrence) · closes the recurrence class under the
+ * CLAUDE.md TK-11431 doctrine (rule 3: ships with a NEGATIVE TEST that
+ * proves it goes RED on an injected fault).
+ *
+ * WHY THIS EXISTS
+ * ----------------------------------------------------------------------------
+ * Variant price writes for DW products go through addSampleVariant() in
+ * lib/shopify.ts AND at least two other choke points (schumacher's own
+ * addSampleVariant, scripts/import-queue-runner.js) — see PENDING-WIREIN memo;
+ * a truly "shared" gate must be wired into all of them. Each has three latent
+ * bug classes, each of which has shipped to the live store more than once:
+ *
+ * Class A — SAMPLE OUTCOME + DEFAULT-PRICE LEAK. Two halves:
+ * (i) OUTCOME (reworked TK-11447): the product must offer at least
+ * ONE sample variant inside the band [sampleFloor, sampleCeiling] —
+ * i.e. "can a customer buy a memo sample at a sample price?" This
+ * REPLACES the old `samplePrice === 4.25` equality, which was MEASURED
+ * WRONG for this catalog (see MEASUREMENT below).
+ * (ii) LEAK: a sample/default price must never surface as the
+ * sellable/roll price. Magnitude alone misses it (when cost is low,
+ * 4.25 clears the markup floor), so a sellable priced at one of this
+ * product's own sample prices OR sourced from a 'defaulted' fallback
+ * is a HARD BLOCK.
+ *
+ * MEASUREMENT (bulk export, all 92,593 ACTIVE products, 2026-09-11):
+ * a hard `samplePrice == 4.25` equality fail-CLOSES on ~1,287
+ * legitimately-priced products whose vendor declares its own sample
+ * price — DW Bespoke Studio $12.00 (a genuine 2ft x 1ft sample),
+ * PS Removable / Artmura / PR Faux Leather $5.00, Fentucci $9.95 and
+ * $4.95, Sister Parish $8.24 — and would HALT those imports, which is
+ * exactly the "gate blocks a legitimate import" risk the wire-in memo
+ * flagged. On the same data the SHAPE test ("sample priced above
+ * $4.25") returned 4,301 hits, ~97% FALSE; the OUTCOME test ("offers
+ * at least one sample-ish variant at or below a ~$50 ceiling")
+ * returned 12, ALL TRUE on live confirm. Reference detector:
+ * ~/.claude/yolo-queue/tk11403-residual/harm.mjs.
+ *
+ * Class B — MARKUP + ANOMALY floors. When cost is KNOWN, the sellable must
+ * clear the DW markup floor (retail = net_cost/0.65/0.85 ~1.81x;
+ * gate floor a conservative 1.5x) — below → BLOCK. When cost is
+ * UNKNOWN, that is a NON-BLOCKING WARNING ('cost-unverified'), so the
+ * ~97 vendors with no cost feed still import (DTD "A-with-teeth"
+ * verdict 2026-09-11). A cost-FREE absolute-dollar floor (default
+ * $5, tunable) BLOCKS any orderable variant priced under it, so
+ * unknown-cost is never a blind allow.
+ *
+ * Class C — $0-ORDERABLE. addSampleVariant sets `numericUnitPrice =
+ * numericPrice || '0.00'`, then inventoryPolicy:'CONTINUE' +
+ * setInventoryQuantity(...,2025) — so a MISSING scraped price
+ * silently becomes a $0 variant that is orderable at checkout. This
+ * exact defect ($0 + orderable) has recurred 6 times. $0/NaN and a
+ * negative price are separate HARD BLOCKS.
+ *
+ * This module is the PURE, DETERMINISTIC, ZERO-DEPENDENCY, DB-FREE gate that
+ * every price-writer can call to BLOCK a bad publish. It knows nothing about
+ * Shopify, Postgres, or the filesystem — cost is resolved by the separate,
+ * impure adapter lib/price-integrity-cost.ts and passed in as `netCost`.
+ *
+ * SEVERITY CONTRACT (A-with-teeth)
+ * ----------------------------------------------------------------------------
+ * assertPriceIntegrity returns { ok, violations, warnings }. `violations` are
+ * block-severity (fail ok, thrown by enforcePriceIntegrity); `warnings` are
+ * warn-severity (recorded, non-blocking). ok = violations.length === 0. Warnings
+ * (notably 'cost-unverified') MUST be surfaced by the caller to a monitored
+ * channel — a swallowed warning is a false green (TK-11431 amendment 2).
+ */
+
+/** One variant, described in the gate's own vocabulary (role, not option string). */
+export interface VariantCheck {
+ /** 'sample' = the $4.25 memo variant; 'sellable' = the real per-unit (Per Roll / Per Yard) variant. */
+ role: 'sample' | 'sellable';
+ /** Numeric price in dollars (NOT a "$54.00 USD" string — resolve that upstream). */
+ price: number;
+ /** True when the variant can be added to cart / checked out (CONTINUE policy or in-stock). */
+ orderable: boolean;
+ /** Optional variant SKU, echoed into violations for traceability. */
+ sku?: string;
+ /**
+ * PROVENANCE of the price (the headline-bug fix, TK-11403).
+ * The recurring live bug is the $4.25 SAMPLE DEFAULT leaking out as the
+ * sellable/roll price. Magnitude alone can't catch it — when net cost is low
+ * enough that 4.25 >= 1.5x cost, Class B and Class C both pass and the leak
+ * clears clean. So we ALSO track how the price got here:
+ * 'scraped' — parsed from the vendor page (trustworthy)
+ * 'vendor' — from a vendor cost/price feed (trustworthy)
+ * 'defaulted' — the code fell back to a literal (e.g. ProductDTO.price
+ * defaults to '4.25', route.ts hardcodes '4.25' on fallback
+ * paths) → a sellable with this source is BLOCKED, class A.
+ * OPTIONAL: when absent, the numeric sellable-equals-sample backstop still
+ * catches the same leak. Provenance is the belt; the backstop is the braces.
+ */
+ priceSource?: 'scraped' | 'vendor' | 'defaulted';
+}
+
+export interface PriceIntegrityInput {
+ /** The DW SKU under test (for logging / violation context). */
+ dwSku: string;
+ /**
+ * OUR net cost in dollars. null/undefined/<=0 ⇒ cost UNVERIFIED. As of the
+ * DTD "A-with-teeth" verdict (2026-09-11) this is a NON-BLOCKING WARNING
+ * ('cost-unverified'), NOT a hard block — blocking on unknown cost would halt
+ * ~97 of 108 vendors that have no cost feed. The absolute-dollar floor (below)
+ * keeps unknown-cost from being a blind allow.
+ */
+ netCost: number | null;
+ /**
+ * The PER-VENDOR DECLARED sample price in dollars — the FLOOR of the acceptable
+ * sample band, and the input that replaced the old global `sampleExpected`
+ * equality (TK-11447). Default 4.25 (the DW house baseline). Resolve it for a
+ * vendor with `resolveSampleFloor(vendor)`.
+ */
+ sampleFloor?: number;
+ /**
+ * Upper bound of the sample band in dollars. Default 50. This is the OUTCOME
+ * bound: a product passes Class A's outcome test when at least one sample
+ * variant is priced within [sampleFloor, sampleCeiling]. Measured 100% precise
+ * on the 92,593-product export (12/12 true positives) where the shape test was
+ * ~97% false.
+ */
+ sampleCeiling?: number;
+ /** Minimum sellable markup multiple over netCost (only checked when cost is KNOWN). Default 1.5. */
+ markupFloor?: number;
+ /**
+ * Cost-FREE absolute-dollar floor. Any ORDERABLE variant priced > 0 but below
+ * this → BLOCK ('below-absolute-floor'), whether or not cost is known — a
+ * conservative anomaly guard so an unknown-cost item can't ship at $2. Default
+ * 5.00 (Cody floated $15 as aggressive; 5.00 avoids false-blocking a legit
+ * cheap item). Tunable per caller.
+ */
+ absoluteFloor?: number;
+ /** Every variant being written for this product. */
+ variants: VariantCheck[];
+}
+
+export interface Violation {
+ class: 'A' | 'B' | 'C';
+ code: string;
+ /**
+ * 'block' → fails the gate (ok:false) and is thrown by enforcePriceIntegrity.
+ * 'warn' → recorded but does NOT fail the gate; the caller MUST surface it to
+ * a monitored channel (see recordOutcome note in the wire-in memo).
+ */
+ severity: 'block' | 'warn';
+ sku?: string;
+ message: string;
+ /** The offending observed value (price). */
+ observed: number;
+ /** Human-readable description of what was expected. */
+ expected: string;
+}
+
+export interface GateResult {
+ /** True iff there are ZERO block-severity violations. Warnings do NOT fail ok. */
+ ok: boolean;
+ /** Block-severity findings (fail the gate). */
+ violations: Violation[];
+ /** Warn-severity findings (recorded, non-blocking — MUST be surfaced by the caller). */
+ warnings: Violation[];
+}
+
+/** Custom error thrown by enforcePriceIntegrity so callers can catch + inspect .violations. */
+export class PriceIntegrityError extends Error {
+ violations: Violation[];
+ constructor(violations: Violation[]) {
+ super(
+ `price integrity gate BLOCKED: ${violations.length} violation(s) — ` +
+ violations.map(v => `[${v.class}:${v.code}${v.sku ? ' ' + v.sku : ''}]`).join(' ')
+ );
+ this.name = 'PriceIntegrityError';
+ this.violations = violations;
+ }
+}
+
+/** DW house baseline sample price, used when a vendor declares none. */
+const DEFAULT_SAMPLE_FLOOR = 4.25;
+/** Outcome bound: above this, a "sample" is not a sample price any customer would pay. */
+const DEFAULT_SAMPLE_CEILING = 50.0;
+const DEFAULT_MARKUP_FLOOR = 1.5;
+const DEFAULT_ABSOLUTE_FLOOR = 5.0;
+/** Epsilon for float comparison on sample-price bands. */
+const SAMPLE_EPSILON = 0.005;
+
+/**
+ * Vendors MEASURED (2026-09-11 bulk export of all 92,593 ACTIVE products) to
+ * declare their own sample price. Keyed case-insensitively on the Shopify vendor
+ * string. Where a vendor declares more than one, the MINIMUM is the floor, so
+ * this map can only reflect what the vendor actually charges — it never invents
+ * a tighter floor than the catalog shows.
+ */
+const VENDOR_DECLARED_SAMPLE_PRICE: Record<string, number> = {
+ 'dw bespoke studio': 12.0,
+ 'ps removable': 5.0,
+ artmura: 5.0,
+ 'pr faux leather': 5.0,
+ fentucci: 4.95, // declares both $9.95 and $4.95 — floor is the minimum
+ 'sister parish': 8.24,
+};
+
+/**
+ * Resolve the per-vendor declared sample price (the Class-A floor) for a vendor
+ * string. Falls back to the DW house baseline $4.25 for any vendor with no
+ * measured declared price. Pure — no I/O, safe for the gate's zero-dep contract.
+ */
+export function resolveSampleFloor(vendor: string | null | undefined): number {
+ if (!vendor) return DEFAULT_SAMPLE_FLOOR;
+ const declared = VENDOR_DECLARED_SAMPLE_PRICE[vendor.trim().toLowerCase()];
+ return typeof declared === 'number' && isFinite(declared) && declared > 0
+ ? declared
+ : DEFAULT_SAMPLE_FLOOR;
+}
+
+/**
+ * Pure gate. Returns { ok, violations, warnings }. NEVER throws. A single
+ * variant may produce multiple findings (e.g. a $0 sellable is both Class B and
+ * Class C). `violations` = block-severity (fail ok); `warnings` = warn-severity
+ * (recorded, non-blocking). ok = violations.length === 0.
+ */
+export function assertPriceIntegrity(input: PriceIntegrityInput): GateResult {
+ // Collect everything here with severity, then split at the end.
+ const findings: Violation[] = [];
+
+ const sampleFloor =
+ typeof input.sampleFloor === 'number' && isFinite(input.sampleFloor) && input.sampleFloor > 0
+ ? input.sampleFloor
+ : DEFAULT_SAMPLE_FLOOR;
+ const rawCeiling =
+ typeof input.sampleCeiling === 'number' && isFinite(input.sampleCeiling)
+ ? input.sampleCeiling
+ : DEFAULT_SAMPLE_CEILING;
+ // An incoherent caller (ceiling below floor) must not produce an empty band that
+ // blocks every product; the floor always sits inside its own band.
+ const sampleCeiling = Math.max(rawCeiling, sampleFloor);
+ const markupFloor =
+ typeof input.markupFloor === 'number' && isFinite(input.markupFloor)
+ ? input.markupFloor
+ : DEFAULT_MARKUP_FLOOR;
+ const absoluteFloor =
+ typeof input.absoluteFloor === 'number' && isFinite(input.absoluteFloor)
+ ? input.absoluteFloor
+ : DEFAULT_ABSOLUTE_FLOOR;
+
+ const variants = Array.isArray(input.variants) ? input.variants : [];
+
+ // Cost known & positive? Needed to PROVE the Class B MARKUP floor. When
+ // unknown, markup is a non-blocking WARNING (A-with-teeth), and the cost-free
+ // absolute-dollar floor below is what keeps unknown-cost from being a blind allow.
+ const costKnown =
+ typeof input.netCost === 'number' && isFinite(input.netCost) && input.netCost > 0;
+ const floorPrice = costKnown ? markupFloor * (input.netCost as number) : NaN;
+
+ // ---- Class A (i): the PRODUCT-LEVEL OUTCOME test (TK-11447) ----------------
+ // Replaces the old per-variant `samplePrice === 4.25` equality, MEASURED WRONG
+ // on 2026-09-11: the equality fail-CLOSES on ~1,287 legitimately-priced
+ // products whose vendor declares its own sample price. The honest question is
+ // an OUTCOME, asked ONCE per product, not per variant:
+ // "can a customer buy a memo sample of this product at a sample price?"
+ // A product passes when AT LEAST ONE sample variant is priced inside the band
+ // [sampleFloor, sampleCeiling]. Measured 12/12 true positives where the shape
+ // test was ~97% false.
+ //
+ // Deliberately NOT a finding when the product ships zero sample variants: a
+ // sellable-only product is a catalog-completeness question, not a MIS-PRICING
+ // one, and this gate must not block an import for it.
+ const sampleVariants = variants.filter(v => v.role === 'sample');
+ const samplePrices = sampleVariants
+ .map(v => (typeof v.price === 'number' && isFinite(v.price) ? v.price : NaN))
+ .filter(p => isFinite(p));
+ const sampleInBand = samplePrices.some(
+ p => p >= sampleFloor - SAMPLE_EPSILON && p <= sampleCeiling + SAMPLE_EPSILON
+ );
+ if (sampleVariants.length > 0 && !sampleInBand) {
+ const cheapest = samplePrices.length ? Math.min(...samplePrices) : NaN;
+ findings.push({
+ class: 'A',
+ code: 'no-sample-in-band',
+ severity: 'block',
+ sku: sampleVariants[0]?.sku,
+ message:
+ `product offers NO sample inside the acceptable band — cheapest sample-role variant is ` +
+ `${fmt(cheapest)}, outside [${fmt(sampleFloor)}, ${fmt(sampleCeiling)}]. ` +
+ `A customer cannot buy a memo sample at a sample price.`,
+ observed: numOrZero(cheapest),
+ expected: `at least one sample variant within [${round2(sampleFloor)}, ${round2(sampleCeiling)}]`,
+ });
+ }
+
+ for (const v of variants) {
+ const price = typeof v.price === 'number' && isFinite(v.price) ? v.price : NaN;
+
+ // ---- Class A: per-sample sanity — BLOCK only BELOW the vendor's own floor ----
+ // The OUTCOME test above already decides whether the product offers a usable
+ // sample. What remains per-variant is the one case the outcome test cannot
+ // express: a sample priced UNDER the vendor's declared floor is a mis-price
+ // (e.g. the $3.50 rows found on Innovations vs their $4.25 house price), and
+ // an under-priced sample is a real money leak. A sample ABOVE the ceiling is
+ // NOT blocked here — that is the outcome test's job, and blocking it per
+ // variant would re-introduce the equality behaviour this ticket removed
+ // (a product may legitimately carry several sample sizes at several prices).
+ if (v.role === 'sample') {
+ if (isFinite(price) && price < sampleFloor - SAMPLE_EPSILON) {
+ findings.push({
+ class: 'A',
+ code: 'sample-below-vendor-floor',
+ severity: 'block',
+ sku: v.sku,
+ message: `sample variant priced ${fmt(price)} is BELOW this vendor's declared sample floor ${fmt(sampleFloor)}`,
+ observed: numOrZero(price),
+ expected: `>= ${round2(sampleFloor)} (vendor-declared sample floor)`,
+ });
+ }
+ }
+
+ // ---- Class A (sellable side): the SAMPLE/DEFAULT-PRICE LEAK guard — BLOCK ----
+ // The headline bug: the $4.25 sample default surfaces as the sellable price.
+ // Two independent hard-block catches, both class A:
+ if (v.role === 'sellable') {
+ // (b) PROVENANCE — a sellable whose price was defaulted (never scraped)
+ // is a leak by construction, regardless of magnitude.
+ if (v.priceSource === 'defaulted') {
+ findings.push({
+ class: 'A',
+ code: 'sellable-defaulted-price',
+ severity: 'block',
+ sku: v.sku,
+ message: `sellable variant priced ${fmt(
+ price
+ )} from a DEFAULTED source (never scraped/vendor) — suspected sample/default-price leak`,
+ observed: numOrZero(price),
+ expected: "priceSource 'scraped' or 'vendor' for a sellable",
+ });
+ }
+ // (a) NUMERIC BACKSTOP — a sellable priced at (≈) the sample price is
+ // essentially always the leak (DW sellables are $30–$2000+). Catches
+ // it even when priceSource is absent.
+ // Compare against THIS PRODUCT'S OWN sample prices (not a global constant),
+ // falling back to the vendor floor when the product ships no sample variant.
+ // A global 4.25 would have missed the leak entirely on any vendor whose
+ // declared sample price is not 4.25 — the same blind spot the equality had.
+ const leakTargets = samplePrices.length ? samplePrices : [sampleFloor];
+ const matched = leakTargets.find(t => Math.abs(price - t) <= SAMPLE_EPSILON);
+ if (matched !== undefined) {
+ findings.push({
+ class: 'A',
+ code: 'sellable-equals-sample',
+ severity: 'block',
+ sku: v.sku,
+ message: `sellable variant priced ${fmt(
+ price
+ )} equals this product's sample price ${fmt(matched)} — suspected sample/default-price leak`,
+ observed: numOrZero(price),
+ expected: `sellable price != any sample price on this product (±${SAMPLE_EPSILON})`,
+ });
+ }
+ }
+
+ // ---- Class B: the MARKUP floor (cost-based) ----
+ if (v.role === 'sellable') {
+ if (!costKnown) {
+ // A-with-teeth: cost unknown ⇒ WARN (allow import, counted), NOT a block.
+ // The caller MUST surface this warning to a monitored channel.
+ findings.push({
+ class: 'B',
+ code: 'cost-unverified',
+ severity: 'warn',
+ sku: v.sku,
+ message: `markup UNVERIFIED — netCost unknown/invalid (${String(
+ input.netCost
+ )}); imported with a warning (magnitude not proven against cost)`,
+ observed: numOrZero(price),
+ expected: `netCost > 0 to verify price >= ${markupFloor}x cost`,
+ });
+ } else if (!(price >= floorPrice)) {
+ findings.push({
+ class: 'B',
+ code: 'below-markup-floor',
+ severity: 'block',
+ sku: v.sku,
+ message: `sellable variant ${fmt(price)} is below the ${markupFloor}x markup floor of ${fmt(
+ floorPrice
+ )} (netCost ${fmt(input.netCost as number)})`,
+ observed: numOrZero(price),
+ expected: `>= ${round2(floorPrice)} (${markupFloor}x netCost ${input.netCost})`,
+ });
+ }
+ }
+
+ // ---- Class B: cost-FREE absolute-dollar floor — BLOCK ----
+ // Fires for ANY orderable variant priced > 0 but under absoluteFloor,
+ // regardless of whether cost is known. $0/negative are Class C, not here.
+ // SAMPLE-ROLE EXEMPTION (TK-11447, caught by the negative test): a sample is
+ // legitimately cheap — the DW house sample is $4.25, BELOW the $5 default
+ // absolute floor — so applying this guard to samples blocks essentially every
+ // standard DW import. Samples already have their own floor enforced above
+ // ('sample-below-vendor-floor', the vendor-declared price). The absolute
+ // floor is an anomaly guard for SELLABLES, where no legitimate item is $2.
+ if (v.role === 'sellable' && v.orderable === true && price > 0 && price < absoluteFloor) {
+ findings.push({
+ class: 'B',
+ code: 'below-absolute-floor',
+ severity: 'block',
+ sku: v.sku,
+ message: `orderable ${v.role} variant ${fmt(price)} is below the absolute floor ${fmt(
+ absoluteFloor
+ )} — suspected mis-price (cost-free anomaly guard)`,
+ observed: numOrZero(price),
+ expected: `>= ${round2(absoluteFloor)} while orderable`,
+ });
+ }
+
+ // ---- Class C: any orderable variant (any role) MUST be priced > 0 — BLOCK ----
+ // Diagnose honestly: a NEGATIVE price is a different fault than a $0/NaN
+ // price and gets its own code so the report isn't misleading.
+ if (v.orderable === true && !(price > 0)) {
+ if (price < 0) {
+ findings.push({
+ class: 'C',
+ code: 'negative-price',
+ severity: 'block',
+ sku: v.sku,
+ message: `${v.role} variant is ORDERABLE at a NEGATIVE price ${fmt(
+ price
+ )} — invalid price`,
+ observed: numOrZero(price),
+ expected: '> 0 while orderable',
+ });
+ } else {
+ // price === 0 or NaN/invalid
+ findings.push({
+ class: 'C',
+ code: 'zero-price-orderable',
+ severity: 'block',
+ sku: v.sku,
+ message: `${v.role} variant is ORDERABLE at ${fmt(
+ price
+ )} — a $0 orderable variant is a checkout money-leak`,
+ observed: numOrZero(price),
+ expected: '> 0 while orderable',
+ });
+ }
+ }
+ }
+
+ const violations = findings.filter(f => f.severity === 'block');
+ const warnings = findings.filter(f => f.severity === 'warn');
+ return { ok: violations.length === 0, violations, warnings };
+}
+
+/**
+ * Fail-closed helper callers use to BLOCK a publish. Runs the pure gate and
+ * THROWS PriceIntegrityError (carrying the block-severity .violations) ONLY when
+ * a block-severity violation exists. Warn-severity findings (e.g. 'cost-unverified')
+ * do NOT throw — the caller must still surface them via assertPriceIntegrity(...).warnings
+ * to a monitored channel (see the wire-in memo's recordOutcome note). Returns
+ * void on a clean-or-warn-only input.
+ */
+export function enforcePriceIntegrity(input: PriceIntegrityInput): void {
+ const result = assertPriceIntegrity(input);
+ if (!result.ok) {
+ throw new PriceIntegrityError(result.violations);
+ }
+}
+
+// ---- small pure helpers (no deps) ----
+function numOrZero(n: number): number {
+ return typeof n === 'number' && isFinite(n) ? n : 0;
+}
+function round2(n: number): number {
+ return Math.round(n * 100) / 100;
+}
+function fmt(n: number): string {
+ return isFinite(n) ? `$${round2(n).toFixed(2)}` : `$NaN`;
+}
diff --git a/DW-Programming/ImportNewSkufromURL/lib/price-integrity-record.ts b/DW-Programming/ImportNewSkufromURL/lib/price-integrity-record.ts
new file mode 100644
index 00000000..cbef5285
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/lib/price-integrity-record.ts
@@ -0,0 +1,57 @@
+/**
+ * price-integrity-record.ts — BEST-EFFORT telemetry sink for the price gate.
+ * ============================================================================
+ * Ticket: TK-11403 (see lib/price-integrity-gate.ts header).
+ *
+ * The gate's WARN findings (notably 'cost-unverified') must be surfaced to a
+ * MONITORED channel, not swallowed in a console.log — a swallowed warning is a
+ * false green (TK-11431 amendment 2). This appends one JSONL line per outcome to
+ * ~/.claude/skills/dw-price-integrity-gate/data/runtime-events.jsonl
+ * which the dw-price-integrity-gate skill's report.mjs folds into its
+ * fleet-health data/latest.json.
+ *
+ * HARD FAIL-SAFE: telemetry MUST NEVER crash or slow an import. The whole body
+ * is wrapped in try/catch that SWALLOWS every error — if the dir/path doesn't
+ * exist on the host it is created; if creation fails, it silently no-ops.
+ */
+
+import { appendFileSync, mkdirSync } from 'node:fs';
+import { homedir } from 'node:os';
+import { join } from 'node:path';
+
+export interface OutcomeRecord {
+ vendor?: string | null;
+ dwSku?: string | null;
+ /** 'warn' = imported with warnings; 'block' = publish blocked by a violation. */
+ outcome: 'warn' | 'block';
+ /** The violation/warning codes involved (e.g. ['cost-unverified'] or ['below-absolute-floor']). */
+ codes?: string[];
+}
+
+const EVENTS_PATH =
+ process.env.PRICE_INTEGRITY_EVENTS_PATH ||
+ join(homedir(), '.claude', 'skills', 'dw-price-integrity-gate', 'data', 'runtime-events.jsonl');
+
+/**
+ * Append one telemetry line. Best-effort, fully swallowed — never throws.
+ */
+export function recordOutcome(rec: OutcomeRecord): void {
+ try {
+ const line =
+ JSON.stringify({
+ ts: new Date().toISOString(),
+ vendor: rec.vendor ?? null,
+ dwSku: rec.dwSku ?? null,
+ outcome: rec.outcome,
+ codes: Array.isArray(rec.codes) ? rec.codes : [],
+ }) + '\n';
+ try {
+ mkdirSync(join(EVENTS_PATH, '..'), { recursive: true });
+ } catch {
+ /* dir may already exist or be uncreatable — ignore */
+ }
+ appendFileSync(EVENTS_PATH, line);
+ } catch {
+ /* telemetry MUST NEVER crash or slow an import — swallow everything */
+ }
+}
diff --git a/DW-Programming/ImportNewSkufromURL/lib/shopify.ts b/DW-Programming/ImportNewSkufromURL/lib/shopify.ts
index c04f6b09..60d47cb1 100644
--- a/DW-Programming/ImportNewSkufromURL/lib/shopify.ts
+++ b/DW-Programming/ImportNewSkufromURL/lib/shopify.ts
@@ -952,13 +952,19 @@ async function createProductWithMetafield(dto: ProductDTO, priceUnit: string, un
export interface AddSampleVariantResult {
priced: boolean;
gateBlocked: boolean;
+ // sampleOk:false when the Sample variant could NOT be established (a real,
+ // non-"already exists" productVariantsBulkCreate userError, or a path that
+ // never reached the sample create). The caller must NOT promote a product to
+ // ACTIVE without a memo sample (DW hard rule: every product has a Sample),
+ // so ACTIVE promotion ANDs this with priced && !gateBlocked.
+ sampleOk: boolean;
}
// Price BOTH the per-unit (Per Roll / Per Yard) variant AND the Sample variant that
// were auto-created from the two Size option values. The per-unit variant carries the
// real scraped vendor price (standing rule: every new SKU must have a price per unit);
// the Sample variant is fixed at $4.25 (standing rule: every product has a Sample).
-async function addSampleVariant(productId: string, sku: string, price: string, unitLabel: string, unitKind: string, vendor: string = '', productType: string = 'Wallcovering'): Promise<AddSampleVariantResult> {
+async function addSampleVariant(productId: string, sku: string, price: string, unitLabel: string, unitKind: string, vendor: string = '', productType: string = 'Wallcovering', priceDefaulted: boolean = false): Promise<AddSampleVariantResult> {
// First, get the product's variants (Shopify auto-created them from productOptions)
const getVariantsQuery = `
query getProductVariants($id: ID!) {
@@ -1033,7 +1039,10 @@ async function addSampleVariant(productId: string, sku: string, price: string, u
price: Number(numericUnitPrice),
orderable: true, // CONTINUE + setInventoryQuantity(...,2025) below make it orderable
sku: `${sku}-${unitKind}`,
- priceSource: numericPrice ? 'scraped' : 'defaulted',
+ // Provenance is 'scraped' ONLY when the caller had a real dto.price that also
+ // parsed to a number; a defaulted literal (e.g. '4.25') or a non-numeric price
+ // is 'defaulted' so the gate's Class-A provenance defense can actually fire.
+ priceSource: (!priceDefaulted && numericPrice) ? 'scraped' : 'defaulted',
},
{ role: 'sample', price: sampleFloor, orderable: false, sku: `${sku}-Sample` },
],
@@ -1066,10 +1075,15 @@ async function addSampleVariant(productId: string, sku: string, price: string, u
// (2026-09-14 fix) do NOT write the bad price — publish blocked. Return gateBlocked
// so createOrGetProduct() force-keeps the product DRAFT and skips publish, instead
// of relying on the caller to notice a swallowed void return.
- if (gateBlocked) return { priced: false, gateBlocked: true };
+ if (gateBlocked) return { priced: false, gateBlocked: true, sampleOk: false };
// 1) Update the per-unit variant
let priced = false;
+ // Sample establishment tracker (review 2026-09-14): stays true when a Sample
+ // variant already exists or is created cleanly; flipped false on a real
+ // create failure so the caller keeps the product DRAFT instead of shipping
+ // an ACTIVE product with no memo sample.
+ let sampleOk = true;
if (unitVariant) {
const upd = await shopifyGraphQL<{
productVariantsBulkUpdate: {
@@ -1096,7 +1110,10 @@ async function addSampleVariant(productId: string, sku: string, price: string, u
priced = true;
}
const iid = upd.productVariantsBulkUpdate.productVariants?.[0]?.inventoryItem?.id;
- if (iid) await setInventoryQuantity(iid, 2025);
+ // Only stock the variant when the price write CONFIRMED error-free. A partial
+ // success (variants populated alongside userErrors) must NOT leave a variant
+ // stocked+orderable at an unconfirmed/failed price (the $0-orderable class).
+ if (iid && !uerr?.length) await setInventoryQuantity(iid, 2025);
}
// 2) Create the Sample variant (idempotent — skip if one already exists)
@@ -1121,6 +1138,17 @@ async function addSampleVariant(productId: string, sku: string, price: string, u
const cerr = cre.productVariantsBulkCreate.userErrors;
if (cerr?.length && !JSON.stringify(cerr).toLowerCase().includes('already exists')) {
console.error(JSON.stringify({ event: 'shopify_sample_variant_error', productId, errors: cerr }));
+ // (review 2026-09-14) A real Sample-create failure must block ACTIVE
+ // promotion — otherwise the product goes live with a sellable but NO memo
+ // sample (DW hard rule violation the price gate can't see, since it only
+ // validated the INTENDED sample pre-write).
+ sampleOk = false;
+ } else if (!cre.productVariantsBulkCreate.productVariants?.length &&
+ !JSON.stringify(cerr || []).toLowerCase().includes('already exists')) {
+ // No variant returned and no "already exists" error => the sample was not
+ // actually created. Treat as not-established so we don't promote blind.
+ console.error(JSON.stringify({ event: 'shopify_sample_variant_missing', productId }));
+ sampleOk = false;
}
}
@@ -1131,7 +1159,7 @@ async function addSampleVariant(productId: string, sku: string, price: string, u
unitLabel,
unitPrice: numericUnitPrice,
}));
- return { priced, gateBlocked: false };
+ return { priced, gateBlocked: false, sampleOk };
} catch (error) {
console.error(JSON.stringify({
timestamp: new Date().toISOString(),
@@ -1143,7 +1171,7 @@ async function addSampleVariant(productId: string, sku: string, price: string, u
// caller does not promote an unpriced/unwritten product to ACTIVE + published
// (2026-09-14 fix — this swallowed exception used to leave the caller with no
// way to know the price write never landed).
- return { priced: false, gateBlocked: false };
+ return { priced: false, gateBlocked: false, sampleOk: false };
}
}
@@ -1997,7 +2025,8 @@ export async function createOrGetProduct(dto: ProductDTO): Promise<ShopifyProduc
// back to 'Wallcovering' (the DW house default) only when it is absent, preserving prior
// behavior for typeless imports. Carnegie and other fabric lines route through this engine.
const resolvedProductType = dto.specs?.product_type || 'Wallcovering';
- const variantResult = await addSampleVariant(product.id, sku, price, unitLabel, unitKind, vendorName, resolvedProductType);
+ // price === dto.price || '4.25'; pass whether it was DEFAULTED so the gate sees true provenance.
+ const variantResult = await addSampleVariant(product.id, sku, price, unitLabel, unitKind, vendorName, resolvedProductType, !dto.price);
// Link the DW SKU to its Shopify product in the registry (audit + future dedup).
if (assignment) {
@@ -2014,7 +2043,10 @@ export async function createOrGetProduct(dto: ProductDTO): Promise<ShopifyProduc
// !variantResult.gateBlocked). A gate BLOCK, gate infra error, or failed price write
// now leaves the product DRAFT + tagged, never ACTIVE + all-channels-published at $0
// (TK-11357 recurrence this fix closes).
- const priceWriteOk = variantResult.priced && !variantResult.gateBlocked;
+ // (review 2026-09-14) sampleOk ANDed in: a product with no established memo
+ // sample must stay DRAFT (DW hard rule: every product has a Sample). A failed
+ // Sample create used to be logged-and-ignored while the product still went ACTIVE.
+ const priceWriteOk = variantResult.priced && !variantResult.gateBlocked && variantResult.sampleOk;
if (eligibleForActive && priceWriteOk) {
const promoted = await setProductStatus(product.id, 'ACTIVE');
if (promoted) {
@@ -2036,6 +2068,10 @@ export async function createOrGetProduct(dto: ProductDTO): Promise<ShopifyProduc
if (variantResult.gateBlocked || !variantResult.priced) {
await addProductTags(product.id, ['Needs-Price-Review']);
}
+ // (review 2026-09-14) A missing/failed Sample is its own kept-DRAFT reason.
+ if (!variantResult.sampleOk) {
+ await addProductTags(product.id, ['Needs-Sample']);
+ }
console.warn(JSON.stringify({
event: 'product_kept_draft',
productId: product.id,
@@ -2043,6 +2079,7 @@ export async function createOrGetProduct(dto: ProductDTO): Promise<ShopifyProduc
eligibleForActive,
gateBlocked: variantResult.gateBlocked,
priced: variantResult.priced,
+ sampleOk: variantResult.sampleOk,
}));
}
diff --git a/DW-Programming/ImportNewSkufromURL/lib/weight-guard.ts b/DW-Programming/ImportNewSkufromURL/lib/weight-guard.ts
new file mode 100644
index 00000000..6f46a611
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/lib/weight-guard.ts
@@ -0,0 +1,74 @@
+// weight-guard.ts — TK-11539 (2026-09-12) CREATE-PATH weight guard for the shared
+// ImportNewSkufromURL engine (Carnegie + every vendor routed through /api/create-product).
+//
+// Prevents a recurrence of the TK-11414 zero-weight gap: createOrGetProduct/addSampleVariant
+// created variants with inventoryItem:{sku,tracked} but NEVER set measurement.weight, so every
+// imported product shipped at ZERO weight. Zero weight collapses orders into the lowest
+// freight tier / the free-shipping band and mis-costs DW shipping (Steve's rule: NO product
+// may go ACTIVE with a missing/zero product WEIGHT).
+//
+// Values mirror the canonical backfill primitive:
+// ~/Projects/designerwallcoverings/scripts/lib/weight-guard.mjs
+// (SAMPLE_WEIGHT_LB, FALLBACK_LB, TYPE_DEFAULT_LB). Keep this in sync with that file and the
+// approved TK-11414 backfill. Unit is always POUNDS (Shopify WeightUnit enum).
+
+export const SAMPLE_WEIGHT_LB = 0.25;
+export const FALLBACK_LB = 2.0;
+
+// product_type -> sellable default weight (POUNDS). Mirrors weight-guard.mjs.
+export const TYPE_DEFAULT_LB: Record<string, number> = {
+ 'Wallcovering': 3.0, 'Wallcoverings': 3.0, 'Wallpaper': 3.0,
+ 'Metallic Wallcovering': 3.0, 'Commercial Wallcovering': 3.0,
+ 'Mural': 4.0,
+ 'Fabric': 1.0, 'Commercial Fabric': 1.0, 'Commercial Drapery': 1.0,
+ 'Trim': 0.5, 'Acoustic Panel': 6.0, 'Pillow': 1.5,
+ 'Upholstered Walls/Panels': 6.0, 'Tin Ceiling Tile': 2.0,
+ 'Hardware': 1.0, 'Furniture': 15.0, 'Memo Sample': 0.25,
+};
+
+export type WeightUnit = 'POUNDS';
+
+export interface WeightMeasurement {
+ measurement: { weight: { value: number; unit: WeightUnit } };
+}
+
+const norm = (t?: string | null): string => String(t ?? '').trim().toLowerCase();
+
+/** Sellable default weight (lb) for a product_type; unknown/blank -> FALLBACK_LB. Case-insensitive. */
+export function sellableWeightLb(productType?: string | null): number {
+ const key = Object.keys(TYPE_DEFAULT_LB).find(k => norm(k) === norm(productType));
+ const w = key ? TYPE_DEFAULT_LB[key] : FALLBACK_LB;
+ return (Number.isFinite(w) && w > 0) ? w : FALLBACK_LB;
+}
+
+/** Sample-variant weight (lb) — always the approved 0.25 lb. */
+export function sampleWeightLb(): number {
+ return SAMPLE_WEIGHT_LB;
+}
+
+/**
+ * THE CREATE-SIDE GUARD. Merge a positive measurement.weight into a
+ * ProductVariantsBulkInput.inventoryItem input so no variant is ever created zero-weight.
+ * role 'sample' -> SAMPLE_WEIGHT_LB (0.25 lb)
+ * role 'sellable' -> per-productType default (FALLBACK_LB when unknown)
+ * The returned inventoryItem ALWAYS carries weight.value > 0.
+ * Valid on ProductVariantsBulkInput.inventoryItem (InventoryItemInput.measurement) in the
+ * Shopify Admin API 2024-07+.
+ */
+export function inventoryItemWithWeight<T extends object>(
+ base: T,
+ opts: { role: 'sellable' | 'sample'; productType?: string | null }
+): T & WeightMeasurement {
+ const value = opts.role === 'sample' ? sampleWeightLb() : sellableWeightLb(opts.productType);
+ return { ...base, measurement: { weight: { value, unit: 'POUNDS' } } };
+}
+
+/**
+ * Invariant used by the negative test and callable as a runtime assert: a variant's
+ * inventoryItem input must carry a finite, positive measurement.weight. Returns false for
+ * the pre-fix payload (no measurement), which is exactly the fault the guard removes.
+ */
+export function hasPositiveWeight(inventoryItemInput: any): boolean {
+ const v = inventoryItemInput?.measurement?.weight?.value;
+ return typeof v === 'number' && Number.isFinite(v) && v > 0;
+}
← 25dadd6f fix(TK-11503): validate Shopify token via read_products prob
·
back to Designer Wallcoverings
·
fix(rimg): drop placeholder srcset + use native loading=lazy e0bf2137 →