Skip to content

fix(brand-profile): hold entity-validation failures as unverified, not fail-open (#3268, re-opens #361) - #366

Open
mikkeline-elleby wants to merge 6 commits into
mainfrom
fix/3268-entity-validation-failopen
Open

mikkeline-elleby wants to merge 6 commits into
mainfrom
fix/3268-entity-validation-failopen

Conversation

@mikkeline-elleby

Copy link
Copy Markdown

Re-opens the Class B wrong-QID entity-validation gate from #361 (closed without a stated reason) and adds the fail-open posture change required by adobe-rnd/llmo-data-retrieval-service#3268.

Background

The brand-profile agent can resolve a brand to the wrong same-named Wikidata entity and inject that other company's products (Behr paint → Iberian ham; Capella University → the star). #359 fixed Class A (right QID, wrong article); this closes Class B (the QID itself is wrong), which #359's article-guard can't catch.

What changed

validateEntityMatchesBrand now returns a three-state outcome instead of a boolean match:

status when effect
verified P856 official-website registrable-domain match publishes (only authoritative pass)
mismatch definitive LLM "no" drop the QID → entity-brand-mismatch (as #361)
unverified no domain match + (LLM "yes" | no verifier | infra error) non-publishable, held for review
  • Fail-open → held. Infra errors (Wikidata/LLM) previously returned match: true (silently trusted); they now return unverified with failOpen: true, so a transient blip during a 1,000+ job regen can't silently re-poison sites.
  • Alarmable fail-open metric. Emitted as structured log lines (entity_validation_outcome per call + entity_validation_fail_open warn) — matching this repo's log-based observability (no metrics SDK). A CloudWatch filter can chart fail-open / total.
  • LLM flag-gate. BRAND_PROFILE_ENTITY_VALIDATION_LLM=false disables the LLM leg (non-matches then just hold as unverified); defaults on (Damian approved the LLM cost).
  • Coverage fix. Validation now runs whenever a domain is present (industry optional) — previously a missing industry skipped the check entirely and let contamination through.

⚠️ One design decision to confirm

Per #3268 ("the LLM may only flag/downgrade, never auto-pass-to-publish — it reads attacker-editable third-party text"), an LLM "yes" no longer publishes — it lands in unverified (held). Net effect: the only path to publish is the deterministic P856 domain match, which raises the "hold for review" volume. The held cohort is worked through by the sibling batch runner (#3271) / consumer gate (#3270). Flagging so it's a conscious call.

@nsdere — could you confirm why #361 was closed?

The record shows no stated reason. Understanding is it was paused, not a defect. This PR re-opens that work; a one-line confirmation would let us merge with confidence.

Tests

Full suite green (444 passing locally via mocha). New/updated cases: P856 verifies; LLM-"no" → mismatch/discard; LLM-"yes" → unverified; garbled answer → unverified (never discard on ambiguity); infra error → unverified + failOpen + products withheld; LLM-flag-off → no validation call + hold; Behr/Capella regression.

Re-opens #361. Closes #360. Refs adobe-rnd/llmo-data-retrieval-service#3268, adobe-rnd/llmo-data-retrieval-service#3200.

🤖 Generated with Claude Code

nsdere and others added 3 commits September 9, 2026 11:54
…main + content

The QID->article guard added in #359 cannot catch a QID that was itself resolved
to the wrong same-named entity (e.g. capella.edu -> Q12970, the star Capella), so
products/sub_brands/competitors still get contaminated even though the guard passes
(wikipedia_verified: true).

Add a validation gate right after the QID is established in extractProducts:
- deterministic pass when the entity's official website (P856) registrable-domain
  matches the brand's site domain;
- otherwise an LLM check of the entity's description against the brand's domain +
  industry.
On no match, drop the QID so neither the Wikidata SPARQL query nor the Wikipedia
fallback runs (no data rather than wrong data). Fails open on infrastructure
errors so it never regresses a brand it merely could not verify.

Introduced by: N/A

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a registrableDomain case whose input fails new URL() even after the
https:// prefix (space in the authority), exercising the catch-and-fall-through
to bare-hostname handling (wikipedia.js:253-254). Closes the codecov/patch gap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t fail-open

Re-opens the Class B wrong-QID entity-validation gate and changes its fail-open
posture. validateEntityMatchesBrand now returns a three-state
{status: verified|mismatch|unverified} instead of a boolean match:

- verified   = authoritative P856 official-website domain match (only status that publishes)
- mismatch   = definitive LLM "no" -> drop the QID (entity-brand-mismatch)
- unverified = no domain match + (LLM "yes" | no verifier | infra error) -> non-publishable,
               held for review. The LLM may only downgrade, never auto-pass-to-publish
               (it reads attacker-editable third-party text), so even an LLM "yes" holds.

Infra errors (Wikidata/LLM) previously failed open to trusted; they are now held and
flagged (failOpen), with an alarmable fail-open-rate metric emitted as structured log
lines. The LLM leg is flag-gated via BRAND_PROFILE_ENTITY_VALIDATION_LLM (defaults on).
Validation now also runs whenever a domain is present (industry optional), closing a gap
where a missing industry skipped the check and let contamination through.

Re-opens #361 (closed without a stated reason; author nsdere to confirm). Closes #360.
Refs adobe-rnd/llmo-data-retrieval-service#3268, adobe-rnd/llmo-data-retrieval-service#3200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 21, 2026 11:33
@codecov

codecov Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical validation gaps can still publish incorrect entities or contaminate competitor output.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 2 Medium severity · 1 Low severity

Open (4)
What changed in this PR

Summary

Adds three-state Wikidata entity validation and fail-open holding for brand-profile product extraction. Several critical gaps remain in ambiguity handling, domain parsing, and competitor protection.

Changes:

  • Added P856/LLM entity validation with structured observability.
  • Withheld unverified product and Wikipedia data.
  • Added extensive validation and regression tests.

Issues

Critical

  • wikipedia.js:373: Any response beginning with n is treated as a definitive mismatch; ambiguous responses such as “not sure” should remain unverified.
  • wikipedia.js:232-236: The incomplete suffix list can make unrelated .org.uk domains normalize identically and falsely publish a wrong QID.
  • index.js:258-262: Competitor inference consumes the unvalidated Wikipedia summary before product validation, so wrong-entity competitor contamination remains possible.

This PR should not be merged until these are fixed.

Major

  • BRAND_PROFILE_ENTITY_VALIDATION_LLM is not documented in README.md.

Suggested Tests

  • Add ambiguous LLM responses such as not sure and no idea.
  • Add unsupported public-suffix cases.
  • Add orchestration tests confirming invalid entities cannot influence competitor inference.
File Description
src/​agents/​brand-profile/​services/​wikipedia.js Adds domain and entity validation logic.
src/​agents/​brand-profile/​services/​product-extractor.js Gates Wikidata/Wikipedia product extraction.
src/​agents/​brand-profile/​index.js Passes validation context and feature flag.
test/​agents/​brand-profile/​services/​wikipedia.test.js Tests domain and validation outcomes.
test/​agents/​brand-profile/​services/​product-extractor.test.js Tests product withholding and validation paths.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/agents/brand-profile/services/wikipedia.js
Comment thread src/agents/brand-profile/index.js
Comment thread src/agents/brand-profile/services/wikipedia.js Outdated
Comment thread src/agents/brand-profile/index.js Outdated
…gate

- Parse LLM answer as an EXACT yes/no; hedged replies ("not sure", "no idea")
  now hold as unverified instead of being discarded as a definitive mismatch.
- Harden registrableDomain's two-level-suffix set (org.uk/ac.uk/gov.uk, *.au,
  *.jp, *.nz, *.in, *.za, *.br, ...) to avoid false P856 domain matches now that
  it is the sole publish-authoritative signal; note PSL as the robust follow-up.
- Document BRAND_PROFILE_ENTITY_VALIDATION_LLM in the README env table.

Refs #366 review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

This PR will trigger a patch release when merged.

@mikkeline-elleby

Copy link
Copy Markdown
Author

Thanks for the review — addressed in aeda5c5:

  1. LLM answer parsing ✅ — now requires an exact yes/no; hedged replies like "not sure"/"no idea" hold as unverified instead of being discarded as a definitive mismatch. Added a regression test.

  2. registrableDomain false matches ✅ — expanded the two-level-suffix set (org.uk/ac.uk/gov.uk, and *.au/.jp/.nz/.in/.za/.br second-levels) so e.g. foo.org.uk and bar.org.uk no longer collapse to org.uk. Noted a full Public Suffix List as the robust follow-up if the fleet needs exotic suffixes.

  3. README ✅ — documented BRAND_PROFILE_ENTITY_VALIDATION_LLM (default-on) in the Agent Executor Environment table.

  4. Competitor inference runs on the wiki summary before validation — good catch, and a real gap: the entity gate currently sits inside product extraction, so a wrong same-named QID can still feed inferCompetitors/persona steps upstream. Fixing it properly means hoisting the validation to run once right after the QID/article is resolved and gating all consumers — a broader refactor that also overlaps the consumer-side gate (adobe-rnd/llmo-data-retrieval-service#3270). I'd prefer to do that as a focused follow-up rather than widen this PR's scope; flagging here so it's tracked. Happy to do it in this PR instead if you'd rather.

…t products

Hoists the Class-B entity validation to run once (Phase 3.5) right after the
Wikidata article is resolved, before competitor inference, persona inference and
product extraction. A wrong same-named QID's Wikipedia summary/text is now withheld
from every consumer (previously it still reached inferCompetitors, and the sitemap
path skipped validation entirely).

- New productService.validateEntity(...) owns the check (shared gpt + LLM flag).
- index.js gates brandWiki (nulls wikidataId/fullText/summary) when not verified and
  emits the fail-open metric here; the verdict is passed into extractProducts, which
  reuses it (no second LLM call) via the new ctx.entityValidation.
- extractProducts keeps its own validation for standalone/direct callers.

Addresses review feedback on #366. Refs adobe-rnd/llmo-data-retrieval-service#3268.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-resolution

- Cover the new branches flagged by codecov/patch: productService.validateEntity
  (P856 verify, no LLM), extractProducts' precomputed-verdict reuse path, and the
  index.js fail-open metric branch.
- Fix: when a precomputed entityValidation verdict is passed, extractProducts no
  longer name-searches a replacement QID (that search is itself a contamination
  vector) — it respects index.js's decision.

Refs #366.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

brand-profile agent resolves the WRONG Wikidata entity for same-named brands (not fixed by #359)

3 participants