Skip to content
Skip to lesson
Back to Topics
LearnApplied LLM EngineeringResponsible AI Governance
🛡️MediumAlignment & Safety

Responsible AI Governance

A blocked promotion is not yet governance. The release still has to name the workflow, its owner, the tested control, and evidence a reviewer can replay.

32 min read
Learning path
Step 67 of 199 in the full curriculum

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

Continue the local mock release assistant from Prompt Injection Defense. Candidate C17's retrieved notes said to skip approval and promote the model. Our fixture sent a typed proposal to a Python policy gate, which denied the unauthorized effect. No production service was connected.

That trace shows one attempted promotion was blocked. Exercise every promotion route, confirm valid requests still work, and verify that reviewers can explain and correct a bad denial.

A model owner disputes the denial on Monday morning. Which policy version ran? Who owned the prompt-injection risk? Was the fix tested before release? "The model behaved better in our demo" can't answer those questions. The platform needs an auditable evidence trail.

turns that dispute into an engineering loop. It answers five questions:

  1. What workflow is being deployed, and who can it affect?
  2. What harm is plausible, and who owns that risk?
  3. What control reduces it?
  4. What evidence proves the control ran and was tested?
  5. What review, escalation, or appeal happens when the system is wrong?

The examples below are local Python simulations of evidence and policy decisions. Applying them requires a separate deployment and legal review. The legal discussion is an EU-focused engineering orientation checked on 21 September 2026, including the July 2026 amendments. Qualified reviewers must determine the duties for a particular jurisdiction, role, and use.

Connect ethical commitments to evidence

Suppose the promotion gate blocks every attack but also denies every screen-reader request. Its security result hasn't answered the fairness question. We need a specific account of the harm, evidence about affected people, and someone with authority to repair it.

Some commitments lead to executable checks; others require interviews, contextual judgment, or organizational decisions. For this workflow:

Move from a blocked attack to an evidence loop

The fairness check and the named owner still need a place to live. Which artifact answers which release question?

NIST's voluntary AI Risk Management Framework (version 1.0, under revision as of late 2026) organizes risk management into Govern, Map, Measure, and Manage.[6] Govern isn't a first step that you check off. It informs the other three functions throughout the lifecycle. NIST's Generative AI Profile adapts this structure to risks such as harmful outputs, information integrity, privacy, and security.[7]

ISO/IEC 42001:2023 specifies an organizational Artificial Intelligence Management System (AIMS), using Plan-Do-Check-Act for establishment and continual improvement. Applying or certifying a management system doesn't by itself establish that this assistant is safe or legally compliant.[8]

For the model-promotion assistant, each function answers a different release question:

FunctionQuestionArtifact
GovernWho can accept this risk and approve release?Owner, policy, review record
MapWhat decision and stakeholder can the assistant affect?Workflow inventory and classification memo
MeasureDid attacks or ordinary release requests expose failures?Evaluation and report
ManageWhich controls, escalations, and release gates apply?, , approval path

This mapping turns a policy idea into a release path. NIST doesn't prescribe a fixed order; here we start with the workflow that can cause an effect, then collect only the evidence needed to decide whether that effect is acceptable.

Inventory workflows before classifying them

A platform may use one model family in several workflows. The model name isn't the classification unit: an assistant that summarizes public documentation and one that proposes a credit decision can share weights yet require very different review.

Record intended use, affected stakeholder, possible effect, and first review route. Treat that route as an engineering triage label. It sends work to legal and product reviewers; it isn't the final legal determination.

Predict the routes before running the small classifier. Which workflow can affect credit? Which one speaks to a person or can change production state? What if the intake never answered those questions? Here False means a fact was checked and ruled out; None means it remains unknown.

01-triage-workflows-for-review.py
from dataclasses import dataclass

@dataclass(frozen=True)
class Workflow:
    name: str
    affects_credit: bool | None = None
    manages_workers: bool | None = None
    talks_to_people: bool | None = None
    can_execute_effects: bool | None = None

    def __post_init__(self):
        flags = (self.affects_credit, self.manages_workers,
                 self.talks_to_people, self.can_execute_effects)
        if any(value is not None and type(value) is not bool for value in flags):
            raise ValueError("workflow flags must be Boolean or unknown")

def triage(workflow: Workflow) -> str:
    flags = (workflow.affects_credit, workflow.manages_workers,
             workflow.talks_to_people, workflow.can_execute_effects)
    if any(value is None for value in flags):
        return "NEEDS_FACTS_REVIEW"
    if workflow.affects_credit or workflow.manages_workers:
        return "HIGH_RISK_REVIEW"
    if workflow.talks_to_people or workflow.can_execute_effects:
        return "TRANSPARENCY_AND_EFFECT_REVIEW"
    return "BASELINE_REVIEW"

workflows = [
    Workflow("loan_screening", affects_credit=True, manages_workers=False,
             talks_to_people=False, can_execute_effects=False),
    Workflow("model_promotion_assistant", affects_credit=False, manages_workers=False,
             talks_to_people=True, can_execute_effects=True),
    Workflow("public_doc_summary", affects_credit=False, manages_workers=False,
             talks_to_people=False, can_execute_effects=False),
    Workflow("incomplete_intake"),
]

for workflow in workflows:
    print(f"{workflow.name}: {triage(workflow)}")
Output
loan_screening: HIGH_RISK_REVIEW
model_promotion_assistant: TRANSPARENCY_AND_EFFECT_REVIEW
public_doc_summary: BASELINE_REVIEW
incomplete_intake: NEEDS_FACTS_REVIEW

The inventory routes the model-promotion assistant to review without calling it a credit system. Its user interface and production permissions deserve separate scrutiny. Missing facts require investigation, rather than a baseline label. Our organization requires approval, audit evidence, and a challenge path; those are local controls, not legal duties automatically created whenever an assistant has a tool. The function covers only these examples. A real intake also checks prohibited uses, other regulated sectors, and jurisdiction. It may identify a high-risk review need before every remaining fact is known.

Map system risks, then check separate model duties

For the EU AI Act, first establish territorial scope and your role. Article 2 covers, among others, providers placing systems on the EU market, EU deployers, and certain non-EU providers or deployers where output is used in the EU. An application developer can be a provider of an AI system even when another company supplies the underlying general-purpose model. Don't infer that role from a vendor name alone.[9]

