Advisory contract

Padosoft\Iam\Ai\Advisory is the single result type of the module. It is final readonly — immutable once
constructed — and carries the answer plus the governance metadata that lets you trust (or distrust) it.

Fields

final readonly class Advisory
{
    public function __construct(
        public string $text,
        public array  $citations = [],     // list<string>
        public bool   $aiUsed = false,
        public bool   $redacted = false,
        public bool   $guardPassed = true,
        public array  $violations = [],     // list<string>
        public string $provider = 'deterministic',
    ) {}
}
Field Type Meaning
text string The answer to show. Model output on a clean path; the deterministic fallback on any failure path. Always safe to display.
citations list<string> References to real evidence the answer may cite (e.g. dec_01H…, orders:refund). Set by the calling module from allowedRefs.
aiUsed bool true only if a model actually produced the text. false when AI is off, the transport threw, or the guard rejected the output.
redacted bool true if redaction fired on the input or the output. Mirrors Redactor::didRedact across both passes.
guardPassed bool true if the hallucination-guard found no invented identifiers. false ⇒ the text is the fallback, not the model’s.
violations list<string> The invented identifiers the guard caught (empty when guardPassed is true).
provider string Transport identity: deterministic (AI off), the provider’s name() (e.g. regolo, ollama), used for audit/telemetry.

Serialization

public function toArray(): array;

Returns:

[
    'text'          => string,
    'citations'     => list<string>,
    'ai_used'       => bool,
    'redacted'      => bool,
    'guard_passed'  => bool,
    'violations'    => list<string>,
    'provider'      => string,
    'advisory_only' => true,   // ALWAYS injected — the self-label
]
advisory_only is always true

toArray() unconditionally adds 'advisory_only' => true. Any serialized advisory self-identifies as a
proposal, never a decision. There is no field that represents an allow/deny verdict — by design.

Field values per pipeline branch

Branch text aiUsed guardPassed violations provider
AI disabled (default) fallback false true [] deterministic
Transport threw fallback false true [] provider name()
Guard found violations fallback true false [id, …] provider name()
Clean success redacted model output true true [] provider name()

redacted is orthogonal to the branch: it is true whenever redaction touched the input or output, on any
branch. → The advisory pipeline

How to read it correctly

$advisory = app(AccessExplainer::class)->explain($decision, 'Why?');

// Show this:
echo $advisory->text;

// Trust decisions to the PDP, not this:
$allowed = $decision['allowed'] === true;   // ✅ enforcement reads the PDP

// Use the flags for telemetry / UX, never for authorization:
if (! $advisory->aiUsed)      { /* deterministic answer — maybe label it */ }
if (! $advisory->guardPassed) { /* model invented an id; you already have the fallback */ }
if ($advisory->redacted)      { /* sensitive data was stripped from the interaction */ }
Do not derive a verdict from an Advisory

There is no allowed on Advisory, and text is prose. Read allow/deny from the PDP decision array. The
advisory’s booleans are governance signals (aiUsed, redacted, guardPassed), not authorization.

JSON example

{
  "text": "Accesso NEGATO (decision dec_01H…). Nessun grant corrispondente per orders:refund.",
  "citations": ["dec_01H…", "orders:refund"],
  "ai_used": false,
  "redacted": true,
  "guard_passed": true,
  "violations": [],
  "provider": "deterministic",
  "advisory_only": true
}

See also