From dae833656b63299f97eb81f54f4c4fa865f903ed Mon Sep 17 00:00:00 2001 From: ttlequals0 Date: Fri, 15 May 2026 15:34:38 -0400 Subject: [PATCH 1/3] fix(2.4.7): community patterns normalize to scope=global on export and import Pre-2.4.7 the community export pipeline copied the source instance's scope (almost always 'podcast') into the bundle, and the import pipeline used it verbatim. podcast_id was already stripped on export, so the imported row landed as scope='podcast' with podcast_id=NULL -- which the matcher will never match against any podcast. Per-podcast filtering for community patterns happens via tag eligibility in text_pattern_matcher._filter_patterns_by_scope, not via the legacy scope column; scope on community rows should just be 'global'. Fixes: - community_export._strip_metadata forces scope='global' in the bundle regardless of source scope. - pattern_service.import_community_pattern forces scope='global' and None for podcast_id/network_id/dai_platform on every create. - _normalize_community_scope migration in schema.py UPDATEs every source='community' row whose scope is not 'global' OR whose podcast/ network ids are non-null. Stamped via community_scope_revision setting, idempotent. - 11 of 12 committed seed bundles rewritten from scope:podcast to scope:global; manifest regenerated. Tests: +2 in test_community_export covering scope=podcast and scope=network source patterns (with non-null ids on the source to prove they get stripped). +1 in test_pattern_service_rewrite covering import of a legacy bundle that carries scope=podcast + podcast_id + network_id; asserts they all get cleared on insert. 1087 unit tests pass. --- CHANGELOG.md | 8 +++++ openapi.yaml | 2 +- patterns/community/capital-one-44026a0f.json | 2 +- patterns/community/carvana-a934bf9a.json | 2 +- patterns/community/carvana-f41cf617.json | 2 +- patterns/community/index.json | 24 +++++++-------- patterns/community/instacart-b77d02bc.json | 2 +- patterns/community/kayak-3f2f65ff.json | 2 +- patterns/community/mint-mobile-17b2b4b8.json | 2 +- patterns/community/monday-com-9e83a5f6.json | 2 +- patterns/community/progressive-1c07273d.json | 2 +- patterns/community/simplisafe-c114bd7f.json | 2 +- patterns/community/squarespace-b052ed12.json | 2 +- patterns/community/zyn-3c348177.json | 2 +- src/community_export.py | 9 +++++- src/database/schema.py | 31 ++++++++++++++++++++ src/pattern_service.py | 13 +++++--- tests/unit/test_community_export.py | 21 +++++++++++++ tests/unit/test_pattern_service_rewrite.py | 19 ++++++++++++ version.py | 2 +- 20 files changed, 121 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2953e5f9..47db12b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.4.7] - 2026-05-15 + +### Fixed + +- **Community patterns now import with `scope='global'` instead of preserving the source instance's scope.** Pre-2.4.7 the export pipeline copied the source pattern's `scope` (almost always `'podcast'`) into the bundle, and the import pipeline used it verbatim. Since `podcast_id` is stripped on export, the imported row was scope='podcast' with `podcast_id=NULL` and never matched anything. Tag eligibility (`text_pattern_matcher._filter_patterns_by_scope`) is what actually gates community patterns per podcast; the legacy scope column should just be `global`. +- **Migration repairs existing community rows.** `_normalize_community_scope` UPDATEs every `source='community'` row to `scope='global'`, clears `podcast_id` / `network_id`. Stamped via `community_scope_revision`; idempotent. +- **12 seed bundle files in `patterns/community/`** were rewritten from `scope: podcast` to `scope: global`. Manifest regenerated. Earlier 2.4.0-2.4.6 syncs pulled them with scope=podcast; the migration above re-stamps those rows on the next container start. + ## [2.4.6] - 2026-05-15 ### Fixed diff --git a/openapi.yaml b/openapi.yaml index 519518e5..80b1b351 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -12,7 +12,7 @@ info: - Monitor system status and trigger cleanup operations - Manage cross-episode ad patterns with network and podcast scope - Submit corrections to improve ad detection accuracy - version: 2.4.6 + version: 2.4.7 contact: name: MinusPod license: diff --git a/patterns/community/capital-one-44026a0f.json b/patterns/community/capital-one-44026a0f.json index 6a88c61c..6be311f9 100644 --- a/patterns/community/capital-one-44026a0f.json +++ b/patterns/community/capital-one-44026a0f.json @@ -1,5 +1,5 @@ { - "scope": "podcast", + "scope": "global", "text_template": "brought to you by Capital One. Capital One's tech team isn't just talking about multi-agentic AI. They already deployed one. It's called Chat Concierge and it's simplifying car shopping. Using self-reflection and layered reasoning with live API checks, it doesn't just help buyers find a car they love. It helps schedule a test drive, get pre-approved for financing, and estimate trade-in value. Advanced, intuitive, and deployed. That's how they stack. That's technology at Capital One.", "intro_variants": [ "brought to you by Capital One. Capital One's tech team isn't just talking about multi-agentic AI. They already deployed one. It's called Chat Concierge and it's simplifying car shopping. Using self-reflection and layered reasoning with live API checks, it doesn't just help buyers find a car they love. It helps" diff --git a/patterns/community/carvana-a934bf9a.json b/patterns/community/carvana-a934bf9a.json index 8d5436d4..fb6e475d 100644 --- a/patterns/community/carvana-a934bf9a.json +++ b/patterns/community/carvana-a934bf9a.json @@ -1,5 +1,5 @@ { - "scope": "podcast", + "scope": "global", "text_template": " just sold my car online. Let's go, Grandpa. Wait, you did? Yep, on Carvana. Just put in the license plate, answered a few questions, got an offer in minutes. Easier than setting up that new digital picture frame. You don't say. Yeah, they're even picking it up tomorrow. Talk about fast. Wow, way to go. So, about that picture frame. Ah, forget about it. Until Carvana makes one, I'm not interested.", "intro_variants": [ "I just sold my car online. Let's go, Grandpa. Wait, you did? Yep, on Carvana. Just put in the license plate, answered a few questions, got an offer in minutes. Easier than setting up that new digital picture frame. You don't say. Yeah, they're even picking it up tomorrow." diff --git a/patterns/community/carvana-f41cf617.json b/patterns/community/carvana-f41cf617.json index 9135aba3..f71c45ff 100644 --- a/patterns/community/carvana-f41cf617.json +++ b/patterns/community/carvana-f41cf617.json @@ -1,5 +1,5 @@ { - "scope": "podcast", + "scope": "global", "text_template": "Carvana's so easy, just a click and we've got ourselves a car. See? So many cars. That's a click-tastic inventory. And check out the financing options. Payments to fit our budget. I mean, that's... Clickonomics 101. Delivery to our door. Just a hop, skip, and a click away. And... bought. No better feeling than when everything just... clicks. Buy your car today on... Carvana. Delivery fees may apply.", "intro_variants": [ "Carvana's so easy, just a click and we've got ourselves a car. See? So many cars. That's a click-tastic inventory. And check out the financing options. Payments to fit our budget. I mean, that's... Clickonomics 101. Delivery to our door. Just a hop, skip, and a click away. And... bought." diff --git a/patterns/community/index.json b/patterns/community/index.json index 73ad1b0f..9ec49651 100644 --- a/patterns/community/index.json +++ b/patterns/community/index.json @@ -1,13 +1,13 @@ { "manifest_version": 1, - "published_at": "2026-05-15T19:22:57Z", + "published_at": "2026-05-15T19:30:18Z", "vocabulary_version": 1, "patterns": [ { "community_id": "3c348177-fdf5-4f4f-a3de-25f4e65bf591", "version": 1, "data": { - "scope": "podcast", + "scope": "global", "text_template": "What are you reaching for? If you're a smoker or vaper, you could be reaching for so much more with Zyn Nicotine Pouches. When you reach for Zyn, you're reaching for 10 satisfying varieties and two strengths, for a smoke-free experience that lets you lean in, for chances to break free from your routine, and a unique nationwide community. Whatever you're reaching for, reach for it with America's number one nicotine pouch brand. Find your Zyn wherever nicotine products are sold near you. This product contains nicotine. Nicotine is an addictive chemical. ", "intro_variants": [], "outro_variants": [], @@ -59,7 +59,7 @@ "community_id": "3f2f65ff-c654-42ca-a724-d6dce1f45881", "version": 1, "data": { - "scope": "podcast", + "scope": "global", "text_template": "Kayak gets my flight, hotel, and rental car right, so I can tune out travel advice that's just plain wrong. Bro, Skycoin. way better than points never fly during a Scorpio full moon just tell the manager you'll sue instant room upgrade stop taking bad travel advice start comparing hundreds of sites with Kayak and get your trip right Kayak got that right", "intro_variants": [ "Kayak gets my flight, hotel, and rental car right, so I can tune out travel advice that's just plain wrong." @@ -85,7 +85,7 @@ "community_id": "1c07273d-4e1c-41fd-93dd-0455904ada87", "version": 1, "data": { - "scope": "podcast", + "scope": "global", "text_template": "\brought to you by Progressive Insurance. Do you ever find yourself playing the budgeting game? Well, with the Name Your Price tool from Progressive, you can find options that fit your budget and potentially lower your bills. Try it at Progressive.com. Progressive casualty insurance company and affiliates. Price and coverage match limited by state law. Not available in all states.", "intro_variants": [ "brought to you by Progressive Insurance. Do you ever find yourself playing the budgeting game?" @@ -111,7 +111,7 @@ "community_id": "17b2b4b8-660e-4ab8-96c0-c51a0dc3163d", "version": 1, "data": { - "scope": "podcast", + "scope": "global", "text_template": "Ryan Reynolds here from Mint Mobile. I don't know if you knew this, but anyone can get the same premium wireless for $15 a month plan that I've been enjoying. It's not just for celebrities. So do like I did and have one of your assistant's assistants switch you to Mint Mobile today. I'm told it's super easy to do at mintmobile.com slash switch. Upfront payment of $45 for three-month plan equivalent to $15 per month required. Intro rate first three months only, then full price plan options available. Taxes and fees extra. See full terms at mintmobile.com.", "intro_variants": [ "Ryan Reynolds here from Mint Mobile. I don't know if you knew this, but anyone can get the same premium wireless for $15 a month plan that I've been enjoying. It's not just for celebrities. So do like I did and have one of your assistant's assistants switch you to" @@ -139,7 +139,7 @@ "community_id": "f41cf617-6ad0-4df5-a07a-a4daeb449697", "version": 1, "data": { - "scope": "podcast", + "scope": "global", "text_template": "Carvana's so easy, just a click and we've got ourselves a car. See? So many cars. That's a click-tastic inventory. And check out the financing options. Payments to fit our budget. I mean, that's... Clickonomics 101. Delivery to our door. Just a hop, skip, and a click away. And... bought. No better feeling than when everything just... clicks. Buy your car today on... Carvana. Delivery fees may apply.", "intro_variants": [ "Carvana's so easy, just a click and we've got ourselves a car. See? So many cars. That's a click-tastic inventory. And check out the financing options. Payments to fit our budget. I mean, that's... Clickonomics 101. Delivery to our door. Just a hop, skip, and a click away. And... bought." @@ -165,7 +165,7 @@ "community_id": "a934bf9a-ebcc-4d64-b734-b82a6de7f25a", "version": 1, "data": { - "scope": "podcast", + "scope": "global", "text_template": " just sold my car online. Let's go, Grandpa. Wait, you did? Yep, on Carvana. Just put in the license plate, answered a few questions, got an offer in minutes. Easier than setting up that new digital picture frame. You don't say. Yeah, they're even picking it up tomorrow. Talk about fast. Wow, way to go. So, about that picture frame. Ah, forget about it. Until Carvana makes one, I'm not interested.", "intro_variants": [ "I just sold my car online. Let's go, Grandpa. Wait, you did? Yep, on Carvana. Just put in the license plate, answered a few questions, got an offer in minutes. Easier than setting up that new digital picture frame. You don't say. Yeah, they're even picking it up tomorrow." @@ -191,7 +191,7 @@ "community_id": "b77d02bc-5ccb-465d-8ecc-c0f3c53efa3d", "version": 1, "data": { - "scope": "podcast", + "scope": "global", "text_template": "We all prefer things a certain way, like groceries. If you want groceries just how you like them, you gotta try Instacart. They have a new preference picker that lets you pick how ripe or unripe you want your bananas. Shoppers can see your preferences up front, helping guide their choices. Because when it comes to groceries, the details matter. Instacart. Get groceries just how you like.", "intro_variants": [ "We all prefer things a certain way, like groceries. If you want groceries just how you like them, you gotta try Instacart. They have a new preference picker that lets you pick how ripe or unripe you want your bananas. Shoppers can see your preferences up front, helping guide their" @@ -217,7 +217,7 @@ "community_id": "b052ed12-557d-4cdb-b24a-28d547dbd9bd", "version": 1, "data": { - "scope": "podcast", + "scope": "global", "text_template": "brought to you by Squarespace. Squarespace makes building and managing your website ridiculously easy. They give you everything you need to showcase what you do and get paid all in one place. And with cutting-edge design tools, anyone can create a custom site that truly fits their brand. Head to squarespace.com slash rogan for a free trial. And when you're ready... Ready to launch? Use the offer code ROGAN to save 10% off your first purchase of a website or domain.", "intro_variants": [ "brought to you by Squarespace. Squarespace makes building and managing your website ridiculously easy. They give you everything you need to showcase what you do and get paid all in one place. And with cutting-edge design tools, anyone can create a custom site that truly fits their brand. Head to" @@ -247,7 +247,7 @@ "community_id": "c114bd7f-39af-4100-826d-e9ed1346d76f", "version": 1, "data": { - "scope": "podcast", + "scope": "global", "text_template": "brought to you by SimpliSafe. The world can be scary sometimes. I mean, take the news for example. It feels like we hear about something outrageous happening Every week. Now, more than ever, it's more important to have a security system for your safety and peace of mind, and SimpliSafe is one of the best options out there, partly because of how proactive it is. It can help stop and prevent crime in real time. AI-powered cameras can detect suspicious activity and alert security agents who can immediately take action. They can speak to intruders, warn them away, flashlights and sirens and and Dispatch Police. With how great a job it does, no long-term contracts and no hidden fees, it's easy to see why SimpliSafe continues to be named best home security systems by U.S. News and World Report. Try it out right now. My listeners can get 50% off a SimpliSafe home security system at simplisafe.com slash . That's 50% off at simplisafe.com slash . There's no safe.", "intro_variants": [ "brought to you by SimpliSafe. The world can be scary sometimes. I mean, take the news for example. It feels like we hear about something outrageous happening Every week. Now, more than ever, it's more important to have a security system for your safety and peace of mind, and SimpliSafe" @@ -275,7 +275,7 @@ "community_id": "44026a0f-9d7a-4f39-815e-5b12575b8107", "version": 1, "data": { - "scope": "podcast", + "scope": "global", "text_template": "brought to you by Capital One. Capital One's tech team isn't just talking about multi-agentic AI. They already deployed one. It's called Chat Concierge and it's simplifying car shopping. Using self-reflection and layered reasoning with live API checks, it doesn't just help buyers find a car they love. It helps schedule a test drive, get pre-approved for financing, and estimate trade-in value. Advanced, intuitive, and deployed. That's how they stack. That's technology at Capital One.", "intro_variants": [ "brought to you by Capital One. Capital One's tech team isn't just talking about multi-agentic AI. They already deployed one. It's called Chat Concierge and it's simplifying car shopping. Using self-reflection and layered reasoning with live API checks, it doesn't just help buyers find a car they love. It helps" @@ -301,7 +301,7 @@ "community_id": "9e83a5f6-097b-4378-8077-b9ccec49b389", "version": 1, "data": { - "scope": "podcast", + "scope": "global", "text_template": "This is a Monday.com ad. The same Monday.com helping people worldwide getting work done faster and better. The same Monday.com designed for every team and every industry. The same Monday.com with built-in AI, scaling your work from day one. The same Monday.com that your team will actually love using. The same Monday.com with an easy and intuitive setup. Go to Monday.com and try it for free. Yes. TheSameMonday.com", "intro_variants": [ "This is a Monday.com ad. The same Monday.com helping people worldwide getting work done faster and better. The same Monday.com designed for every team and every industry. The same Monday.com with built-in AI, scaling your work from day one. The same Monday.com that your team will actually love using. The" diff --git a/patterns/community/instacart-b77d02bc.json b/patterns/community/instacart-b77d02bc.json index a71cc729..920553fc 100644 --- a/patterns/community/instacart-b77d02bc.json +++ b/patterns/community/instacart-b77d02bc.json @@ -1,5 +1,5 @@ { - "scope": "podcast", + "scope": "global", "text_template": "We all prefer things a certain way, like groceries. If you want groceries just how you like them, you gotta try Instacart. They have a new preference picker that lets you pick how ripe or unripe you want your bananas. Shoppers can see your preferences up front, helping guide their choices. Because when it comes to groceries, the details matter. Instacart. Get groceries just how you like.", "intro_variants": [ "We all prefer things a certain way, like groceries. If you want groceries just how you like them, you gotta try Instacart. They have a new preference picker that lets you pick how ripe or unripe you want your bananas. Shoppers can see your preferences up front, helping guide their" diff --git a/patterns/community/kayak-3f2f65ff.json b/patterns/community/kayak-3f2f65ff.json index 1683c572..96cbdf9a 100644 --- a/patterns/community/kayak-3f2f65ff.json +++ b/patterns/community/kayak-3f2f65ff.json @@ -1,5 +1,5 @@ { - "scope": "podcast", + "scope": "global", "text_template": "Kayak gets my flight, hotel, and rental car right, so I can tune out travel advice that's just plain wrong. Bro, Skycoin. way better than points never fly during a Scorpio full moon just tell the manager you'll sue instant room upgrade stop taking bad travel advice start comparing hundreds of sites with Kayak and get your trip right Kayak got that right", "intro_variants": [ "Kayak gets my flight, hotel, and rental car right, so I can tune out travel advice that's just plain wrong." diff --git a/patterns/community/mint-mobile-17b2b4b8.json b/patterns/community/mint-mobile-17b2b4b8.json index e0666f7d..2071534f 100644 --- a/patterns/community/mint-mobile-17b2b4b8.json +++ b/patterns/community/mint-mobile-17b2b4b8.json @@ -1,5 +1,5 @@ { - "scope": "podcast", + "scope": "global", "text_template": "Ryan Reynolds here from Mint Mobile. I don't know if you knew this, but anyone can get the same premium wireless for $15 a month plan that I've been enjoying. It's not just for celebrities. So do like I did and have one of your assistant's assistants switch you to Mint Mobile today. I'm told it's super easy to do at mintmobile.com slash switch. Upfront payment of $45 for three-month plan equivalent to $15 per month required. Intro rate first three months only, then full price plan options available. Taxes and fees extra. See full terms at mintmobile.com.", "intro_variants": [ "Ryan Reynolds here from Mint Mobile. I don't know if you knew this, but anyone can get the same premium wireless for $15 a month plan that I've been enjoying. It's not just for celebrities. So do like I did and have one of your assistant's assistants switch you to" diff --git a/patterns/community/monday-com-9e83a5f6.json b/patterns/community/monday-com-9e83a5f6.json index 620e47d3..ccab98c0 100644 --- a/patterns/community/monday-com-9e83a5f6.json +++ b/patterns/community/monday-com-9e83a5f6.json @@ -1,5 +1,5 @@ { - "scope": "podcast", + "scope": "global", "text_template": "This is a Monday.com ad. The same Monday.com helping people worldwide getting work done faster and better. The same Monday.com designed for every team and every industry. The same Monday.com with built-in AI, scaling your work from day one. The same Monday.com that your team will actually love using. The same Monday.com with an easy and intuitive setup. Go to Monday.com and try it for free. Yes. TheSameMonday.com", "intro_variants": [ "This is a Monday.com ad. The same Monday.com helping people worldwide getting work done faster and better. The same Monday.com designed for every team and every industry. The same Monday.com with built-in AI, scaling your work from day one. The same Monday.com that your team will actually love using. The" diff --git a/patterns/community/progressive-1c07273d.json b/patterns/community/progressive-1c07273d.json index 1da526a0..aa9ddeab 100644 --- a/patterns/community/progressive-1c07273d.json +++ b/patterns/community/progressive-1c07273d.json @@ -1,5 +1,5 @@ { - "scope": "podcast", + "scope": "global", "text_template": "\brought to you by Progressive Insurance. Do you ever find yourself playing the budgeting game? Well, with the Name Your Price tool from Progressive, you can find options that fit your budget and potentially lower your bills. Try it at Progressive.com. Progressive casualty insurance company and affiliates. Price and coverage match limited by state law. Not available in all states.", "intro_variants": [ "brought to you by Progressive Insurance. Do you ever find yourself playing the budgeting game?" diff --git a/patterns/community/simplisafe-c114bd7f.json b/patterns/community/simplisafe-c114bd7f.json index 23ce1a12..a6ce5872 100644 --- a/patterns/community/simplisafe-c114bd7f.json +++ b/patterns/community/simplisafe-c114bd7f.json @@ -1,5 +1,5 @@ { - "scope": "podcast", + "scope": "global", "text_template": "brought to you by SimpliSafe. The world can be scary sometimes. I mean, take the news for example. It feels like we hear about something outrageous happening Every week. Now, more than ever, it's more important to have a security system for your safety and peace of mind, and SimpliSafe is one of the best options out there, partly because of how proactive it is. It can help stop and prevent crime in real time. AI-powered cameras can detect suspicious activity and alert security agents who can immediately take action. They can speak to intruders, warn them away, flashlights and sirens and and Dispatch Police. With how great a job it does, no long-term contracts and no hidden fees, it's easy to see why SimpliSafe continues to be named best home security systems by U.S. News and World Report. Try it out right now. My listeners can get 50% off a SimpliSafe home security system at simplisafe.com slash . That's 50% off at simplisafe.com slash . There's no safe.", "intro_variants": [ "brought to you by SimpliSafe. The world can be scary sometimes. I mean, take the news for example. It feels like we hear about something outrageous happening Every week. Now, more than ever, it's more important to have a security system for your safety and peace of mind, and SimpliSafe" diff --git a/patterns/community/squarespace-b052ed12.json b/patterns/community/squarespace-b052ed12.json index 5aefb8ee..7f8dd68a 100644 --- a/patterns/community/squarespace-b052ed12.json +++ b/patterns/community/squarespace-b052ed12.json @@ -1,5 +1,5 @@ { - "scope": "podcast", + "scope": "global", "text_template": "brought to you by Squarespace. Squarespace makes building and managing your website ridiculously easy. They give you everything you need to showcase what you do and get paid all in one place. And with cutting-edge design tools, anyone can create a custom site that truly fits their brand. Head to squarespace.com slash rogan for a free trial. And when you're ready... Ready to launch? Use the offer code ROGAN to save 10% off your first purchase of a website or domain.", "intro_variants": [ "brought to you by Squarespace. Squarespace makes building and managing your website ridiculously easy. They give you everything you need to showcase what you do and get paid all in one place. And with cutting-edge design tools, anyone can create a custom site that truly fits their brand. Head to" diff --git a/patterns/community/zyn-3c348177.json b/patterns/community/zyn-3c348177.json index 7748c40f..83d1a7c8 100644 --- a/patterns/community/zyn-3c348177.json +++ b/patterns/community/zyn-3c348177.json @@ -1,5 +1,5 @@ { - "scope": "podcast", + "scope": "global", "text_template": "What are you reaching for? If you're a smoker or vaper, you could be reaching for so much more with Zyn Nicotine Pouches. When you reach for Zyn, you're reaching for 10 satisfying varieties and two strengths, for a smoke-free experience that lets you lean in, for chances to break free from your routine, and a unique nationwide community. Whatever you're reaching for, reach for it with America's number one nicotine pouch brand. Find your Zyn wherever nicotine products are sold near you. This product contains nicotine. Nicotine is an addictive chemical. ", "intro_variants": [], "outro_variants": [], diff --git a/src/community_export.py b/src/community_export.py index 17a775af..d4d84f37 100644 --- a/src/community_export.py +++ b/src/community_export.py @@ -283,8 +283,15 @@ def _strip_metadata(pattern: Dict, sponsor_row: Dict) -> Dict: intro_variants = [strip_pii(v) for v in intro_variants] outro_variants = [strip_pii(v) for v in outro_variants] + # Community patterns travel without a podcast_id / network_id, so a + # 'podcast' or 'network' scope on the source instance no longer makes + # sense on the receiver -- the row would never match anything. + # Eligibility on the receiver is governed by sponsor tags vs the + # podcast's tag set (text_pattern_matcher._filter_patterns_by_scope), + # not by the legacy scope column. Force global at the boundary. + return { - 'scope': pattern.get('scope') or 'global', + 'scope': 'global', 'text_template': text_template, 'intro_variants': intro_variants, 'outro_variants': outro_variants, diff --git a/src/database/schema.py b/src/database/schema.py index 31a9aea8..ea254887 100644 --- a/src/database/schema.py +++ b/src/database/schema.py @@ -1325,6 +1325,37 @@ def _run_schema_migrations(self): except Exception as e: logger.error(f"Variant re-encode repair failed: {e}") + # Pre-2.4.7 community imports preserved the source pattern's scope + # (usually 'podcast') without a podcast_id, so they never matched. + # Re-stamp every source=community row to scope='global'. + try: + self._normalize_community_scope(conn) + except Exception as e: + logger.error(f"Community scope normalize failed: {e}") + + def _normalize_community_scope(self, conn): + """Set scope='global' on every source=community pattern; clear + podcast_id / network_id since they were stripped on export. Stamped + via `community_scope_revision` so this runs once per database.""" + row = conn.execute( + "SELECT value FROM settings WHERE key = 'community_scope_revision'" + ).fetchone() + if row and row['value'] == '1': + return + cursor = conn.execute( + "UPDATE ad_patterns SET scope = 'global', podcast_id = NULL, " + "network_id = NULL WHERE source = 'community' AND " + "(scope != 'global' OR podcast_id IS NOT NULL OR network_id IS NOT NULL)" + ) + repaired = cursor.rowcount + conn.execute( + "INSERT OR REPLACE INTO settings (key, value, updated_at) " + "VALUES ('community_scope_revision', '1', strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))" + ) + conn.commit() + if repaired: + logger.info(f"Normalized scope=global on {repaired} community pattern rows") + def _repair_double_encoded_variants(self, conn): """Re-encode any ad_patterns.intro_variants / outro_variants column whose stored value parses (via json.loads) to a string rather than diff --git a/src/pattern_service.py b/src/pattern_service.py index ee481cbb..d2af8c29 100644 --- a/src/pattern_service.py +++ b/src/pattern_service.py @@ -984,13 +984,18 @@ def import_community_pattern(self, data: Dict) -> int: ) return existing['id'] + # Force scope=global. Older bundles (and the 2.4.0 seed files + # shipped pre-2.4.7) carried scope='podcast' verbatim from the + # source instance, which would make the row un-matchable without + # the (stripped) podcast_id. Tag eligibility handles per-podcast + # filtering for community patterns. pattern_id = self.db.create_ad_pattern( - scope=data['scope'], + scope='global', text_template=data['text_template'], sponsor_id=sponsor_id, - podcast_id=data.get('podcast_id'), - network_id=data.get('network_id'), - dai_platform=data.get('dai_platform'), + podcast_id=None, + network_id=None, + dai_platform=None, intro_variants=data.get('intro_variants') or [], outro_variants=data.get('outro_variants') or [], created_by='community', diff --git a/tests/unit/test_community_export.py b/tests/unit/test_community_export.py index a9534bc0..e96fc495 100644 --- a/tests/unit/test_community_export.py +++ b/tests/unit/test_community_export.py @@ -193,6 +193,27 @@ def test_build_export_payload_repairs_double_encoded_variants(): assert payload['outro_variants'] == outros +def test_export_forces_scope_global_even_when_source_is_podcast(): + """Source instances mostly run patterns at scope='podcast'. The bundle + must normalize to 'global' because podcast_id is stripped on export + -- a podcast-scoped row without a podcast_id never matches anything. + Also confirm podcast_id / network_id never leak into the payload, so + a future regression in _strip_metadata can't reintroduce them.""" + pattern = _pattern(scope='podcast', podcast_id='cordkillers-only-audio') + payload = build_export_payload(pattern, [_sponsor()]) + assert payload['scope'] == 'global' + assert 'podcast_id' not in payload + assert 'network_id' not in payload + + +def test_export_forces_scope_global_for_network_scope_too(): + pattern = _pattern(scope='network', network_id='some-network') + payload = build_export_payload(pattern, [_sponsor()]) + assert payload['scope'] == 'global' + assert 'podcast_id' not in payload + assert 'network_id' not in payload + + def test_build_export_payload_passes_single_encoded_through(): """Correctly-encoded patterns must still produce list[str], idempotent.""" intros = ['Intro variant one for Squarespace ad.'] diff --git a/tests/unit/test_pattern_service_rewrite.py b/tests/unit/test_pattern_service_rewrite.py index bf4782e6..b92cea20 100644 --- a/tests/unit/test_pattern_service_rewrite.py +++ b/tests/unit/test_pattern_service_rewrite.py @@ -152,6 +152,25 @@ def test_import_community_pattern_insert_then_update(db): assert 'updated community text' in row['text_template'] +def test_import_community_pattern_forces_scope_global(db): + """A bundle that arrives with scope='podcast' (pre-2.4.7 export) must + still land as scope='global' with podcast_id / network_id cleared.""" + svc = PatternService(db) + pid = svc.import_community_pattern({ + 'community_id': 'legacy-podcast-scope', + 'version': 1, + 'scope': 'podcast', + 'sponsor': 'Squarespace', + 'text_template': 'community pattern text template body for squarespace promo SHOW save ten', + 'podcast_id': 'should-be-dropped', + 'network_id': 'should-also-be-dropped', + }) + row = db.get_ad_pattern_by_id(pid) + assert row['scope'] == 'global' + assert row['podcast_id'] is None + assert row['network_id'] is None + + def test_import_community_pattern_respects_protected(db): svc = PatternService(db) cid = 'protected-c-001' diff --git a/version.py b/version.py index c9e914fc..999d130b 100644 --- a/version.py +++ b/version.py @@ -1 +1 @@ -__version__ = "2.4.6" +__version__ = "2.4.7" From cad689c7de59b748599194dd04c5820d3b175c36 Mon Sep 17 00:00:00 2001 From: ttlequals0 Date: Fri, 15 May 2026 15:39:11 -0400 Subject: [PATCH 2/3] fix(labeler): update labeler.yml to actions/labeler@v5 schema The workflow pins actions/labeler@v5 but the config still used v4 syntax ('label: [glob]'). v5 rejects that with 'found unexpected type for label X (should be array of config options)'. The labeler job has been failing silently on every PR since 2.4.0; the 'pattern' label never got auto-applied to any of the community-pattern PRs. Switch to v5 syntax. Same intent: PRs touching patterns/community/**/*.json get the 'pattern' label. --- .github/labeler.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 1e3bbea2..83f7b37f 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,2 +1,4 @@ pattern: - - 'patterns/community/**/*.json' + - changed-files: + - any-glob-to-any-file: + - 'patterns/community/**/*.json' From 30fe249816601b4a9fd430efc29912495fce8189 Mon Sep 17 00:00:00 2001 From: ttlequals0 Date: Fri, 15 May 2026 16:06:38 -0400 Subject: [PATCH 3/3] ui(2.4.7): prefix detection-reason line with 'Match:' label The reason field is the detector's own rationale string -- usually something like 'Brilliant (pattern #309)', but on boundary-extension hits the reviewer's free-text observation lands there instead (e.g., 'Quo (business phone system)' on a Zyn-pattern match that swept up an adjacent Quo ad). Showing it as a bare description below the sponsor badge looks like a contradiction. Prefixing 'Match:' frames it as a note about the detection rather than a second sponsor claim. UI-only tweak. No data shape change. --- CHANGELOG.md | 4 ++++ frontend/src/pages/EpisodeDetail.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47db12b0..80576a8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [2.4.7] - 2026-05-15 +### Changed + +- **Detection-note line on the episode page is now prefixed with "Match:"** so it reads as the matcher's rationale instead of looking like a contradicting sponsor when the field carries free-text reviewer notes (e.g. boundary extension that sweeps adjacent ads into one detection, where `reason` ends up describing the tail brand while `sponsor` is the matched pattern's brand). + ### Fixed - **Community patterns now import with `scope='global'` instead of preserving the source instance's scope.** Pre-2.4.7 the export pipeline copied the source pattern's `scope` (almost always `'podcast'`) into the bundle, and the import pipeline used it verbatim. Since `podcast_id` is stripped on export, the imported row was scope='podcast' with `podcast_id=NULL` and never matched anything. Tag eligibility (`text_pattern_matcher._filter_patterns_by_scope`) is what actually gates community patterns per podcast; the legacy scope column should just be `global`. diff --git a/frontend/src/pages/EpisodeDetail.tsx b/frontend/src/pages/EpisodeDetail.tsx index e67abda9..c798d859 100644 --- a/frontend/src/pages/EpisodeDetail.tsx +++ b/frontend/src/pages/EpisodeDetail.tsx @@ -569,9 +569,14 @@ function EpisodeDetail() { {formatConfidence(segment)} - {/* Row 2: Description - full width below badges for better mobile display */} + {/* Row 2: Detector's own note about the match. Framed as + a "Match:" label so it doesn't read as a contradicting + sponsor when the field carries reviewer-overwritten + free text (e.g., boundary extension that swept up an + adjacent ad's content). */} {segment.reason && (

+ Match:{' '}

)}