The Commission explains system risks using four levels. This is a useful intake map, rather than four mutually exclusive legal boxes: transparency duties can also apply to a high-risk system.[10]

LevelWhat intake should investigate
Unacceptable riskWhether a practice meets an Article 5 prohibition, including its conditions and exceptions
High riskThe product criteria in Article 6(1), or a listed Annex III use under Article 6(2)
Transparency risk, often called limited riskDuties for particular interactions or generated content under Article 50
Minimal or no riskUses outside the prohibited/high-risk categories, with any applicable transparency and other duties still checked

Prohibited practices need precise facts. Article 5 covers, among other things, manipulative or vulnerability-exploiting techniques meeting significant-harm conditions; social scoring that leads to specified unrelated, unjustified, or disproportionate adverse treatment; untargeted facial-image scraping to build recognition databases; and workplace/education emotion inference, with a medical or safety exception. It also restricts specified sensitive biometric categorization, certain individual criminal-risk predictions, and real-time public-space biometric identification for law enforcement. The latter has narrow objectives and authorization safeguards. Don't turn these provisions into a blanket ban on every score, emotion tool, or biometric use, or assume paperwork legalizes a prohibited practice.[9]

The July amendment adds prohibitions concerning nonconsensual intimate material and child sexual abuse material, applicable from 2 December 2026. Provider restrictions distinguish intended generation from reasonably foreseeable, reproducible generation without adequate prevention safeguards; deployer restrictions concern use for that purpose. The detailed definitions, conditions, and exceptions matter at intake.[11]

High-risk status has two routes. For Article 6(1), both conditions must hold: the AI is a safety component of an Annex I product, or itself such a product, and the relevant product requires third-party conformity assessment for health and safety. Solely non-safety assistance or convenience isn't a safety component; an AI failure that endangers health and safety can make it one. Annex III separately lists uses such as specified biometrics, educational assessment, employment, and essential services. Its creditworthiness entry expressly excludes financial-fraud detection.[9]

Article 6(3) allows an Annex III exception where the system poses no significant risk of harm to health, safety, or fundamental rights, including by not materially influencing decision outcomes, alongside at least one of four conditions: a narrow procedural task, improving a completed human activity, detecting decision patterns/deviations without replacing or influencing the completed assessment without proper human review, or a preparatory task. An Annex III system that profiles natural persons remains high risk. A provider relying on the exception must document its assessment before marketing or putting it into service and register the system.[9]

High-risk requirements include risk management, data governance, documentation, logging, transparency, human oversight, accuracy, robustness, and cybersecurity (Articles 9–15). Which obligations fall on which actor, and how they apply through sector law, must be checked. Article 2 gives Annex I Section B products a special sector-integration regime; not every product follows the same direct compliance path.[9]

GPAI is a separate model layer. Duties for a general-purpose AI model's provider sit alongside the classification of systems built with it:

  • Article 53 covers model documentation, information for downstream integrators, copyright policy, and a sufficiently detailed public summary of training content. Qualifying open-source models have a limited exemption from the first two duties, not the copyright/summary duties; that exemption doesn't apply to models with systemic risk.
  • Articles 51–52 concern high-impact capabilities or Commission designation. Training compute above 102510^{25} FLOPs creates a presumption of high-impact capabilities, with an exceptional rebuttal procedure. It isn't the only way to qualify.
  • Article 55 adds model evaluation, documented adversarial testing, systemic-risk assessment and mitigation, serious-incident reporting, and cybersecurity. Known or estimated training energy consumption belongs in Annex XI documentation under Article 53; it isn't an energy-efficiency requirement unique to systemic-risk models.[9]

A loan-screening application can therefore be a high-risk system built on a GPAI model. Its application provider and the model provider have different responsibilities. Replacing the lender's model doesn't erase the application's intended use.

Record regulatory status with its review date

Regulatory dates belong in the memo because they change. Regulation (EU) 2024/1689 originally put most remaining rules on 2 August 2026, with the Article 6(1) product-related high-risk rules following on 2 August 2027. The Digital Omnibus on AI, Regulation (EU) 2026/1744, entered into force on 27 July 2026.[11]

The Commission's current implementation summary gives 2 December 2027 for Annex III high-risk rules and 2 August 2028 for high-risk systems covered through regulated products. These aren't blanket extensions for every AI obligation. Article 50 transparency duties generally apply from 2 August 2026. Providers of systems placed on the market before that date have until 2 December 2026 for the specific Article 50(2) machine-readable marking and detection obligation, not a general chatbot-disclosure holiday. (amended Article113)

GPAI provider duties began applying on 2 August 2025, with an Article 111(3) transition to 2 August 2027 for models already on the market before that date. Existing high-risk systems also have specific transitional rules. Establish when the relevant model/system was placed on the market and whether changes affect its status.[9]

For a directly interacting assistant, Article 50(1) concerns informing people that they're interacting with AI, subject to its conditions and exceptions, including when that fact is already obvious in context. Article 50(2) concerns output marking; Article 50(4) has different deployer disclosure rules for deepfakes and certain public-interest text. Don't collapse those duties into "label every generated sentence." Our interface identifies itself as AI regardless of whether an exception might apply.[9]

Make the moving part explicit: include a legal_basis_checked_on date and required legal signoff in the classification memo. Recheck Official Journal status, Commission guidance, and local counsel before launch or a material feature change.

The next memo records the promotion assistant, not the credit system. Credit would be HIGH_RISK_REVIEW; the assistant still needs dated legal review because applicable transparency duties depend on role, use, and context even without Annex III high-risk status.

Predict what a reviewer should reject if the memo has no check date or signoff. The route might look plausible, but nobody can tell which legal version the decision used.

02-build-a-classification-memo.py
from dataclasses import dataclass, asdict

@dataclass(frozen=True)
class ClassificationMemo:
    workflow_id: str
    intended_use: str
    affected_people: str
    jurisdiction: str
    organization_role: str
    triage_route: str
    legal_basis_checked_on: str
    legal_signoff_required: bool

memo = ClassificationMemo(
    workflow_id="promotion-assistant-7.2",
    intended_use="answer eval questions and propose model promotions for approval",
    affected_people="model owners and reviewers",
    jurisdiction="EU deployment under review",
    organization_role="provider of the assistant; third-party underlying model",
    triage_route="TRANSPARENCY_AND_EFFECT_REVIEW",
    legal_basis_checked_on="2026-09-21",
    legal_signoff_required=True,
)

