Skip to content

fix(brand-profile): bind Wikipedia/Wikidata lookups to a site-validated entity (LLMO-6580) - #339

Open
cwjwisse wants to merge 1 commit into
mainfrom
fix/llmo-6580-brand-profile-entity-binding
Open

fix(brand-profile): bind Wikipedia/Wikidata lookups to a site-validated entity (LLMO-6580)#339
cwjwisse wants to merge 1 commit into
mainfrom
fix/llmo-6580-brand-profile-entity-binding

Conversation

@cwjwisse

Copy link
Copy Markdown

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

  • Brand-name resolution is now confidence-aware: it never uses a bare 2–3 letter acronym, or a dev/www/store/support subdomain label, as the brand. A new resolver prefers the base profile's brand name, the site's own page title, and the registrable apex domain; a low-confidence result does not proceed to fuzzy lookups.
  • Every Wikipedia/Wikidata fetch is now bound to an entity validated against the site — the entity's official-website (P856) host must match the site's registrable domain (label matching is allowed only for high-confidence names). If no entity validates, the agent produces no products rather than guessing. The decoupled opensearch "<name> company" calls are removed from both the product-extraction and competitor-inference paths; article text is fetched via the validated entity's Wikipedia sitelink.
  • A content-safety backstop screens generated products against a harm denylist (crime, terrorism, weapons/military, adult, drugs, hate). When the source entity is validated as the customer's own, flagged content is surfaced rather than deleted (so a legitimate defence or pharmaceutical customer keeps its real products); for unvalidated sources the content is dropped.
  • Persist now refuses to overwrite a brand profile whose products_metadata.source is manual-curated, protecting the hand-verified catalogues already written to production under this ticket.
  • The Wikipedia-products fallback is gated behind a kill-switch environment flag 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

  • The root cause was reproduced directly against the public Wikipedia/Wikidata APIs this session: an opensearch for a d*-acronym brand ("Dnp/Dhl/Dnb company") returns the "D-Company" (Dawood Ibrahim) article, and for an e*-acronym ("Edb/Edc/Eon company") returns "E Company, 506th Infantry Regiment" — the two fabrication clusters observed in production.
  • The immediate data remediation for this ticket (ask 1) has already been applied to production and independently verified. It wrote 20 hand-curated product catalogues tagged products_metadata.source = manual-curated; the persist guard added in this PR exists to keep the pipeline from overwriting those rows.

7. Test plan

  • Local: no end-to-end pipeline run was performed this session — the change is gated OFF by default and is behaviourally inert until enabled (see section 8). The root-cause reproduction is described in section 6.
  • dev / stage: with the flag off (default), confirm the agent performs no by-name Wikipedia fallback and that an unvalidated entity yields no products. Then set BRAND_PROFILE_ENABLE_WIKI_PRODUCTS=true in Vault (dx_mysticat/{env}/task-processor) and run the agent against a short-acronym site (e.g. a d* domain): confirm it no longer fabricates an unrelated organisation's catalogue, and that a correctly-resolved brand yields its real products.
  • prod: keep the flag OFF until the ask-1 scrub and the P2 backfill are complete; confirm the persist guard leaves manual-curated rows untouched.

8. Deployment & merge order

  • Related: LLMO-6580 ask-1 production data remediation (already applied this session; not a PR).
  • Related: P2 backfill regeneration of the ~976 wikipedia_llm/hybrid sites (separate follow-up) — must run only after this ships and the flag is enabled.
  • Sequence: merge & deploy this PR (flag OFF, inert) → validate on dev/stage → enable BRAND_PROFILE_ENABLE_WIKI_PRODUCTS in Vault → run the P2 backfill.

🤖 Generated with Claude Code

…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

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cwjwisse
cwjwisse requested a review from MysticatBot July 28, 2026 20:48
@MysticatBot

Copy link
Copy Markdown

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

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.

2 participants