Skip to content

feat: add verified coupon code suggested action - #2719

Open
alectimison-maker wants to merge 2 commits into
webbrain-one:mainfrom
alectimison-maker:feat/verified-coupon-action
Open

feat: add verified coupon code suggested action#2719
alectimison-maker wants to merge 2 commits into
webbrain-one:mainfrom
alectimison-maker:feat/verified-coupon-action

Conversation

@alectimison-maker

@alectimison-maker alectimison-maker commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a Find coupon codes suggested action for recognized commerce product, non-empty cart, and checkout pages.
  • Start with a read-only accessibility-tree preflight, then pin a bounded, merchant-verifiable execution plan in both Chrome and Firefox.
  • Generate a reviewed static snapshot of 3,767 coupon merchant domains from CouponFollow's fixed numeric/A-Z directory indexes plus the original vetted regional storefronts.
  • Add deterministic coverage for routing, false positives, generator input validation, browser parity, state preservation, and the no-order boundary.

Motivation

Issue #2707 asks for a one-click way to find active coupon codes, especially at checkout. A useful slice needs to distinguish a candidate found elsewhere from a discount the merchant actually accepts, while avoiding accidental checkout changes.

The first revision used a conservative hand-maintained domain list. Maintainer feedback correctly identified that this would not scale to the merchants represented by coupon directories, so the list is now generated by an explicit maintenance command instead.

Design

  • npm run update:coupon-domains reads CouponFollow's 27 bounded numeric/A-Z merchant indexes with concurrency, timeout, response-size, host, route, and domain validation. It fails closed if any index yields no merchants.
  • The command writes byte-identical generated modules for Chrome and Firefox. --check verifies reproducibility without modifying files.
  • The reviewed snapshot contains 3,767 unique domains. Runtime lookup uses a Set plus host-label suffix traversal, so subdomains are supported without scanning the complete list.
  • The extension deliberately does not update from third parties when the sidebar launches. This keeps startup independent of coupon-site availability and avoids sending every visited merchant host into a new runtime data path. Updates remain explicit, reviewable repository changes.
  • Match a known merchant and require a product route with a purchase signal, or a cart/checkout route with a visible value or coupon field. Lookalike domains, empty carts, help pages, marketing forms, hidden tracking fields, and gift-card redemption pages are rejected.
  • Inspect the current page first, prefer merchant-provided offers, and bound external research to five candidate codes.
  • On product pages without a coupon field, report candidates as unverified and do not add an item or start checkout just to test them.
  • At checkout, preserve any existing coupon, gift cards, store credit, and rewards. A code is called active only when the merchant accepts it and the payable total or explicit discount changes; the best saving is kept, otherwise the original state is restored.
  • The ready plan forbids ordering, payment/address/shipping changes, memberships or subscriptions, affiliate redirects, extension installation, and continuing through CAPTCHA or rate limits.
  • Planner bypass accepts only get_accessibility_tree as the immediate tool. The existing permission gate and untrusted-page-content boundary remain unchanged for later actions.

Testing

  • npm run update:coupon-domains -- --check — passed; 3,767 domains, no changes.
  • node test/run.js — 1,573 passed; the sole failure is the pre-existing changelog/package mismatch (27.1.0 vs 27.1.5).
  • npm run test:toolbar-guard — 33/33 passed.
  • npm run test:security — 60/60 passed, including Chrome/Firefox prompt-injection parity.
  • npm run test:ci — 14 scenarios validated; cloud capture test passed.
  • npm run test:fixtures — 143/145 passed. The same two selection-dialog failures reproduce independently of this source seam and were already documented against the clean base revision.
  • WEBMCP_DEBUG=1 WEBMCP_TIMEOUT_MS=60000 npm run test:webmcp — protocol and extension functional assertions pass, then teardown times out; the identical timeout was already reproduced on the clean base revision.
  • node --check on the generator and all changed runtime/data modules, plus git diff --check — passed.
  • npm run build:zip — Chrome, Edge, and Firefox archives built successfully; each contains src/ui/coupon-domains.js and src/ui/recommended-actions.js.

Compatibility and risks

  • No new manifest permissions, runtime network request, dependency, API, external coupon provider, affiliate integration, storage format, or migration.
  • Chrome and Firefox receive matching behavior and byte-identical generated domain data.
  • Coupon-directory membership is evidence that a merchant commonly has codes, not proof that any candidate works. Merchant-side verification remains mandatory.
  • The snapshot can become stale or its upstream HTML can change; the bounded updater fails closed and makes changes reviewable before release.