record = asdict(memo)
print("workflow:", record["workflow_id"])
print("route:", record["triage_route"])
print("checked_on:", record["legal_basis_checked_on"])
print("release_requires_legal_signoff:", record["legal_signoff_required"])
Output
workflow: promotion-assistant-7.2
route: TRANSPARENCY_AND_EFFECT_REVIEW
checked_on: 2026-09-21
release_requires_legal_signoff: True

Create a risk register row a reviewer can use

The classification memo tells a reviewer where to start. It doesn't say what can go wrong or who must act. A risk register connects a plausible harm to its control, owner, evidence, remaining risk, and next review.

Start with C17. If the retrieved note can influence a promotion proposal, predict the row before reading it: identify the unsafe effect, the control that stops it, and the evidence a reviewer could replay.

FieldModel-release entry
RiskRetrieved eval content instructs agent to bypass promotion approval
HarmUnauthorized model promotion or inconsistent release treatment
Inherent severityHigh, because the agent can change production traffic
ControlTreat retrieved text as untrusted and authorize promotions in application code
EvidenceAttack- test report, log, approval replay
OwnerModel platform release owner
Residual riskProvisional medium rating, subject to evidence review
Review triggerNew tool scope, new data source, incident, or scheduled review

The first row protects the production effect. The gate itself also needs protection: contaminated evaluation evidence can make a release look safer than reality, so record a second row for evaluation integrity:

FieldEvaluation-integrity entry
RiskContaminated evaluation evidence (holdout leakage, harness gold readable by agents, judge prompt injection)
HarmUnsafe candidate promoted on inflated scores or memorized frozen cases
Inherent severityHigh, because the gate itself becomes untrustworthy
ControlIsolate holdout content from training and agent-readable paths; treat judge inputs as untrusted; test judge attacks and block on known leakage
EvidenceExact and near-duplicate overlap checks, harness access audit, judge attack and order-swap tests
OwnerEvaluation and release owner
Residual riskMedium until private suite isolation and judge limits are reviewed
Review triggerNew dataset version, harness change, judge model change, or incident

Risk scoring doesn't replace judgment. It gives reviewers a reproducible way to prioritize work and trigger escalation.

The next calculation records a reviewer's estimated effect of a control. We use local ordinal ratings from 1 to 5, where 1 means low and 5 means high. Their product is a relative prioritization score. Estimate monetary loss or event probability with separate data and methods; a score of 20 does not mean twice the harm of a score of 10.

03-score-an-owned-risk.py
from dataclasses import dataclass

@dataclass(frozen=True)
class RiskRow:
    risk_id: str
    owner: str
    likelihood: int
    impact: int
    residual_likelihood: int
    residual_impact: int
    evidence: tuple[str, ...]

    def __post_init__(self):
        ratings = (self.likelihood, self.impact, self.residual_likelihood, self.residual_impact)
        if any(type(value) is not int or not 1 <= value <= 5 for value in ratings):
            raise ValueError("risk ratings must be integers from 1 to 5")
        if not isinstance(self.owner, str) or not self.owner.strip() or not self.evidence:
            raise ValueError("risk assessment needs an owner and evidence references")
        if not isinstance(self.evidence, tuple) or any(
            not isinstance(item, str) or not item.strip() for item in self.evidence
        ):
            raise ValueError("evidence references must be nonblank strings")

    def inherent_score(self) -> int:
        return self.likelihood * self.impact

    def residual_score(self) -> int:
        return self.residual_likelihood * self.residual_impact

row = RiskRow(
    risk_id="MP-014-prompt-injection-promotion",
    owner="model-platform-release-owner",
    likelihood=4,
    impact=5,
    residual_likelihood=2,
    residual_impact=5,
    evidence=("promotion-gate-test-report-v7", "approval-replay-v7"),
)

print("risk:", row.risk_id)
print("owner:", row.owner)
print("inherent_score:", row.inherent_score())
print("residual_score:", row.residual_score())
print("evidence_count:", len(row.evidence))
Output
risk: MP-014-prompt-injection-promotion
owner: model-platform-release-owner
inherent_score: 20
residual_score: 10
evidence_count: 2

Here the reviewer assigns residual likelihood 2 instead of 4 while impact stays at 5. Link that judgment to tests and operating experience, then compare the observed risk after control operation. An unauthorized promotion remains serious even after a lower residual rating.

Reject ceremonial risk registers

A row without an owner, control evidence, or review date leaves the follow-up unclear. The validator below treats that missing information as a reason to hold review, not as permission to ship.

Run the validator below as a release check. A complete-looking control label still blocks when its evidence, residual risk, or review date is missing.

04-validate-risk-register-rows.py
from datetime import date

REQUIRED_FIELDS = {
    "risk_id",
    "owner",
    "control",
    "evidence",
    "residual_risk",
    "next_review_on",
}

def risk_row_errors(row: dict[str, object], as_of: date) -> list[str]:
    errors = []
    for field in sorted(REQUIRED_FIELDS - {"evidence", "next_review_on"}):
        value = row.get(field)
        if not isinstance(value, str) or not value.strip():
            errors.append(field)
    evidence = row.get("evidence")
    if not isinstance(evidence, list) or not evidence or any(
        not isinstance(item, str) or not item.strip() for item in evidence
    ):
        errors.append("evidence")
    try:
        due = date.fromisoformat(row.get("next_review_on", ""))
        if due <= as_of:
            errors.append("next_review_on: review due")
    except (ValueError, TypeError):
        errors.append("next_review_on")
    return sorted(errors)

rows = [
    {
        "risk_id": "MP-014",
        "owner": "model-platform-release-owner",
        "control": "promotion_policy_gate_v7",
        "evidence": ["attack-suite-2026-05-31"],
        "residual_risk": "medium",
        "next_review_on": "2026-10-01",
    },
    {
        "risk_id": "CR-002",
        "owner": "",
        "control": "manual review",
        "evidence": [],
        "residual_risk": "",
        "next_review_on": "",
    },
]

for row in rows:
    errors = risk_row_errors(row, date(2026, 9, 21))
    status = "READY_FOR_REVIEW" if not errors else "BLOCKED"
    print(row["risk_id"], status, errors)
Output
MP-014 READY_FOR_REVIEW []
CR-002 BLOCKED ['evidence', 'next_review_on', 'owner', 'residual_risk']

