[object Object]

← back to Gmc Titlefix

TK-11449: add substring-vs-token negative test + NOT_MEASURED bucket + adjacent-surface caveats

6718c593c2cb175a735754b94b78c128e3f3714e · 2026-09-11 11:24:33 -0700 · Steve

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QzRJQxiT3DKZRRfzWsRTVM

Files touched

Diff

commit 6718c593c2cb175a735754b94b78c128e3f3714e
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Sep 11 11:24:33 2026 -0700

    TK-11449: add substring-vs-token negative test + NOT_MEASURED bucket + adjacent-surface caveats
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01QzRJQxiT3DKZRRfzWsRTVM
---
 data/tk11449-smartcollection-title-rules.json | 117 +++++++++++++++++---------
 tk11449-negtest-substring-semantics.mjs       |  30 +++++++
 2 files changed, 106 insertions(+), 41 deletions(-)

diff --git a/data/tk11449-smartcollection-title-rules.json b/data/tk11449-smartcollection-title-rules.json
index 42771e0..2dccec4 100644
--- a/data/tk11449-smartcollection-title-rules.json
+++ b/data/tk11449-smartcollection-title-rules.json
@@ -4,7 +4,7 @@
  "read_only": true,
  "mutations": 0,
  "cost_usd": 0,
- "source": "shopify admin graphql 2024-10 · designer-laboratory-sandbox.myshopify.com",
+ "source": "shopify admin graphql 2024-10 \u00b7 designer-laboratory-sandbox.myshopify.com",
  "enumerated_at": "2026-09-11T18:18:52.825Z",
  "simulated_at": "2026-09-11T18:20:16.652Z",
  "1_ENUMERATION": {
@@ -2187,7 +2187,7 @@
      {
       "column": "VENDOR",
       "relation": "CONTAINS",
-      "condition": "Coordonné Europe"
+      "condition": "Coordonn\u00e9 Europe"
      }
     ],
     "title_substrings": [
@@ -7416,7 +7416,7 @@
      {
       "column": "TAG",
       "relation": "EQUALS",
-      "condition": "Rimini������ Faux Leathers"
+      "condition": "Rimini\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd Faux Leathers"
      },
      {
       "column": "TAG",
@@ -9920,7 +9920,7 @@
      {
       "column": "VENDOR",
       "relation": "EQUALS",
-      "condition": "Fentucci™"
+      "condition": "Fentucci\u2122"
      }
     ],
     "title_substrings": [
@@ -14640,7 +14640,7 @@
    {
     "id": "gid://shopify/Collection/298505863219",
     "handle": "coordonne",
-    "title": "Coordonné",
+    "title": "Coordonn\u00e9",
     "online_store_published": true,
     "publications": [
      "Online Store",
@@ -14667,26 +14667,26 @@
      {
       "column": "TITLE",
       "relation": "CONTAINS",
-      "condition": "Coordonné"
+      "condition": "Coordonn\u00e9"
      },
      {
       "column": "TAG",
       "relation": "EQUALS",
-      "condition": "Coordonné"
+      "condition": "Coordonn\u00e9"
      },
      {
       "column": "VENDOR",
       "relation": "EQUALS",
-      "condition": "Coordonné"
+      "condition": "Coordonn\u00e9"
      },
      {
       "column": "VENDOR",
       "relation": "EQUALS",
-      "condition": "Coordonné Europe"
+      "condition": "Coordonn\u00e9 Europe"
      }
     ],
     "title_substrings": [
-     "Coordonné"
+     "Coordonn\u00e9"
     ],
     "simulated": {
      "edit_A_inch_normalize": null,
@@ -15650,7 +15650,7 @@
      {
       "column": "VENDOR",
       "relation": "EQUALS",
-      "condition": "Boråstapeter"
+      "condition": "Bor\u00e5stapeter"
      },
      {
       "column": "VENDOR",
@@ -15660,7 +15660,7 @@
      {
       "column": "VENDOR",
       "relation": "EQUALS",
-      "condition": "Boråstapeter Wallpaper"
+      "condition": "Bor\u00e5stapeter Wallpaper"
      }
     ],
     "title_substrings": [
@@ -18307,7 +18307,7 @@
    {
     "id": "gid://shopify/Collection/159993659443",
     "handle": "mind-the-gap",
-    "title": "Mind the Gap™",
+    "title": "Mind the Gap\u2122",
     "online_store_published": true,
     "live_products_count": 1261,
     "appliedDisjunctively": true,
@@ -19045,7 +19045,7 @@
      {
       "column": "VENDOR",
       "relation": "EQUALS",
-      "condition": "Infinite Size™ Commercial"
+      "condition": "Infinite Size\u2122 Commercial"
      }
     ]
    },
@@ -19462,7 +19462,7 @@
    {
     "id": "gid://shopify/Collection/283469086771",
     "handle": "coordonne-collection-european-wallpaper",
-    "title": "Coordonné Wallcovering Collection",
+    "title": "Coordonn\u00e9 Wallcovering Collection",
     "online_store_published": false,
     "live_products_count": 29,
     "appliedDisjunctively": false,
@@ -19470,7 +19470,7 @@
      {
       "column": "VENDOR",
       "relation": "EQUALS",
-      "condition": "Coordonné Europe"
+      "condition": "Coordonn\u00e9 Europe"
      }
     ]
    },
@@ -19560,7 +19560,7 @@
      {
       "column": "VENDOR",
       "relation": "EQUALS",
-      "condition": "Boråstapeter Wallpaper"
+      "condition": "Bor\u00e5stapeter Wallpaper"
      },
      {
       "column": "VENDOR",
@@ -19820,7 +19820,7 @@
      {
       "column": "VENDOR",
       "relation": "EQUALS",
-      "condition": "Boråstapeter Wallpaper"
+      "condition": "Bor\u00e5stapeter Wallpaper"
      },
      {
       "column": "VENDOR",
@@ -19880,7 +19880,7 @@
      {
       "column": "VENDOR",
       "relation": "EQUALS",
-      "condition": "Coordonné Europe"
+      "condition": "Coordonn\u00e9 Europe"
      },
      {
       "column": "VENDOR",
@@ -19896,8 +19896,8 @@
    },
    {
     "id": "gid://shopify/Collection/294320046131",
-    "handle": "novasuede™-microfiber-suede",
-    "title": "Novasuede™ Microfiber Suede",
+    "handle": "novasuede\u2122-microfiber-suede",
+    "title": "Novasuede\u2122 Microfiber Suede",
     "online_store_published": true,
     "live_products_count": 173,
     "appliedDisjunctively": false,
@@ -24862,7 +24862,7 @@
    {
     "id": "gid://shopify/Collection/299962433587",
     "handle": "malibu-walls-coastal",
-    "title": "Malibu Walls — Coastal",
+    "title": "Malibu Walls \u2014 Coastal",
     "online_store_published": false,
     "live_products_count": 752,
     "appliedDisjunctively": false,
@@ -24882,7 +24882,7 @@
    {
     "id": "gid://shopify/Collection/299962466355",
     "handle": "malibu-walls-contemporary",
-    "title": "Malibu Walls — Contemporary",
+    "title": "Malibu Walls \u2014 Contemporary",
     "online_store_published": false,
     "live_products_count": 1042,
     "appliedDisjunctively": false,
@@ -24902,7 +24902,7 @@
    {
     "id": "gid://shopify/Collection/299962499123",
     "handle": "malibu-walls-traditional",
-    "title": "Malibu Walls — Traditional",
+    "title": "Malibu Walls \u2014 Traditional",
     "online_store_published": false,
     "live_products_count": 483,
     "appliedDisjunctively": false,
@@ -24922,7 +24922,7 @@
    {
     "id": "gid://shopify/Collection/299962531891",
     "handle": "malibu-walls-transitional",
-    "title": "Malibu Walls — Transitional",
+    "title": "Malibu Walls \u2014 Transitional",
     "online_store_published": false,
     "live_products_count": 286,
     "appliedDisjunctively": false,
@@ -24942,7 +24942,7 @@
    {
     "id": "gid://shopify/Collection/299962564659",
     "handle": "malibu-walls-scandinavian",
-    "title": "Malibu Walls — Scandinavian",
+    "title": "Malibu Walls \u2014 Scandinavian",
     "online_store_published": false,
     "live_products_count": 514,
     "appliedDisjunctively": false,
@@ -24962,7 +24962,7 @@
    {
     "id": "gid://shopify/Collection/299962597427",
     "handle": "malibu-walls-minimalist",
-    "title": "Malibu Walls — Minimalist",
+    "title": "Malibu Walls \u2014 Minimalist",
     "online_store_published": false,
     "live_products_count": 429,
     "appliedDisjunctively": false,
@@ -24982,7 +24982,7 @@
    {
     "id": "gid://shopify/Collection/299962630195",
     "handle": "malibu-walls-abstract",
-    "title": "Malibu Walls — Abstract",
+    "title": "Malibu Walls \u2014 Abstract",
     "online_store_published": false,
     "live_products_count": 595,
     "appliedDisjunctively": false,
@@ -25002,7 +25002,7 @@
    {
     "id": "gid://shopify/Collection/299962662963",
     "handle": "malibu-walls-grasscloth",
-    "title": "Malibu Walls — Grasscloth & Woven",
+    "title": "Malibu Walls \u2014 Grasscloth & Woven",
     "online_store_published": false,
     "live_products_count": 297,
     "appliedDisjunctively": false,
@@ -25022,7 +25022,7 @@
    {
     "id": "gid://shopify/Collection/299962695731",
     "handle": "malibu-walls-floral",
-    "title": "Malibu Walls — Floral & Botanical",
+    "title": "Malibu Walls \u2014 Floral & Botanical",
     "online_store_published": false,
     "live_products_count": 363,
     "appliedDisjunctively": false,
@@ -25042,7 +25042,7 @@
    {
     "id": "gid://shopify/Collection/299962728499",
     "handle": "malibu-walls-geometric",
-    "title": "Malibu Walls — Geometric",
+    "title": "Malibu Walls \u2014 Geometric",
     "online_store_published": false,
     "live_products_count": 506,
     "appliedDisjunctively": false,
@@ -25062,7 +25062,7 @@
    {
     "id": "gid://shopify/Collection/299962761267",
     "handle": "malibu-walls-stripes",
-    "title": "Malibu Walls — Stripes",
+    "title": "Malibu Walls \u2014 Stripes",
     "online_store_published": false,
     "live_products_count": 401,
     "appliedDisjunctively": false,
@@ -25082,7 +25082,7 @@
    {
     "id": "gid://shopify/Collection/299962794035",
     "handle": "malibu-walls-bedroom",
-    "title": "Malibu Walls — Bedroom",
+    "title": "Malibu Walls \u2014 Bedroom",
     "online_store_published": false,
     "live_products_count": 134,
     "appliedDisjunctively": false,
@@ -25102,7 +25102,7 @@
    {
     "id": "gid://shopify/Collection/299962826803",
     "handle": "malibu-walls-living-room",
-    "title": "Malibu Walls — Living Room",
+    "title": "Malibu Walls \u2014 Living Room",
     "online_store_published": false,
     "live_products_count": 128,
     "appliedDisjunctively": false,
@@ -27715,7 +27715,7 @@
   {
    "id": "gid://shopify/Collection/297892872243",
    "handle": "phillipe-romano-sante-collection",
-   "title": "Phillipe Romano - Santé Collection",
+   "title": "Phillipe Romano - Sant\u00e9 Collection",
    "products_count": 32,
    "online_store_published": true,
    "publications": [
@@ -28039,7 +28039,7 @@
    "ACTIVE": 75,
    "ARCHIVED": 66
   },
-  "validation": "local rule replication cross-checked against Shopify's own evaluation: collection(279808081971).products returned 141 members with the identical 75 ACTIVE / 66 ARCHIVED split as the simulated family set — the simulator reproduces ground truth, so a zero-delta result is MEASURED, not assumed",
+  "validation": "local rule replication cross-checked against Shopify's own evaluation: collection(279808081971).products returned 141 members with the identical 75 ACTIVE / 66 ARCHIVED split as the simulated family set \u2014 the simulator reproduces ground truth, so a zero-delta result is MEASURED, not assumed",
   "candidate_edits": {
    "A_inch_normalize": "Pipa 12\" -> Pipa 12-inch (REMOVES the inch-mark substring)",
    "B_append_suffix": "append \" Wallcovering | DW Bespoke Studios\" (APPEND ONLY, removes nothing)"
@@ -28293,7 +28293,7 @@
    }
   ],
   "pre_existing_data_bug": {
-   "note": "family-shaped titles that the collection ruleSet already MISSES (size token does not match any rule condition) — standing merchandising gap independent of this ticket",
+   "note": "family-shaped titles that the collection ruleSet already MISSES (size token does not match any rule condition) \u2014 standing merchandising gap independent of this ticket",
    "rows": [
     {
      "handle": "piza-stripe-classic-green-legacy-2024",
@@ -28328,7 +28328,42 @@
      "title": "Piza Pussy Willows"
     }
    ]
-  }
+  },
+  "simulator_negative_test": {
+   "why": "A positive-only check on a detector proves nothing. Shopify's TITLE CONTAINS could be TOKEN-based rather than raw-SUBSTRING; if it were, every zero-delta result here would be measuring the wrong thing.",
+   "test": "collection 'dresses-phillipe-romano' has the single rule TITLE CONTAINS 'dress'. Fetched its live members.",
+   "result": "30/30 members match 'dress' as a RAW SUBSTRING; only 11/30 match it as a whole word. Members include 'Tek-wall Lumen Headdress' and 'Tek-wall Lucent Sundress'.",
+   "conclusion": "Raw-substring, case-insensitive semantics CONFIRMED against live data. The simulator models the right operation.",
+   "second_validation": "collection(279808081971).products returned exactly 141 members with the identical 75 ACTIVE / 66 ARCHIVED split as the independently-built family set \u2014 simulator reproduces Shopify's own evaluation."
+  },
+  "NOT_MEASURED": {
+   "note": "Per the no-false-green rule these are reported separately rather than folded into UNAFFECTED.",
+   "collections": [
+    {
+     "handle": "upholstery-fabrics",
+     "live_products_count": 13458,
+     "unmodellable_rule": "PRODUCT_CATEGORY_ID EQUALS gid://shopify/TaxonomyCategory/vp-1-4-2-8"
+    },
+    {
+     "handle": "coral",
+     "live_products_count": 4176,
+     "unmodellable_rule": "VARIANT_TITLE CONTAINS 'CORAL'"
+    },
+    {
+     "handle": "beige-cream-wallpaper-collection",
+     "live_products_count": 8769,
+     "unmodellable_rule": "PRODUCT_CATEGORY_ID EQUALS gid://shopify/TaxonomyCategory/hg-3-68"
+    }
+   ],
+   "absolute_membership": "NOT MEASURED for the family (the simulator cannot evaluate VARIANT_TITLE / PRODUCT_CATEGORY_ID from product-level fields).",
+   "delta_under_both_edits": "PROVEN ZERO by rule invariance, not by assumption: all three are appliedDisjunctively=true; neither candidate edit changes the truth value of ANY of their rules (product-title edits do not touch variant titles, taxonomy category, or tags, and neither edit introduces the token 'Upholstery'). A disjunctive set whose every rule holds its truth value cannot change membership."
+  },
+  "adjacent_surfaces_a_ruleSet_sweep_CANNOT_catch": [
+   "PRODUCT HANDLE / landing-page URL. This repo already owns gmc-landing-handle-canary, which exists because renaming a Shopify product has produced handle drift with NO redirect, leaving live GMC offers pointing at dead landing pages. A ruleSet sweep is blind to this. Before any title edit ships, confirm the write path sets ONLY `title` and never `handle`, then re-run the handle canary after.",
+   "BOOST / storefront-search facets and any hard-coded title-substring filters in theme Liquid or app config \u2014 outside the Admin collection ruleSet API and therefore outside this sweep.",
+   "MANUAL collections (66 of them) pin membership by product ID, so a title edit cannot drop a product from one \u2014 they are structurally immune, not merely unmeasured.",
+   "Saved Admin segments / bulk-edit views and any external script that matches on the literal title string."
+  ]
  },
  "5_FLEET_WIDE_TITLE_FRAGILITY": {
   "entirely_title_driven_collections": 47,
@@ -28759,7 +28794,7 @@
     ]
    },
    {
-    "signature": "<=3 chars — over-broad substring",
+    "signature": "<=3 chars \u2014 over-broad substring",
     "count": 7,
     "rules": [
      {
@@ -29064,7 +29099,7 @@
    },
    "zero_downtime_sequencing": [
     "STEP 1 (safe, additive, no membership change): because the ruleSet is DISJUNCTIVE, ADD the four post-edit conditions to the existing four. Nothing matches them yet, so membership stays 141.",
-    "STEP 2: apply the title edit. Each product satisfies the new condition the instant its old one stops matching — membership never dips.",
+    "STEP 2: apply the title edit. Each product satisfies the new condition the instant its old one stops matching \u2014 membership never dips.",
     "STEP 3 (cleanup, after verification): remove the four stale inch-mark conditions.",
     "This ordering makes the change reversible at every step and removes the need to land two writes atomically (Shopify offers no atomic title+ruleSet transaction)."
    ],
@@ -29080,7 +29115,7 @@
    "triggered_by": "edit B only (append \" Wallcovering | DW Bespoke Studios\"). Edit A does NOT trigger it.",
    "effect": "ADDITIVE, not destructive: TITLE CONTAINS \"Bespoke\" starts matching 71 more family products (70 -> 141 family members; collection 1,972 -> ~2,043).",
    "note": "70 of the 141 already carry the suffix, so this is the continuation of an existing convention, not a new class. No rule change is REQUIRED to prevent breakage, but it is a live membership change on a published collection and should be acknowledged/approved rather than discovered.",
-   "if_unwanted": "There is no clean exclusion — TITLE CONTAINS \"Bespoke\" is inherently matched by the proposed suffix. Either accept the additions or choose a suffix that does not contain the token \"Bespoke\"."
+   "if_unwanted": "There is no clean exclusion \u2014 TITLE CONTAINS \"Bespoke\" is inherently matched by the proposed suffix. Either accept the additions or choose a suffix that does not contain the token \"Bespoke\"."
   }
  ],
  "RAILS": {
diff --git a/tk11449-negtest-substring-semantics.mjs b/tk11449-negtest-substring-semantics.mjs
new file mode 100644
index 0000000..1512877
--- /dev/null
+++ b/tk11449-negtest-substring-semantics.mjs
@@ -0,0 +1,30 @@
+/** TK-11449 READ-ONLY negative/discriminating test: does Shopify's TITLE CONTAINS behave as a raw
+ *  SUBSTRING match (my simulator's assumption) or as a TOKEN match? 'dress' vs titles like
+ *  "Dresses ..." discriminates the two. If token-based, my whole simulation is measuring the wrong thing. */
+import fs from 'fs';
+const env=Object.fromEntries(fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8')
+  .split('\n').filter(l=>l.includes('=')&&!l.trim().startsWith('#'))
+  .map(l=>[l.slice(0,l.indexOf('=')).trim(), l.slice(l.indexOf('=')+1).trim().replace(/^["']|["']$/g,'')]));
+const S='designer-laboratory-sandbox.myshopify.com', TOKEN=env.SHOPIFY_ADMIN_TOKEN;
+const q=async(query,variables)=>{const r=await fetch(`https://${S}/admin/api/2024-10/graphql.json`,{method:'POST',
+  headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},body:JSON.stringify({query,variables})});
+  return r.json();};
+(async()=>{
+ // A) 'dress' collection — discriminates substring vs token
+ const j=await q(`query($h:String!){ collectionByHandle(handle:$h){ id handle ruleSet{appliedDisjunctively rules{column relation condition}}
+   products(first:60){nodes{title}} } }`,{h:'dresses-phillipe-romano'});
+ const c=j.data.collectionByHandle;
+ const titles=c.products.nodes.map(n=>n.title);
+ const exactWord=titles.filter(t=>/\bdress\b/i.test(t)).length;
+ const substr=titles.filter(t=>t.toLowerCase().includes('dress')).length;
+ console.log('dresses-phillipe-romano ruleSet:',JSON.stringify(c.ruleSet));
+ console.log(' members sampled:',titles.length,'| raw-substring match:',substr,'| whole-word match:',exactWord);
+ console.log(' sample titles:',titles.slice(0,5));
+ console.log(' VERDICT:', substr===titles.length && exactWord<titles.length
+   ? 'SUBSTRING semantics CONFIRMED (members match substring but not whole-word) — simulator correct'
+   : (substr===titles.length? 'substring holds but test not discriminating here' : 'MISMATCH — investigate'));
+
+ // B) the lone VARIANT_TITLE rule — could a product title edit touch it?
+ const k=await q(`query{ collections(first:250, query:"") { nodes{ handle ruleSet{rules{column relation condition}} } } }`,{});
+ console.log('\n(skipping full re-enum; VARIANT_TITLE located from artifact instead)');
+})();

← 4f6033c TK-11449: read-only sweep of all 709 Shopify collections for  ·  back to Gmc Titlefix  ·  TK-11448: verify GMC image buckets at full population; find 72d7a55 →