Skip to content

Resolving disclosures

Ask ai_disclosure.resolver rather than reading the field:

use Drupal\ai_disclosure\LabelRequirement;

$resolved = \Drupal::service('ai_disclosure.resolver')->resolve($entity);
if ($resolved !== NULL && $resolved->getLabelRequirement() === LabelRequirement::Required) {
  // ...
}

resolve() returns an immutable ResolvedDisclosure, or NULL when the entity has no effective disclosure. It carries the grade, the review flag, the editorial responsibility, the description, the EU icon, the IPTC URI, where the value came from, the scope assessment, and the verdict.

The verdict has three values

getLabelRequirement() returns a LabelRequirement: Required, NotRequired or Undetermined. There is deliberately no boolean accessor. A bool cannot carry the third value, and every caller that had not been updated would have read "not assessed" as "not required" - the silent no this API exists to prevent. Compare against the enum.

getDisclosureModality() returns a DisclosureModality: Standard, Unobtrusive or Undetermined. It qualifies a Required verdict on the deep fake branch, where an evidently artistic work limits the manner of the disclosure without lifting it. DisclosureModality::effective() collapses Undetermined to Standard, the form that discloses the most.

getScopeAssessment() returns the four answers the verdict was computed on, so a caller can say which question is still open rather than only that the answer is undetermined.

Cacheability

The object reports its own cacheability, so attaching it to a render array is enough to have the page follow changes to the grade, the profile and the field settings - as long as resolve() returned something to attach. A NULL result carries none, which is wrong when the NULL itself depends on configuration: a bundle with the field but no default profile set, for instance, must stop rendering "nothing" once an administrator sets one.

For that case, use resolveWithCacheability() instead. It returns ['resolved' => ?ResolvedDisclosure, 'cacheability' => CacheableMetadata]: the cacheability is populated even when resolved is NULL, so attach it to the render array regardless. resolve() itself just discards it and stays the right call for code that never caches its result.

Resolution itself is memoized: an entity is resolved at most once per request, keyed on its ID, language and disclosure item value, so rendering the same field twice - a formatter's view() and viewElements(), for instance - does not resolve it twice. Call resetMemo() if you change something the resolver reads outside of the item itself, such as a grade or a profile, from code the module's own hook_entity_update() does not cover.

Broken references

A broken grade or profile reference never throws: resolution falls through to the next step and logs the dangling reference once per request. The compliance report surfaces the same dangling references in its configuration notices.

The same rule in SQL and in JavaScript

Two surfaces cannot call resolve(), so the rule is written twice more.

The report cannot resolve every row, so ai_disclosure.query_conditions restates the resolution precedence as SQL conditions for the module's Views filters. DisclosureQueryConditionsTest compares the two row by row over a dataset covering every branch of the precedence.

The editing widget cannot make an AJAX request on every change, so js/ai-disclosure-widget.js recomputes the status line as the editor works.

ResolvedDisclosure::labelVerdictFor() stays the definition. The other two follow it, and a change to the rule is a change to all three.