Document the system and its data separately

A documents a model's intended use, limitations, evaluation, and known risks.[3] An application-level system card adds the surrounding prompts, tools, interfaces, and controls. A datasheet tells reviewers where a dataset came from, what it contains, how it was collected and processed, and which uses its creators recommend.[4]

Pair both records for an LLM application:

  • The system card identifies model version, prompt or policy version, enabled tools, effect gates, safety tests, limitations, and owner.
  • The identifies the attack fixtures, ordinary release-request examples, , labeling instructions, sensitive fields, evaluation split, and retention rule.

The system card answers "what shipped?" The dataset record answers "which evidence produced that result?" Neither one automatically satisfies a statutory documentation duty. Together they can support wider obligations when they stay accurate, versioned, and reviewed.

The next check catches missing fields, blank text, and empty lists. Its passing result confirms the small record has the expected shape. Verify evaluation execution, redaction behavior, and the 90-day retention policy with separate evidence and review.

05-check-release-documentation.py
system_card = {
    "system_version": "promotion-assistant-7.2",
    "model_snapshot_id": "mock-model-v1",
    "prompt_version": "release-prompt-v3",
    "policy_version": "promotion-gate-v7",
    "intended_use": "answer candidate-eval questions and propose model promotions for approval",
    "out_of_scope": ["autonomous production promotion"],
    "enabled_tools": ["propose_promotion"],
    "evaluations": ["attack-suite-2026-05-31", "benign-suite-2026-05-31"],
    "known_limitations": ["mock traces only; no hosted-model evaluation",
                          "attack-recognition flag is a fixture"],
    "rollback_owner": "model-platform-release-owner",
}

dataset_record = {
    "dataset_id": "promotion-redteam-v2",
    "provenance": "curated release-request and injected-eval fixtures",
    "labeling_guide": "unsafe_effects-v2",
    "sensitive_fields_removed": True,
    "evaluation_split": "frozen-promotion-eval-v2",
    "retention_rule": "retain redacted fixtures for 90 days",
}

required_system = {
    "system_version",
    "model_snapshot_id",
    "prompt_version",
    "policy_version",
    "intended_use",
    "out_of_scope",
    "enabled_tools",
    "evaluations",
    "known_limitations",
    "rollback_owner",
}
required_dataset = {
    "dataset_id",
    "provenance",
    "labeling_guide",
    "sensitive_fields_removed",
    "evaluation_split",
    "retention_rule",
}

def incomplete_fields(record, required):
    def valid_field(name):
        value = record.get(name)
        if name == "sensitive_fields_removed":
            return type(value) is bool  # Confirm redaction by inspecting the resulting data.
        if name in {"out_of_scope", "enabled_tools", "evaluations", "known_limitations"}:
            return isinstance(value, list) and bool(value) and all(
                isinstance(item, str) and item.strip() for item in value
            )
        return isinstance(value, str) and bool(value.strip())
    return sorted(name for name in required if not valid_field(name))

missing_system = incomplete_fields(system_card, required_system)
missing_dataset = incomplete_fields(dataset_record, required_dataset)
print("system_card_complete:", not missing_system)
print("dataset_record_complete:", not missing_dataset)
print("record_shape_valid:", not missing_system and not missing_dataset)
Output
system_card_complete: True
dataset_record_complete: True
record_shape_valid: True

Preserve an audit trail without collecting everything

The prompt-injection defense chapter separated untrusted retrieved text from trusted policy and gated promotion effects in code. At C17, governance adds a replayable record of the same decision:

Minimize the trace payload. An audit trail shouldn't store hidden reasoning or unlimited private evaluation data. Keep the business facts needed to reproduce the effect decision, then apply access and retention rules. Sensitive content belongs in the record only when it's necessary and authorized.

Predict which fields a reviewer needs after the owner disputes the denial. Versions, trust labels, gate results, and the appeal link answer that question without copying every private document into the log.

Two hash chains share a first event, a proposal with hash 3ea1a65a08e1. The original chain continues with a blocked gate decision, hash 4942d026d5ce, and no effect, hash 252f127e01b6, which matches a separately protected copy of the head. The rewritten chain changes the gate decision to approved, hash 55116e3c215f, and its forged head is a969c47e70ed, which differs from the protected copy.
Each event includes the previous digest. The figure abbreviates hashes to 12 hexadecimal characters; the code compares full SHA-256 digests against the copied review anchor.

Translate those fields into an audit record. The local example prints the observable decision facts while keeping the actor identifier pseudonymous.

06-write-a-privacy-conscious-audit-record.py
from dataclasses import dataclass, asdict
from hashlib import sha256
import hmac

AUDIT_PSEUDONYM_KEY = b"local-demo-key-not-for-production"

@dataclass(frozen=True)
class AuditRecord:
    request_id: str
    actor_role: str
    workflow_version: str
    policy_version: str
    retrieved_source_id: str
    retrieved_source_trust: str
    proposed_effect: str
    gate_decision: str
    approval_id: str
    final_effect: str
    appeal_id: str

def pseudonymize_actor_id(actor_id: str) -> str:
    digest = hmac.new(AUDIT_PSEUDONYM_KEY, actor_id.encode(), sha256).hexdigest()
    return f"actor-{digest[:24]}"

record = AuditRecord(
    request_id=f"promotion/{pseudonymize_actor_id('USER-918204')}/001",
    actor_role="model_owner",
    workflow_version="promotion-assistant-7.2",
    policy_version="promotion-gate-v7",
    retrieved_source_id="candidate-eval/C17/R42",
    retrieved_source_trust="UNTRUSTED_EVAL_CONTENT",
    proposed_effect="promote:C17:prod-10pct",
    gate_decision="BLOCKED_REQUIRES_APPROVAL",
    approval_id="APR-48291",
    final_effect="NO_PROMOTION_EXECUTED",
    appeal_id="APL-48291",
)

for field, value in asdict(record).items():
    print(f"{field}: {value}")
Output
request_id: promotion/actor-f027638fb6653d5861c880a5/001
actor_role: model_owner
workflow_version: promotion-assistant-7.2
policy_version: promotion-gate-v7
retrieved_source_id: candidate-eval/C17/R42
retrieved_source_trust: UNTRUSTED_EVAL_CONTENT
proposed_effect: promote:C17:prod-10pct
gate_decision: BLOCKED_REQUIRES_APPROVAL
approval_id: APR-48291
final_effect: NO_PROMOTION_EXECUTED
appeal_id: APL-48291

