fix(brand-profile): bind Wikipedia/Wikidata lookups to a site-validated entity (LLMO-6580) - #339
fix(brand-profile): bind Wikipedia/Wikidata lookups to a site-validated entity (LLMO-6580)#339cwjwisse wants to merge 5 commits into
Conversation
…ed entity (LLMO-6580)
The brand-profile products extractor could attach a foreign (often harmful)
entity's product catalogue to a real customer. Root cause: when Wikidata SPARQL
returned fewer than the threshold, the code ran a decoupled opensearch for
`${brandName} company` and blindly took titles[0] with no check that the article
belonged to the resolved entity. A bare 2-3 letter acronym (or a dev/www
subdomain label) produced by the old domain-based brand-name fallback then
fuzzy-matched famous same-initials articles (d*->"D-Company", e*->"E Company").
Fix:
- New services/brand-resolver.js resolves a brand name with a confidence signal
and the site's registrable domain (hand-rolled multi-part-TLD table, no new
runtime dependency). It never emits a bare acronym or a stop-label subdomain as
a high-confidence name, and keeps a best-effort, short-timeout homepage title
fetch that fails safe to null.
- wikipedia.js gains getWikidataEntity, validateEntityAgainstSite,
findValidatedWikidataEntity, fetchWikipediaExtractByTitle and
fetchValidatedSummary. Every fetch is bound to an entity validated against the
site (strong P856 official-website host match, or a weak label match only for
non-low-confidence names). Low-confidence acronyms require P856. The decoupled
by-name opensearch is gone from the product and competitor paths; the old
findWikidataId/fetchWikipediaFullText are kept but marked @deprecated.
- product-extractor.js extractProducts takes an options object bound to the site
identity, produces no products when nothing validates, and applies a
content-safety backstop: harmful categories are hard-dropped from unvalidated
(label-only) sources but kept-and-flagged (sensitive_category) for
P856-validated entities and the customer's own sitemap.
- index.js rewires the brand-name, competitor-summary and product call sites,
adds the BRAND_PROFILE_ENABLE_WIKI_PRODUCTS kill-switch (default off), and adds
a persist() guard that never overwrites a manual-curated product catalogue.
Adds unit tests (brand-resolver, entity validation, d*/e* regression fixtures
asserting no products and no by-name opensearch, manual-curated persist guard);
lint clean, full suite green, branch coverage 96.5% (>= 95% gate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Mysticat review failed: Claude CLI crashed (exit 1): stderr= stdout={"type":"result","subtype":"error_max_budget_usd","duration_ms":5,"duration_api_ms":1456609,"is_error":true,"num_turns":1,"stop_reason":null,"session_id":"591d73c7-53df-4796-bf41-6e7cc38b91bf","total_cost_usd":11.888550450000004,"usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_i |
…580) Applies local review-kit PR review fixes to the entity-binding change: - brand-resolver: generalize splitHost to treat any <generic>.<2-char-ccTLD> (com.my, co.th, gov.in, or.kr, ...) as a two-label public suffix, so an unlisted ccTLD can no longer collapse the registrable domain to the bare suffix and produce a false-positive P856 "strong" match. - wikipedia: replace the ratio>=0.5 weak label match with bidirectional token containment so two distinct names sharing one token (Swiss Life / Swiss Re) no longer validate; guard P856 against a bare-suffix site domain; add fetch timeouts (AbortController) to all Wikidata/Wikipedia calls; stop logging a valid second label candidate as "Rejected". - product-extractor: add SPARQL/sitemap fetch timeouts; guard the SPARQL query against a malformed Wikidata id; catch the plural "escorts" in the harm denylist. - README: correct the brand-name confidence and label-match wording. - tests: cover generalized ccTLD splitHost, sibling-name and bare-suffix rejection, harm-denylist false-positive/plural handling, and the SPARQL id guard.
|
This PR will trigger a patch release when merged. |
…rofile-entity-binding
Make the brand-profile agent's Wikipedia/Wikidata grounding accept an entity only when its P856 official-website registrable domain matches the customer's registrable domain. Search still runs by resolved brand name, but a search result is never trusted until P856 validates it; if nothing validates, the pipeline produces no Wikipedia/Wikidata products. Removed behavior: - Weak label/alias matching, normalizeName, and corporate-suffix matching in wikipedia.js, plus the deprecated by-name functions (fetchWikipediaSummary, fetchWikipediaFullText, findWikidataId) and their service methods. - The content-safety denylist and harmful-content filtering in product-extractor.js (HARM_PATTERNS, hitsHarm, itemHitsHarm, applyContentSafetyGate) and the sensitive_category / safety_filtered metadata. This is an intentional product decision: after P856-only binding, sensitive products belonging to the customer's own validated entity may flow through without sensitive_category / safety_filtered metadata. - brandConfidence plumbing through extractProducts / fetchValidatedSummary and the skipped_low_confidence terminal source value; a no-match now always reports products_metadata.source = "none_no_validated_entity". Retained safeguards: - BRAND_PROFILE_ENABLE_WIKI_PRODUCTS kill switch (default off). - products_metadata.source === "manual-curated" persistence guard. - Fetch timeouts, malformed Wikidata-ID SPARQL guard, exact-enwiki-title extraction (no opensearch), and generalized ccTLD / bare-public-suffix rejection (isBareSuffix now mirrors splitHost's public-suffix logic). Tests updated to the P856-only contract; README documents the P856-only validation and drops label validation, sensitive_category, safety_filtered, and skipped_low_confidence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address local review-kit findings that are in scope for the P856-only refactor: - brand-resolver.js: correct the resolveBrandName docstring, which claimed the confidence signal is "retained for logging/observability" although nothing logs it; it is retained for callers/tests and is not consumed by the P856 validation path. - wikipedia.test.js: add coverage for two previously unexercised branches of the core entity guard - validateEntityAgainstSite accepting a match found after a non-matching P856 host (multi-host loop), and findValidatedWikidataEntity stopping the candidate scan once the first candidate validates (asserted via fetch call count). - product-extractor.test.js: drop a manufactured duplicate sub_brand from the hybrid-merge fixture that the test never asserted on, so the test no longer implies coverage of within-source sub_brand deduplication that the code does not perform. Deferred as out of scope for this refactor (tracked for follow-up before the kill switch is enabled): adopting a maintained public-suffix list to close the fail-open gap for unlisted suffixes, deduplicating the validated-entity resolution across the competitor and product paths, and flipping the extractProducts enableWikiProducts default to false. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Hey @cwjwisse,
Verdict: Approve - well-designed fix for a serious data integrity issue; ships inert behind a kill-switch.
Complexity: HIGH - large diff; new service module + complete rewrite of the Wikipedia/Wikidata integration path.
Changes: Binds all Wikipedia/Wikidata product lookups to a P856 (official-website) validated entity, replacing the decoupled by-name search that allowed unrelated entities to be attached to customer sites (9 files).
Non-blocking (5): minor issues and suggestions
- suggestion: resolve entity once at orchestrator level - both the competitor-summary path (
fetchValidatedSummary) and the product-extraction path (extractProducts) callfindValidatedWikidataEntityindependently, doubling Wikidata round-trips when the flag is ON. Resolve once inindex.jsand pass the entity down. -src/agents/brand-profile/index.js:174 - suggestion: batch entity fetch with
wbgetentities&ids=Q1|Q2|...instead of sequential one-by-one fetches infindValidatedWikidataEntity- cuts worst-case wall time from 50s to ~10s for the P2 backfill. -src/agents/brand-profile/services/wikipedia.js:153 - suggestion: add a response body size cap in
fetchSiteName-resp.text()without a limit could buffer large homepages (SPAs inlining bundles); consider reading only the first ~64KB since you only need<head>content. -src/agents/brand-profile/services/brand-resolver.js:166 - nit:
timedFetchis duplicated identically inwikipedia.jsandproduct-extractor.js(and the same pattern is inlined inbrand-resolver.js) - extract to a shared utility before the pattern drifts. -src/agents/brand-profile/services/wikipedia.js:35 - nit: PR body (Section 3) mentions a "content-safety backstop" harm denylist that is not present in this diff - clarify whether it ships in a follow-up PR or was accidentally omitted.
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 14s | Cost: $8.23 | Commit: a705acc4e4f13a8a31433a9e50642b9ad7891acd
If this code review was useful, please react with 👍. Otherwise, react with 👎.
1. Abstract
Binds the brand-profile agent's Wikipedia/Wikidata product lookups to a site-validated entity, replacing the decoupled by-name search that allowed one organisation's article to be extracted as another organisation's product catalogue.
2. Reasoning
LLMO-6580: the brand-profile agent fabricated defamatory product catalogues for real customers and government agencies — e.g. DHL, Dai Nippon Printing, and Dun & Bradstreet tagged as the "D-Company" (Dawood Ibrahim) organised-crime syndicate, and the Singapore Economic Development Board and Export Development Canada tagged with military/weapons content. Root cause: when a site had no brand name, the agent derived one from the domain and collapsed it to a 2–3 letter acronym; both the Wikidata entity search and a separate Wikipedia opensearch then fuzzy-matched a famous same-initials article, whose contents the LLM extracted as the customer's products. The failure was reproduced deterministically against the public APIs.
3. High-level overview of the changes
dev/www/store/supportsubdomain label, as a high-confidence brand. The resolver prefers the base profile's brand name, the site's own page title, and the registrable apex domain, and returns the site's registrable domain, which is the signal the entity validation compares against.opensearch "<name> company"calls are removed from both the product-extraction and competitor-inference paths; article text is fetched via the validated entity's exact Wikipedia sitelink title.sensitive_category/safety_filteredmetadata) is intentionally removed as a product decision. With P856-only binding, extracted products come from the customer's own validated entity, so this filtering is no longer the control that keeps a foreign catalogue off a customer. The accepted trade-off: sensitive products that legitimately belong to the customer's own validated entity may now flow through withoutsensitive_category/safety_filteredmetadata.products_metadata.sourceismanual-curated, protecting the hand-verified catalogues already written to production under this ticket.BRAND_PROFILE_ENABLE_WIKI_PRODUCTS) that defaults OFF, so deploying this change is behaviourally inert until the flag is deliberately enabled.4. Required information
6. Additional information outside the code
d*-acronym brand ("Dnp/Dhl/Dnb company") returns the "D-Company" (Dawood Ibrahim) article, and for ane*-acronym ("Edb/Edc/Eon company") returns "E Company, 506th Infantry Regiment" — the two fabrication clusters observed in production.products_metadata.source = manual-curated; the persist guard added in this PR exists to keep the pipeline from overwriting those rows.github.io,me.uk, non-generic ccTLD second levels) — adopt a maintained public-suffix list to close this. Additionally the validated entity is resolved twice (competitor-summary and product paths) when the flag is on; resolve once and thread it through.7. Test plan
none_no_validated_entity, exact-title extraction (no opensearch), the kill-switch, the manual-curated persist guard, and the malformed-Wikidata-ID SPARQL guard.BRAND_PROFILE_ENABLE_WIKI_PRODUCTS=truein Vault (dx_mysticat/{env}/task-processor) and run the agent against a short-acronym site (e.g. ad*domain): confirm it no longer fabricates an unrelated organisation's catalogue, and that a correctly-resolved brand yields its real products.manual-curatedrows untouched.8. Deployment & merge order
wikipedia_llm/hybridsites (separate follow-up) — must run only after this ships and the flag is enabled.BRAND_PROFILE_ENABLE_WIKI_PRODUCTSin Vault → run the P2 backfill.🤖 Generated with Claude Code