Scope

  • No sidebar-launch or background refresh from third-party coupon directories.
  • No coupon-provider API or affiliate business model.
  • No attempt to guarantee product-page candidates without merchant-side verification.
  • No live checkout fixture that requires a real account, cart, or payment flow.

Closes #2707

@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

@alectimison-maker is attempting to deploy a commit to the esokullu's projects Team on Vercel.

A member of the Team first needs to authorize it.

@esokullu

esokullu commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

this is good start but we need to expand it with all e-commerce sites that https://www.coupons.com/ etc. list. because these would be the sites that offer coupons most of the time.Even create a js or python script which will generate that list under scripts/ for us so it's automated. also would be nice if the list could be automatically updated at every launch of the sidebar.

According to chatgpt they are:

RetailMeNot — probably the closest direct analogue; huge promo-code database.
Slickdeals — extremely popular, but more community/deal-driven than pure coupons; users vote and comment on deals.
CouponCabin — coupons + cashback.
Groupon — especially local businesses, restaurants, activities and services, though it also has online deals.
Rakuten — primarily cashback, but also aggregates coupons and promo codes.
Honey — heavily browser-extension oriented; automatically tests coupon codes at checkout across thousands of stores.
Offers.com — traditional coupon/promo-code format.
DealNews — more curated deals than pure coupon codes.
Brad's Deals — editorially curated deals/coupons.
CouponFollow — coupon-code database and extension; also fairly prominent.
Picodi — probably one of the strongest truly international coupon platforms. It operates localized sites across many countries, including Türkiye, India, UAE, Saudi Arabia, South Africa, Malaysia, Hong Kong, etc.
Wethrift — very international, especially for online stores. It claims coverage of roughly 100,000 stores, including lots of international and smaller merchants.
Groupon — international brand, although more focused on local experiences/services plus deals than being purely a promo-code search engine.
RetailMeNot — still useful for global brands, but fundamentally more U.S.-oriented.
Honey — global-ish because it works wherever supported online merchants operate, though it's primarily an automatic coupon extension rather than a traditional coupon directory.
Coupert — similar to Honey: international coupon database + browser extension.
CouponFollow — substantial international merchant coverage, though U.S. is still prominent.
SimplyCodes — increasingly prominent for promo codes, though still U.S.-heavy.

npm install cheerio tldts p-limit

import * as cheerio from "cheerio";
import { getDomain } from "tldts";
import pLimit from "p-limit";
import fs from "node:fs/promises";
import zlib from "node:zlib";
import { promisify } from "node:util";

const gunzip = promisify(zlib.gunzip);

const CONCURRENCY = Number(process.env.CONCURRENCY || 4);
const DELAY_MS = Number(process.env.DELAY_MS || 250);

// 0 = unlimited
const MAX_PAGES = Number(process.env.MAX_PAGES || 0);

const USER_AGENT =
  "CouponDomainResearchBot/1.0 (+domain research; low-rate crawler)";