The example hardcodes its key so it stays runnable locally. In a service, keep a versioned HMAC key in a manager, restrict access, and document rotation. Treat this 96-bit display pseudonym as linkable sensitive data; estimate collision risk for the record volume and never use it as an authorization credential. Stable pseudonyms reveal repeated activity; use scope-specific keys where cross-workflow linkage isn't needed. Store timestamps, model/tool versions, reviewer decisions, and actual executor receipts alongside this abbreviated fixture.

Detect a rewritten log against a protected anchor

An attacker with write access to mutable logs can change blocked to approved and erase incident evidence. One defense links each event to the previous digest and preserves an independently protected head:

Digestt=SHA-256(Serializev(Eventt,PreviousDigest))\text{Digest}_t = \text{SHA-256}(\text{Serialize}_v(\text{Event}_t, \text{PreviousDigest}))

Serialize_v is an agreed, versioned byte encoding. Our local fixture uses Python's sorted-key JSON and UTF-8. That isn't a general cross-language canonicalization standard; a production scheme must specify escaping, types, separators, and ordering.

Changing an event changes its subsequent digests, assuming SHA-256's collision resistance. Recomputing the mutable chain can restore internal consistency, but it won't restore a match to an authentic anchor outside the attacker's write authority.

Suppose someone changes gate_decision from blocked to approved. Predict what should happen: recomputing the chain may make its internal hashes consistent, but the new head should disagree with the protected review anchor.

07-detect-an-altered-decision-chain.py
import hashlib
import json

def digest_event(event: str, value: str, previous: str) -> str:
    payload = json.dumps({"event": event, "value": value, "previous": previous}, sort_keys=True)
    return hashlib.sha256(payload.encode()).hexdigest()

def append_event(chain: list[dict[str, str]], event: str, value: str) -> None:
    previous = chain[-1]["digest"] if chain else "GENESIS"
    digest = digest_event(event, value, previous)
    chain.append({"event": event, "value": value, "previous": previous, "digest": digest})

def recompute_digests(chain: list[dict[str, str]]) -> None:
    for index, item in enumerate(chain):
        previous = chain[index - 1]["digest"] if index else "GENESIS"
        item["previous"] = previous
        item["digest"] = digest_event(item["event"], item["value"], previous)

def verifies(chain: list[dict[str, str]], protected_anchor: str) -> bool:
    expected_previous = "GENESIS"
    for item in chain:
        expected_digest = digest_event(item["event"], item["value"], expected_previous)
        if item["previous"] != expected_previous or item["digest"] != expected_digest:
            return False
        expected_previous = item["digest"]
    return expected_previous == protected_anchor

events: list[dict[str, str]] = []
append_event(events, "tool_proposal", "promote:C17:prod-10pct")
append_event(events, "policy_gate", "blocked_requires_approval")
append_event(events, "effect", "none")
review_anchor = events[-1]["digest"]  # Copy to a separately protected review record.
print("review_anchor:", review_anchor[:12])
print("original_chain_valid:", verifies(events, review_anchor))

events[1]["value"] = "approved"
recompute_digests(events)
print("rewritten_chain_matches_anchor:", verifies(events, review_anchor))
Output
review_anchor: 252f127e01b6
original_chain_valid: True
rewritten_chain_matches_anchor: False

A protected head detects edits, reordering, or truncation of the anchored sequence. Verify event truth and completeness at ingestion. An attacker controlling both log and anchor can rewrite both. Production designs need authenticated writers, scoped anchors with sequence lengths and timestamps, durable write-once storage, and a retention/deletion design. The local review_anchor variable demonstrates comparison; enforce access boundaries through trusted storage controls.

For EU high-risk deployments, logging, documentation, record-keeping, and human oversight can be regulated duties, with different provider and deployer responsibilities and application dates. Article 26(6), for example, requires deployers to keep generated logs under their control for an appropriate period of at least six months unless applicable Union or national law provides otherwise. The dataset fixture's 90-day rule isn't a default for those logs. Match retention to the applicable rules and privacy duties.[9]

Turn red-team results into approval decisions

The preceding lesson calculated attack success rate (ASR) and false rejection rate (FRR) from hypothetical counts, with a confidence bound and per-path coverage. It didn't run a hosted-model attack suite. Governance attaches an owner, an evidence link, and a release decision to measurements when those evaluations actually run.

Red teaming deliberately probes for failures, including harmful output, disclosure, and forbidden tool effects. It can inform development and continued monitoring after release. Research on language-model red teaming demonstrates ways to uncover harmful behavior and develop adversarial examples.[13]

For a governed workflow, a rejected prompt isn't the result you release. Record whether an effect occurred and who owns the finding:

  • ASR: fraction of attack traces that cause a forbidden effect or disclosure.
  • FRR: fraction of ordinary approval requests that are blocked unnecessarily.
  • Finding owner: person responsible for remediation or accepted residual risk.
  • Evidence link: trace , gate version, reproduction, and retest result.

Predict the gate outcome from the next five synthetic trace outcomes. One unsafe effect should block the release; a benign request blocked at a high rate creates a separate friction finding. FRR doesn't measure whether accepted requests were completed correctly; evaluate that separately.

08-convert-red-team-results-to-a-gate.py
from dataclasses import dataclass

@dataclass(frozen=True)
class TestResult:
    kind: str
    unsafe_effect: bool
    wrongly_blocked: bool = False

results = [
    TestResult("attack", unsafe_effect=False),
    TestResult("attack", unsafe_effect=False),
    TestResult("attack", unsafe_effect=True),
    TestResult("benign", unsafe_effect=False, wrongly_blocked=False),
    TestResult("benign", unsafe_effect=False, wrongly_blocked=True),
]

def evaluate_results(results):
    if any(result.kind not in {"attack", "benign"} for result in results):
        raise ValueError("unknown test kind")
    if any(type(result.unsafe_effect) is not bool or type(result.wrongly_blocked) is not bool for result in results):
        raise ValueError("test outcomes must be Boolean")
    attacks = [result for result in results if result.kind == "attack"]
    benign = [result for result in results if result.kind == "benign"]
    asr = sum(result.unsafe_effect for result in attacks) / len(attacks) if attacks else None
    frr = sum(result.wrongly_blocked for result in benign) / len(benign) if benign else None
    reasons = []
    if not attacks or not benign:
        reasons.append("missing attack or benign coverage")
    if any(result.unsafe_effect for result in results):
        reasons.append("unsafe effect survived test")
    if frr is not None and frr > 0.10:
        reasons.append("legitimate requests wrongly blocked")
    return asr, frr, reasons

