← 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
M data/tk11449-smartcollection-title-rules.jsonA tk11449-negtest-substring-semantics.mjs
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 →