const sites = [
  {
    name: "CouponFollow",
    origin: "https://couponfollow.com",
    seeds: [
      "https://couponfollow.com/site",
      ...["0", ..."abcdefghijklmnopqrstuvwxyz"].map(
        c => `https://couponfollow.com/site/browse/${c}/all`
      )
    ],
    domainFromUrl(url) {
      const u = new URL(url);

      // /site/godaddy.com
      const m = u.pathname.match(/^\/site\/([^/?#]+)$/i);

      if (!m) return null;

      return normalizeDomain(m[1]);
    },
    interestingUrl(url) {
      return /^\/site\//i.test(new URL(url).pathname);
    }
  },

  {
    name: "SimplyCodes",
    origin: "https://simplycodes.com",
    seeds: [
      "https://simplycodes.com/",
      "https://simplycodes.com/category"
    ],
    domainFromUrl(url) {
      const u = new URL(url);

      // /store/godaddy.com
      const m = u.pathname.match(/^\/store\/([^/?#]+)$/i);

      if (!m) return null;

      return normalizeDomain(m[1]);
    },
    interestingUrl(url) {
      const p = new URL(url).pathname;

      return (
        p.startsWith("/store/") ||
        p.startsWith("/category")
      );
    }
  },

  {
    name: "Coupert",
    origin: "https://www.coupert.com",
    seeds: [
      "https://www.coupert.com/"
    ],
    domainFromUrl(url) {
      const u = new URL(url);

      // /store/boody.com
      const m = u.pathname.match(/^\/store\/([^/?#]+)$/i);

      if (!m) return null;

      return normalizeDomain(m[1]);
    },
    interestingUrl(url) {
      return new URL(url).pathname.startsWith("/store/");
    }
  },

  {
    name: "Wethrift",
    origin: "https://www.wethrift.com",
    seeds: [
      "https://www.wethrift.com/coupons"
    ],
    domainFromUrl() {
      // Wethrift uses brand slugs like:
      // /godaddy
      // so we cannot get the domain from the URL alone.
      return null;
    },
    interestingUrl(url) {
      const p = new URL(url).pathname;

      // Avoid obvious non-store sections.
      const excluded = [
        "/about",
        "/contact",
        "/submit",
        "/savvy",
        "/privacy",
        "/terms",
        "/tag/",
        "/blog",
        "/coupons"
      ];

      return !excluded.some(x => p === x || p.startsWith(x));
    },
    extractFromPage: true
  },

  {
    name: "Picodi",
    origin: "https://www.picodi.com",
    seeds: [
      "https://www.picodi.com/us/",
      "https://www.picodi.com/tr/",
      "https://www.picodi.com/uk/",
      "https://www.picodi.com/de/",
      "https://www.picodi.com/fr/",
      "https://www.picodi.com/es/",
      "https://www.picodi.com/it/",
      "https://www.picodi.com/in/",
      "https://www.picodi.com/ae/",
      "https://www.picodi.com/sa/",
      "https://www.picodi.com/sg/",
      "https://www.picodi.com/my/",
      "https://www.picodi.com/ph/",
      "https://www.picodi.com/hk/",
      "https://www.picodi.com/pk/"
    ],
    domainFromUrl() {
      // Picodi URLs are /us/godaddy, not /us/godaddy.com.
      return null;
    },
    interestingUrl(url) {
      const p = new URL(url).pathname;

      // Roughly /country/store-name
      return /^\/[a-z]{2}(?:-[a-z]{2})?\/[^/]+\/?$/i.test(p);
    },
    extractFromPage: true
  }
];


/* ------------------------------------------------------- */
/* Utilities                                               */
/* ------------------------------------------------------- */

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function normalizeDomain(value) {
  if (!value) return null;

  value = decodeURIComponent(value)
    .trim()
    .toLowerCase()
    .replace(/^https?:\/\//, "")
    .replace(/^www\./, "")
    .split("/")[0]
    .split("?")[0]
    .split("#")[0]
    .replace(/[.,;:]+$/, "");

  const domain = getDomain(value);

  return domain || null;
}

function sameSite(url, origin) {
  try {
    const a = normalizeDomain(new URL(url).hostname);
    const b = normalizeDomain(new URL(origin).hostname);

    return a === b;
  } catch {
    return false;
  }
}

function absoluteUrl(href, base) {
  try {
    if (!href) return null;

    if (
      href.startsWith("javascript:") ||
      href.startsWith("mailto:") ||
      href.startsWith("tel:")
    ) {
      return null;
    }

    return new URL(href, base).href;
  } catch {
    return null;
  }
}


/* ------------------------------------------------------- */
/* Networking                                              */
/* ------------------------------------------------------- */

async function fetchBuffer(url, options = {}) {
  await sleep(DELAY_MS);

  const res = await fetch(url, {
    redirect: options.redirect || "follow",
    headers: {
      "User-Agent": USER_AGENT,
      "Accept": "*/*"
    }
  });

  if (!res.ok) {
    throw new Error(`${res.status} ${res.statusText}`);
  }

  const buffer = Buffer.from(await res.arrayBuffer());

  if (
    url.endsWith(".gz") ||
    res.headers.get("content-type")?.includes("gzip")
  ) {
    try {
      return await gunzip(buffer);
    } catch {
      return buffer;
    }
  }

  return buffer;
}

async function fetchText(url) {
  return (await fetchBuffer(url)).toString("utf8");
}


/* ------------------------------------------------------- */
/* Sitemap discovery                                       */
/* ------------------------------------------------------- */

function extractLocs(xml) {
  return [...xml.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/gi)]
    .map(m =>
      m[1]
        .replaceAll("&amp;", "&")
        .trim()
    );
}

async function discoverSitemaps(origin) {
  const candidates = new Set([
    `${origin}/sitemap.xml`,
    `${origin}/sitemap_index.xml`,
    `${origin}/sitemap-index.xml`
  ]);

  try {
    const robots = await fetchText(`${origin}/robots.txt`);

    for (const match of robots.matchAll(/^sitemap:\s*(.+)$/gim)) {
      candidates.add(match[1].trim());
    }
  } catch {
    // robots.txt unavailable; continue with standard sitemap names.
  }

  return [...candidates];
}

async function crawlSitemap(url, seen = new Set()) {
  if (seen.has(url)) return [];

  seen.add(url);

  let xml;

  try {
    xml = await fetchText(url);
  } catch {
    return [];
  }

  const locs = extractLocs(xml);

  const sitemapUrls = locs.filter(x =>
    /sitemap.*(?:\.xml|\.xml\.gz|\.gz)(?:$|\?)/i.test(x)
  );

  const pageUrls = locs.filter(x => !sitemapUrls.includes(x));

  for (const sitemap of sitemapUrls) {
    const children = await crawlSitemap(sitemap, seen);

    pageUrls.push(...children);
  }

  return pageUrls;
}


/* ------------------------------------------------------- */
/* Merchant-domain extraction                              */
/* ------------------------------------------------------- */

const IGNORE_DOMAINS = new Set([
  "google.com",
  "googleapis.com",
  "googleusercontent.com",
  "gstatic.com",
  "facebook.com",
  "instagram.com",
  "twitter.com",
  "x.com",
  "youtube.com",
  "tiktok.com",
  "linkedin.com",
  "pinterest.com",
  "apple.com",
  "microsoft.com",
  "cloudflare.com",
  "cloudfront.net",
  "amazonaws.com",
  "doubleclick.net",
  "googletagmanager.com",
  "google-analytics.com"
]);

function shouldIgnoreDomain(domain, siteDomain) {
  if (!domain) return true;

  if (domain === siteDomain) return true;

  if (IGNORE_DOMAINS.has(domain)) return true;

  return false;
}

function extractDomainsFromText(text) {
  const found = new Set();

  // Good-enough domain matcher.
  const regex =
    /\b(?:[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?\.)+(?:com|net|org|io|co|ai|app|dev|store|shop|me|us|uk|de|fr|es|it|nl|se|no|fi|dk|pl|cz|tr|in|ae|sa|sg|my|ph|hk|pk|au|ca|nz|jp|kr|br|mx|za)\b/gi;

  for (const m of text.matchAll(regex)) {
    const domain = normalizeDomain(m[0]);

    if (domain) found.add(domain);
  }

  return found;
}

function extractPageDomains(html, pageUrl, site) {
  const $ = cheerio.load(html);

  const siteDomain = normalizeDomain(new URL(site.origin).hostname);
  const found = new Set();

  // 1. Absolute outbound links.
  $("a[href]").each((_, element) => {
    const href = absoluteUrl($(element).attr("href"), pageUrl);

    if (!href) return;

    try {
      const host = new URL(href).hostname;
      const domain = normalizeDomain(host);

      if (!shouldIgnoreDomain(domain, siteDomain)) {
        found.add(domain);
      }
    } catch {}
  });

  // 2. Visible/plain-text domains.
  const textDomains = extractDomainsFromText($.text());

  for (const domain of textDomains) {
    if (!shouldIgnoreDomain(domain, siteDomain)) {
      found.add(domain);
    }
  }

  // 3. Embedded JSON / source can contain merchant URLs
  // even when not rendered visibly.
  const htmlDomains = extractDomainsFromText(html);

  for (const domain of htmlDomains) {
    if (!shouldIgnoreDomain(domain, siteDomain)) {
      found.add(domain);
    }
  }

  return found;
}


/* ------------------------------------------------------- */
/* Internal-link crawler fallback                          */
/* ------------------------------------------------------- */

async function crawlInternal(site, initialUrls) {
  const queue = [...initialUrls];
  const visited = new Set();
  const discovered = new Set(initialUrls);

  while (queue.length) {
    if (MAX_PAGES && visited.size >= MAX_PAGES) break;

    const url = queue.shift();

    if (visited.has(url)) continue;
    visited.add(url);

    let html;

    try {
      html = await fetchText(url);
    } catch (err) {
      console.error(`[${site.name}] fetch failed: ${url}`);
      continue;
    }

    const $ = cheerio.load(html);

    $("a[href]").each((_, element) => {
      const href = absoluteUrl($(element).attr("href"), url);

      if (!href) return;
      if (!sameSite(href, site.origin)) return;

      const clean = href.split("#")[0];

      if (!discovered.has(clean)) {
        discovered.add(clean);

        if (
          site.interestingUrl(clean) ||
          clean === site.origin ||
          clean.startsWith(site.origin)
        ) {
          queue.push(clean);
        }
      }
    });

    if (visited.size % 100 === 0) {
      console.log(
        `[${site.name}] visited=${visited.size} discovered=${discovered.size}`
      );
    }
  }

  return [...discovered];
}


/* ------------------------------------------------------- */
/* Site processing                                         */
/* ------------------------------------------------------- */

async function processSite(site) {
  console.log(`\n=== ${site.name} ===`);

  const candidateUrls = new Set(site.seeds);

  /*
   * First try sitemaps. This is dramatically cheaper than
   * fetching every merchant page.
   */
  const sitemapUrls = await discoverSitemaps(site.origin);

  for (const sitemap of sitemapUrls) {
    console.log(`[${site.name}] checking sitemap ${sitemap}`);

    const urls = await crawlSitemap(sitemap);

    for (const url of urls) {
      if (sameSite(url, site.origin) && site.interestingUrl(url)) {
        candidateUrls.add(url);
      }
    }
  }

  /*
   * If sitemap discovery didn't reveal much, crawl links.
   */
  if (candidateUrls.size <= site.seeds.length + 10) {
    console.log(
      `[${site.name}] sitemap yielded little; using internal crawl`
    );

    const crawled = await crawlInternal(site, site.seeds);

    for (const url of crawled) {
      if (site.interestingUrl(url)) {
        candidateUrls.add(url);
      }
    }
  }

  console.log(
    `[${site.name}] ${candidateUrls.size.toLocaleString()} candidate URLs`
  );

  const results = new Map();

  /*
   * First: domains directly encoded in merchant URLs.
   */
  for (const url of candidateUrls) {
    const domain = site.domainFromUrl(url);

    if (domain) {
      if (!results.has(domain)) {
        results.set(domain, []);
      }

      results.get(domain).push(url);
    }
  }

  /*
   * Wethrift/Picodi-like sites need actual page inspection.
   */
  if (site.extractFromPage) {
    const limit = pLimit(CONCURRENCY);

    let processed = 0;

    const urls = [...candidateUrls].filter(site.interestingUrl);

    await Promise.all(
      urls.map(url =>
        limit(async () => {
          if (MAX_PAGES && processed >= MAX_PAGES) return;

          processed++;

          try {
            const html = await fetchText(url);
            const domains = extractPageDomains(html, url, site);

            for (const domain of domains) {
              if (!results.has(domain)) {
                results.set(domain, []);
              }

              results.get(domain).push(url);
            }
          } catch (err) {
            console.error(
              `[${site.name}] error ${url}: ${err.message}`
            );
          }

          if (processed % 100 === 0) {
            console.log(
              `[${site.name}] inspected ${processed}/${urls.length}`
            );
          }
        })
      )
    );
  }

  return results;
}


/* ------------------------------------------------------- */
/* Main                                                    */
/* ------------------------------------------------------- */

const combined = new Map();

for (const site of sites) {
  const results = await processSite(site);

  console.log(
    `[${site.name}] found ${results.size.toLocaleString()} candidate merchant domains`
  );

  for (const [domain, pages] of results) {
    if (!combined.has(domain)) {
      combined.set(domain, {
        domain,
        sources: new Set(),
        pages: new Set()
      });
    }

    const row = combined.get(domain);

    row.sources.add(site.name);

    for (const page of pages) {
      row.pages.add(page);
    }
  }
}


/* ------------------------------------------------------- */
/* Export                                                  */
/* ------------------------------------------------------- */

const output = [...combined.values()]
  .map(row => ({
    domain: row.domain,
    sources: [...row.sources].sort(),
    pages: [...row.pages].slice(0, 10)
  }))
  .sort((a, b) => a.domain.localeCompare(b.domain));

await fs.writeFile(
  "coupon-domains.json",
  JSON.stringify(output, null, 2)
);

await fs.writeFile(
  "coupon-domains.txt",
  output.map(x => x.domain).join("\n") + "\n"
);

await fs.writeFile(
  "coupon-domains.csv",
  [
    "domain,sources",
    ...output.map(x =>
      `"${x.domain}","${x.sources.join("|")}"`
    )
  ].join("\n")
);


/* ------------------------------------------------------- */
/* Interesting examples                                    */
/* ------------------------------------------------------- */

console.log("\n================================");
console.log(`TOTAL DOMAINS: ${output.length.toLocaleString()}`);
console.log("================================\n");

const examples = [
  "godaddy.com",
  "namecheap.com",
  "hostinger.com",
  "shopify.com",
  "wix.com"
];

for (const domain of examples) {
  const hit = combined.get(domain);

  if (hit) {
    console.log(
      `✓ ${domain}: ${[...hit.sources].join(", ")}`
    );
  } else {
    console.log(`✗ ${domain}`);
  }
}

console.log("\nWritten:");
console.log("  coupon-domains.txt");
console.log("  coupon-domains.csv");
console.log("  coupon-domains.json");

Source: https://chatgpt.com/share/6a787742-3c20-83eb-b913-e825cc0ef9de

@alectimison-maker

Copy link
Copy Markdown
Contributor Author

Thanks — I agree that the original hand-maintained list was too narrow, and I implemented the directory-generated approach in 9aa0abdc.

The new npm run update:coupon-domains command reads CouponFollow's fixed 27 numeric/A-Z merchant indexes and produces byte-identical reviewed snapshots for Chrome and Firefox. The current snapshot expands coverage from the initial list to 3,767 unique merchant domains. The updater has bounded concurrency, timeouts and response sizes, validates the exact source host/route and domain syntax, fails if an index unexpectedly becomes empty, and supports --check. It adds no dependencies.

I deliberately did not fetch or replace the list on every sidebar launch. That would make sidebar startup depend on third-party availability and create a new runtime data/privacy path tied to the user's current merchant. Shipping the reviewed snapshot keeps the extension deterministic and offline; maintainers can refresh it explicitly before a release (and a future scheduled bot PR could automate that reviewable step).

I also did not copy the proposed open-ended multi-site crawler as-is: several proposed sources currently return Cloudflare challenges to automated clients, and the recursive fallback has no default page bound. The fixed CouponFollow all-stores indexes provide a reproducible first source without those failure modes, while the generator remains small enough to extend with another stable public index later.

Validation after the change: 1,573 core tests passed (one pre-existing changelog/version mismatch), toolbar 33/33, security 60/60, CI 14 scenarios plus cloud capture, deterministic 3,767-domain regeneration, Chrome/Firefox parity, and successful Chrome/Edge/Firefox package builds. The two known selection-dialog fixture failures and WebMCP teardown timeout remain unchanged and are documented in the updated PR body.

@alectimison-maker

Copy link
Copy Markdown
Contributor Author

I did a deeper pass across Honey, Capital One Shopping, Edge, SimplyCodes, and the open-source Caramel implementation. I agree with broadening coverage, and I think the safest next step is a small follow-up PR focused on data quality and trigger precision rather than adding more runtime behavior to this PR.

One important distinction is that directory membership does not mean a merchant currently has working codes. For example, the current SimplyCodes store directory lists 645,414 stores, but 51,521 with verified codes right now (about 8%). I therefore propose treating directory membership as a useful prior, not as a sufficient trigger by itself.

Proposed scope for the follow-up:

  • replace the generator hand-maintained public-suffix exclusions with PSL-aware normalization, using tldts only during generation or a pinned PSL snapshot;
  • generate a reviewable provenance manifest with source URL, first/last seen timestamps, evidence tier, and per-source counts;
  • separate explicit network refresh from a deterministic, offline consistency check;
  • add anomaly gates for empty sources, unusually large churn, and public or multi-tenant suffixes;
  • tier the runtime trigger: a visible promo field plus a non-empty cart may trigger even for an unknown merchant; recent active-code evidence may trigger in a commerce context; directory-only membership cannot trigger alone;
  • keep the shipped snapshot static and reviewed. A scheduled workflow may open a data-refresh PR, but sidebar startup would make no third-party request.

This should improve long-tail recall without turning every historical directory listing into a potentially noisy recommendation, while retaining the deterministic and offline behavior of the current implementation. I would keep transactional coupon application/restoration and stateful synthetic checkout fixtures as separate later contributions once we agree on the trigger and data model.

Would you be comfortable with this scope as the next PR after #2719, or would you prefer PSL/provenance and tiered triggering to be split into two smaller PRs? I will wait for your feedback before starting it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Suggested Action: coupon code

2 participants