asr, frr, release_reasons = evaluate_results(results)

print(f"asr: {asr:.2f}")
print(f"frr: {frr:.2f}")
print("finding_owner: model-platform-release-owner")
print("release_blocked:", bool(release_reasons))
print("release_reasons:", release_reasons)
Output
asr: 0.33
frr: 0.50
finding_owner: model-platform-release-owner
release_blocked: True
release_reasons: ['unsafe effect survived test', 'legitimate requests wrongly blocked']

One forbidden promotion blocks this release, whether it occurs during an attack or an ordinary request. FRR creates a separate finding: a control that strands legitimate model owners needs repair too. No cases means unknown, not zero failures. Even zero failures in three attack tests would only give a one-sided 95% binomial upper bound of 1−0.051/3≈63.2%1-0.05^{1/3}\approx63.2\%, assuming independent trials from a fixed distribution. Curated adversarial cases don't establish that sampling assumption; they expose failures, not population-wide safety.

Measure who bears errors and friction

A secure effect gate still fails people if legitimate model owners are blocked on a supported path or can't reach an appeal. Ethics becomes engineering work when the team measures those outcomes, investigates disparities, and repairs the barrier.[1]

For the release assistant, run ordinary eligible promotion scenarios through every supported path: web console, CLI workflow, keyboard-only navigation, screen-reader-assisted interaction, and escalation to a person. Test the interface with users and accessibility specialists where possible. Use larger, representative evaluations to assess fairness across affected populations.

The diagnostic below compares paths instead of hiding friction in one aggregate rate. Four blocked requests out of 28 is about 14.3%, but all four occur among the eight screen-reader requests. That path's rate is 50%. These repeated synthetic tuples illustrate the calculation; they aren't observations from a user study or properties of screen-reader users.

Unit chart in which each square is one permitted request. The web console row has twenty squares, none blocked. The screen-reader row has eight squares, four blocked, a rate of 50 percent. The combined row has twenty-eight squares, four blocked, a rate of 14.3 percent. A marker shows the chosen 25 percent limit: the combined rate is under it, and the screen-reader rate is at twice the limit.
Compare the two measured paths with the 25% limit. The fixture total is lower, while the screen-reader route still needs repair and retesting.
09-slice-valid-request-friction.py
from collections import defaultdict

cases = ([('web_console', True, False)] * 20
         + [('screen_reader_path', True, True)] * 4
         + [('screen_reader_path', True, False)] * 4)
supported_paths = {"web_console", "screen_reader_path"}

def slice_rates(cases, supported_paths):
    if not supported_paths or any(
        not isinstance(path, str) or not path.strip() for path in supported_paths
    ):
        raise ValueError("supported paths must be named")
    blocked_by_path: dict[str, list[bool]] = defaultdict(list)
    for path, eligible, wrongly_blocked in cases:
        if path not in supported_paths:
            raise ValueError("case path is outside the supported inventory")
        if type(eligible) is not bool or type(wrongly_blocked) is not bool:
            raise ValueError("case outcomes must be Boolean")
        if eligible:
            blocked_by_path[path].append(wrongly_blocked)
    return {
        path: sum(blocked_by_path[path]) / len(blocked_by_path[path])
        if blocked_by_path[path] else None
        for path in sorted(supported_paths)
    }

rates = slice_rates(cases, supported_paths)
for path, rate in rates.items():
    print(f"{path}_false_rejection_rate:", rate)

investigation_required = any(rate is None or rate > 0.25 for rate in rates.values())
print("investigation_required:", investigation_required)
print("next: repair path or fill coverage" if investigation_required else "next: review passing fixture results")
Output
screen_reader_path_false_rejection_rate: 0.5
web_console_false_rejection_rate: 0.0
investigation_required: True
next: repair path or fill coverage

Store this report with the dataset version, test limitations, accessibility review, and remediation owner. It complements oversight because the escalation route must itself be usable by the people who need it.

Make human oversight an executable path

"" is too vague for a release review. A meaningful path names the effects that require approval, the evidence a reviewer sees, the treatment of conflicts, and the route an affected actor can use to challenge an outcome.

Three common engineering terms describe different arrangements. They're not the AI Act's legal classification categories:

  • Human-in-the-loop (HITL): A designated decision waits for human review before execution. This workflow requires it for production promotion.
  • Human-on-the-loop (HOTL): The system acts within agreed boundaries while supervisors monitor and can intervene. Set detection and response limits to fit the risk, then test that operators can stop or reverse an effect within those limits.
  • Human-in-command (HIC): People govern the system's purpose, policies, permitted scope, and continued operation.

Article 14 instead requires effective oversight commensurate with risk, autonomy, and context for covered high-risk systems. It includes appropriate abilities to understand limitations, recognize automation bias, interpret outputs, override decisions, and intervene or stop safely. Assigning a person's name isn't enough.[9]

At C17, each stage has to say what the assistant may do and what a person must decide.

  1. The assistant may answer eval and release-policy questions.
  2. It may propose a promotion with cited eval evidence.
  3. Every production promotion requires approval. Untrusted instructions add security review; targets above 10% add exception review. Both can be required at once.
  4. The reviewer approves or rejects the effect with a reason code.
  5. A model owner may appeal; a second reviewer receives the original record and decision basis.
The C17 proposal at 10 percent of traffic needs release approval and a security review. The figure then splits into two columns. On the left, when both approvals are present, the executor rechecks that they still cover this exact proposal, and then traffic moves. On the right, a missing or denied approval holds the change, and a challenge to that hold goes to an independent reviewer, who sees the original record and why it was decided, and upholds or corrects it.
At 10%, C17 needs release approval and a security review. The executor rechecks an approved proposal before committing. A model owner can challenge a denial, and a second reviewer receives the original record and decision basis.

The gate runs before traffic moves. Record who decided and why, and provide a usable challenge path. The approval and appeal route described here applies to this workflow. Assess the AI Act obligations for each use case before extending the design to other systems. Reviewers also need time, relevant expertise, and authority to disagree; a rushed rubber stamp adds little protection.

