drop additional domains a broader tracked domain already covers - #682
jamiusaliu wants to merge 9 commits into
Conversation
|
@jamiusaliu is attempting to deploy a commit to the Blue Whale Labs Team on Vercel. A member of the Team first needs to authorize it. |
Adds my GitHub username to the signature registry, per CONTRIBUTING.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxTY4DGQWJn6My6bRCoPeM
Brand matching is suffix-based, so `categorizeDomain` already counts `blog.acme.io` as the brand's whenever `acme.io` is tracked. Keeping both is harmless but leaves an entry in the settings list that does nothing. normalizeBrandUpdate now filters an additional domain out when another additional domain, or the website in the same update, already covers it. It reuses inDomainSet rather than testing the suffix itself, so this answers "equals, or is a subdomain of" the same way the categorizer and the citation snapshot do, and it inherits the label-boundary walk that keeps `notacme.io` out of `acme.io`. The website is only consulted when it is part of the same update. The settings form always submits both, and this function deliberately never reads the stored brand. Fixes elmohq#571 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxTY4DGQWJn6My6bRCoPeM
1728c19 to
c660bc6
Compare
|
Rebased on main and updated for #690. That PR replaced the No change to behaviour or to the tests. |
The issue asks for the entry not to be allowed, not for it to disappear on
save. normalizeBrandUpdate now returns an error naming what already covers it
("blog.acme.com (already covered by acme.com)"), the same way it reports an
invalid domain, and the settings form runs the identical check as the chip is
added so the user finds out while typing.
The rule moves to @workspace/lib/domains alongside inDomainSet, which it was
already built on, so the onboarding pipeline can reach it too. findRedundantDomains
returns each flagged domain mapped to the one covering it, which is what lets
the messages name a domain; dropRedundantDomains is the same rule for the paths
that correct their input rather than reject it.
updateBrandFn now reads the brand's stored website and passes it in, so an
update that edits domains without resending the website is judged against the
website the brand actually has. addDomainToBrandFn does its own check against
the brand's website and existing list before appending.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The settings form was the only path that checked. Five others write additionalDomains, and they already carried a weaker version of the same rule — dropping an exact match of the website — which is what generalizes. createBrand and updateBrand cover the onboarding wizard and the admin /api/v1/brands POST and PATCH. splitDomainsForStorage is where the API's one flat list becomes a website plus the rest, so it is also where the rest stop being worth storing. analyze.ts is where these most often originate: the model routinely answers with a site's own subdomains next to the site. These correct their input rather than error. Nobody typed the model's answer, and the API's flat list carries no field to point at. The wizard's domain input gets the settings form's check so the user still sees it at entry time. Competitor filtering keeps seeing every domain the model called the brand's, including the ones dropped, so a competitor never gets through on a subdomain that was just collapsed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Redundant additional domains get discouraged where they are typed and nowhere else. Every server-side change comes back out: normalizeBrandUpdate, the brands API, onboarding, and the analyzer all store exactly what they are given again. Enforcing it server-side made a small fix expensive. Brands already hold redundant entries, and rejecting on save meant a rename could be blocked by a domain the user never touched, while backfilling them away is not something we want to do. Reporting may also want to break these out one day rather than treat them as interchangeable, and the API is the wrong place to carry a contract this implicit. What is left is redundantDomainReason, next to the inDomainSet it reads from, and the settings form and wizard calling it as the chip is added. The settings website field becomes controlled so the check sees what is typed rather than what was last saved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The model routinely answers with a site's own subdomains next to the site, so the wizard pre-filled chips the domain input would now reject. The schema description says what "already covered" means rather than leaving it implied by "do not include the primary website". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
analyze.ts has no concept of how domains are matched downstream, so explaining it there asked the model to reason about a rule it cannot see. "Omit the primary website and any subdomain of the primary website" is the same instruction without the premise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drop the two validateDomain preambles and the test asides, and cut the redundantDomainReason and story docs to the parts a reader cannot get from the code: that the return is a message fragment, that nothing enforces this on stored domains, and which combobox the story reaches for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was no story for this route, so the domain input could only be exercised through the wizard. Renders the route component directly, the way the LLM settings story does, with the brand from the use-brands mock and the organization from the router mock's loader data. Covers both branches of the message: a subdomain of the website, and the website itself. An exact repeat of a domain already in the list never reaches validation — TagsInput drops it as a duplicate first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks for taking this the rest of the way. Rejecting the domain at the input is a better read of #571 than what I sent: the issue says these "shouldn't allow" subdomains, and a silent drop at save time answered a different question. Naming the covering domain in the message is the part I would not have thought to add. One thing you may not have from your side: #724 landed on I did not run E2E, the build, or the new stories. I have also left the branch alone rather than rebase it, since you are working in there. Say the word if a rebase or anything else would help. |
Fixes #571.
Matching is suffix-based (
categorizeDomain,apps/web/src/lib/domain-categories.server.ts), soblog.acme.iois already counted as the brand's wheneveracme.iois tracked. As the issue says, this is not serious, the entry just sits in the settings list doing nothing.What changed
normalizeBrandUpdatenow filters out an additional domain when something broader already covers it:eu.blog.acme.io,blog.acme.io,acme.iocollapses toacme.io)Dropping silently rather than erroring, to match how the function already treats exact duplicates. Redundant seemed closer to duplicate than to invalid.
Two things worth a second opinion
The label boundary.
isCoveredByanchors on., sonotacme.ioandfake-acme.iosurvive next toacme.io. A bareendsWithwould silently delete a genuinely different domain, which would be a worse bug than the one being fixed. There is a test for it.The website is only consulted when it is in the same update. A partial edit that sends
additionalDomainsalone has no website to compare against, and this function deliberately never touches the database. The settings form always submits both, so the real path is covered. Happy to move the check intoupdateBrandFnwhere the stored brand is available if you would rather it hold for API callers too.One thing I did not assume: an additional domain exactly equal to the website domain is also dropped, since
categorizeDomaintreats===and the suffix case identically. Say the word if you would rather only strict subdomains were removed.Testing
Six tests added to
apps/web/src/lib/__tests__/brand-settings.test.ts, written before the change. Four failed againstmain, the two boundary guards passed from the start.apps/webis at 353 passing, up from 347, withbiome checkandtsc --noEmitclean.CLA signed in the first commit.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KxTY4DGQWJn6My6bRCoPeM
Open workspace in Conductor