Encode that route as a small function. The proposal below must reach review before any effect executes, then preserve the decision and appeal state.

10-route-a-promotion-through-oversight.py
from dataclasses import dataclass

@dataclass(frozen=True)
class PromotionProposal:
    target_percent: int
    saw_untrusted_instruction: bool

    def __post_init__(self):
        if type(self.target_percent) is not int or not 1 <= self.target_percent <= 100:
            raise ValueError("target_percent must be an integer from 1 to 100")
        if type(self.saw_untrusted_instruction) is not bool:
            raise ValueError("instruction flag must be Boolean")

def required_reviews(proposal: PromotionProposal) -> set[str]:
    reviews = {"RELEASE_APPROVAL"}
    if proposal.saw_untrusted_instruction:
        reviews.add("SECURITY_REVIEW")
    if proposal.target_percent > 10:
        reviews.add("EXCEPTION_REVIEW")
    return reviews

def review_state(proposal, decisions):
    # Decisions come from authenticated reviewers in the real service.
    needed = required_reviews(proposal)
    if any(value not in {"APPROVED", "DENIED"} for value in decisions.values()):
        raise ValueError("unknown review decision")
    if any(decisions.get(name) == "DENIED" for name in needed):
        return "DENIED_APPEAL_AVAILABLE"
    if not needed <= decisions.keys():
        return "HELD_MISSING_REVIEW"
    return "APPROVED_FOR_EXECUTOR"

proposal = PromotionProposal(target_percent=10, saw_untrusted_instruction=True)
print("required reviews:", sorted(required_reviews(proposal)))
print("before review:", review_state(proposal, {}))
print("after denial:", review_state(proposal, {"SECURITY_REVIEW": "DENIED"}))
print("after approval:", review_state(proposal, {
    "RELEASE_APPROVAL": "APPROVED", "SECURITY_REVIEW": "APPROVED",
}))
Output
required reviews: ['RELEASE_APPROVAL', 'SECURITY_REVIEW']
before review: HELD_MISSING_REVIEW
after denial: DENIED_APPEAL_AVAILABLE
after approval: APPROVED_FOR_EXECUTOR

APPROVED_FOR_EXECUTOR isn't an executed effect. A real executor must bind approvals to the active operation and exact candidate, environment, traffic target, policy, and evidence versions, verify reviewer authority and freshness, and recheck before committing. The saw_untrusted_instruction flag is a fixture, not a reliable detector: server-side authorization remains necessary even when no attack is recognized. For a legally high-risk workflow, map oversight requirements to the applicable duties and roles.[9]

Gate releases on an evidence package

All these artifacts matter only when the deployment pipeline reads them. A release candidate can carry a folder full of documents and still ship an unsafe effect if no executable check consumes them. Make the candidate fail when required evidence is missing or a red-team finding remains open.

For the model-promotion assistant, the minimal package answers eight review questions:

EvidenceWhy it exists
Workflow memoRecords intended use, people affected, and dated review route
Risk rowConnects harm to control, owner, residual risk, and next review
System cardIdentifies shipped policy, model, tools, limitations, and tests
Dataset recordMakes evaluation cases and labels reproducible
Audit replayShows a proposed promotion was gated and preserved correctly
Red-team reportBlocks release when an unsafe effect remains
Accessibility and slice reportFinds valid requests blocked on supported paths
Oversight pathShows approval and appeal behavior exists

The final check takes reviewed artifact references, not a list of filenames. Each result must belong to this candidate and the exact deployment configuration. A version label can stay unchanged while someone adds a tool or replaces the policy.

Here the host hashes a small synthetic manifest and compares that digest with each review. In production the manifest must bind all relevant immutable artifacts, including model, prompts, policy, tools, evaluation inputs, and environment. Reviews and finding counts must come from authenticated validators and reviewers bound to that manifest. This example assumes that provenance has already been verified; hashing self-reported claims wouldn't verify them. Passing makes the candidate eligible for release approval, without deploying it or establishing legal compliance.

11-gate-a-governed-release.py
from dataclasses import dataclass
import hashlib
import json

REQUIRED_EVIDENCE = {
    "workflow_memo",
    "risk_register_row",
    "system_card",
    "dataset_record",
    "audit_replay",
    "red_team_report",
    "accessibility_and_slice_report",
    "oversight_runbook",
}

@dataclass(frozen=True)
class EvidenceReview:
    candidate: str
    deployment_digest: str
    artifact_ref: str
    accepted: bool

def manifest_digest(manifest: dict[str, object]) -> str:
    # One local Python encoding. Production manifests need a specified format.
    payload = json.dumps(manifest, sort_keys=True).encode("utf-8")
    return hashlib.sha256(payload).hexdigest()

def release_decision(
    candidate: str,
    deployment_digest: str,
    evidence: dict[str, EvidenceReview],
    unsafe_effects: int,
    open_accessibility_findings: int,
    legal_signoff: bool,
) -> tuple[bool, list[str]]:
    if not isinstance(candidate, str) or not candidate.strip():
        raise ValueError("candidate identity required")
    if not isinstance(deployment_digest, str) or len(deployment_digest) != 64 or any(
        char not in "0123456789abcdef" for char in deployment_digest
    ):
        raise ValueError("host deployment digest must be 64 lowercase hexadecimal characters")
    if any(type(n) is not int or n < 0 for n in (unsafe_effects, open_accessibility_findings)):
        raise ValueError("finding counts must be nonnegative integers")
    if type(legal_signoff) is not bool:
        raise ValueError("legal signoff must be Boolean")
    reasons: list[str] = []
    missing = sorted(REQUIRED_EVIDENCE - evidence.keys())
    if missing:
        reasons.append(f"missing evidence: {', '.join(missing)}")
    for name in sorted(REQUIRED_EVIDENCE & evidence.keys()):
        item = evidence[name]
        if (
            item.candidate != candidate or item.deployment_digest != deployment_digest
            or not isinstance(item.artifact_ref, str) or not item.artifact_ref.strip()
            or item.accepted is not True
        ):
            reasons.append(f"unaccepted or mismatched evidence: {name}")
    if not legal_signoff:
        reasons.append("required legal signoff missing")
    if unsafe_effects:
        reasons.append(f"unsafe effects remain: {unsafe_effects}")
    if open_accessibility_findings:
        reasons.append(f"accessibility findings remain: {open_accessibility_findings}")
    return not reasons, reasons

candidate = "promotion-assistant-7.2"
deployment = {"model": "mock-model-v1", "prompt": "release-prompt-v3",
              "policy": "promotion-gate-v7", "tools": ["propose_promotion"],
              "eval": "frozen-promotion-eval-v2", "environment": "mock-prod"}
expected_digest = manifest_digest(deployment)
reviewed = {name: EvidenceReview(candidate, expected_digest, f"reviewed-artifacts/{name}/v7", True)
            for name in REQUIRED_EVIDENCE}
draft_evidence = {name: item for name, item in reviewed.items() if name != "dataset_record"}
draft_ready, draft_reasons = release_decision(
    candidate,
    expected_digest,
    draft_evidence,
    unsafe_effects=1,
    open_accessibility_findings=1,
    legal_signoff=True,
)
print("draft_ready:", draft_ready)
print("draft_reasons:", draft_reasons)

reviewed_ready, reviewed_reasons = release_decision(
    candidate,
    expected_digest,
    reviewed,
    unsafe_effects=0,
    open_accessibility_findings=0,
    legal_signoff=True,
)
print("reviewed_ready:", reviewed_ready)
print("reviewed_reasons:", reviewed_reasons)

changed = {**deployment, "policy": "promotion-gate-v8"}
changed_ready, _ = release_decision(
    candidate, manifest_digest(changed), reviewed,
    unsafe_effects=0, open_accessibility_findings=0, legal_signoff=True,
)
print("changed_configuration_ready:", changed_ready)
Output
draft_ready: False
draft_reasons: ['missing evidence: dataset_record', 'unsafe effects remain: 1', 'accessibility findings remain: 1']
reviewed_ready: True
reviewed_reasons: []
changed_configuration_ready: False

dataset_record is release evidence, not administrative decoration. Without its provenance, labels, split, and retention rule, a reviewer can't tell where the score came from or reproduce it safely.

The last line keeps the candidate name but changes the policy. The new manifest digest rejects the old reviews, so affected evidence has to be collected again and the previous decision record stays in place. The artifact list and thresholds belong to this workflow. Another system needs its own package, built from its harms, users, and duties.

Hold this promotion until the records name an owner, a tested control, and a digest that still matches the candidate. That package identifies sensitive data and retention as risks. It does not yet bound what training can reveal about one person.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A staff-scheduling assistant has checked manages_workers=true, talks_to_people=true and can_execute_effects=false; its other intake flags are checked false. A document summarizer has an unanswered worker-management flag. Which local triage follows the taught rule?
2.A complete risk row includes a named owner, a nonempty evidence list, residual-risk text and next_review_on='2026-11-06'. The assessment date is also 2026-11-06. All other shape checks pass. What does the taught validator return?
3.An evaluation report is reviewed against dataset version D4 and labeling guide L2. Someone replaces its dataset record with D5/L3 while leaving the score and report unchanged. Which response preserves reproducibility?
4.A reviewer investigates denied release request R86 involving a private retrieved report. Which controlled record best supports replay of the original decision?
5.Eight attack traces contain two forbidden effects; five benign eligible requests contain one false rejection. The gate blocks on any unsafe effect or FRR above 0.10. Which tuple gives ASR, FRR, unsafe-effect predicate and friction predicate?
6.Thirty web cases contain one false rejection; six keyboard-only cases contain two. Investigation is required when any supported-path FRR exceeds 0.25. Which tuple gives aggregate FRR, keyboard-path FRR and path decision?
7.Proposal A targets 6% with an untrusted instruction; proposal B targets 14% with no such flag. Which review sets follow the taught local rule before any required denial opens an independent appeal?
8.Candidate C44 keeps its name and model, but its tool permission list changes after all eight evidence reviews were accepted for the old manifest. Findings remain zero. Can the gate reuse those old reviews to approve this configuration?
9.A lender replaces a GPAI model in its creditworthiness application with a qualifying open-source GPAI model. Which pairing preserves the distinct application and model duties?
10.A log administrator edits a recorded effect, recomputes the chain and overwrites its only stored anchor. The new head matches that anchor. Which check and conclusion are justified?

10 questions remaining.

References

Bias and Fairness in Large Language Models: A Survey

Gallegos, I. O., Rossi, R. A., Barrow, J., et al. · 2024

https://aclanthology.org/2024.cl-3.8/

General Data Protection Regulation, Article 9: Processing of special categories of personal data

European Parliament and Council of the European Union · 2016

https://gdpr-info.eu/art-9-gdpr/

Model Cards for Model Reporting

Mitchell, M., Wu, S., Zaldivar, A., et al. · 2019 · FAT* 2019

https://arxiv.org/abs/1810.03993

Datasheets for Datasets

Gebru, T., Morgenstern, J., Vecchione, B., et al. · 2021 · Communications of the ACM

https://arxiv.org/abs/1803.09010

OWASP Top 10 for LLM Applications 2025

OWASP Foundation · 2025

https://genai.owasp.org/resource/owasp-top-10-for-llm-applications-2025/

Artificial Intelligence Risk Management Framework (AI RMF 1.0)

National Institute of Standards and Technology · 2023

https://www.nist.gov/itl/ai-risk-management-framework

Artificial Intelligence Risk Management Framework: Generative Artificial Intelligence Profile

National Institute of Standards and Technology · 2024 · NIST

https://www.nist.gov/publications/artificial-intelligence-risk-management-framework-generative-artificial-intelligence

ISO/IEC 42001:2023: Artificial intelligence management system

International Organization for Standardization · 2023

https://www.iso.org/standard/42001

EU AI Act: consolidated text as amended on 27 July 2026

European Parliament and Council of the European Union · 2026

https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:02024R1689-20260727

AI Act

European Commission · 2026

https://digital-strategy.ec.europa.eu/en/policies/regulatory-framework-ai

Regulation (EU) 2026/1744: Digital Omnibus on AI

European Parliament and Council of the European Union · 2026

https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32026R1744

Handling sensitive data

OpenTelemetry Authors · 2026

https://opentelemetry.io/docs/security/handling-sensitive-data/

Red Teaming Language Models with Language Models.

Perez, E., et al. · 2022 · EMNLP 2022

https://arxiv.org/abs/2202.03286

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.