+ Scoring a real Hugging Face card, then asking what the EU AI Act actually needs.
+ Watch the full four-minute walkthrough, including running a contract and the metrics still missing an evaluator.
+
+
+
+
**AICertify is the open execution and evidence layer for AI governance.** Describe an AI system in a contract, supply the facts only your organisation can know, attach or compute measured metrics, evaluate that evidence against versioned [GOPAL](https://github.com/Principled-Evolution/gopal) policies through [Open Policy Agent](https://www.openpolicyagent.org/), and generate dated PDF, Markdown, JSON, or HTML reports.
**The goal is simple: move from “we have an AI policy” to evidence another engineer, auditor, or risk team can inspect and reproduce.**
diff --git a/aicertify/opa_core/extraction.py b/aicertify/opa_core/extraction.py
index 1cd2dcc..f8d96ef 100644
--- a/aicertify/opa_core/extraction.py
+++ b/aicertify/opa_core/extraction.py
@@ -359,10 +359,19 @@ def extract_policy_results_with_schema(
return extracted_policies
except Exception as e:
- logger.error(f"Error validating OPA results against schema: {e}")
- # Dont try to recover using a more flexible approach
- logger.warning(
- "Failed to validate OPA results against schema, returning empty list"
+ # Expected, and the common case. This path validates against the
+ # `report_output` schema, which four of gopal's policies define. Every
+ # other policy fails it, and the caller then reads the decision rules
+ # instead (see the note on extract_results_from_packages below).
+ #
+ # Logged at error until the #78 fallback landed, which meant a normal
+ # successful evaluation printed a pydantic validation report per policy
+ # and looked to a first-time user like a crash. The behaviour was
+ # already correct; only the log level was left behind.
+ logger.debug(
+ "No report_output schema match (%s); the caller falls back to "
+ "decision rules.",
+ e.__class__.__name__,
)
return []
diff --git a/aicertify/opa_policies b/aicertify/opa_policies
index 4321e7a..0936496 160000
--- a/aicertify/opa_policies
+++ b/aicertify/opa_policies
@@ -1 +1 @@
-Subproject commit 4321e7a4fc28061610d345319d6c778e877bbd85
+Subproject commit 09364967d494b4137b8c492135c9c7738964e601
diff --git a/docs/demo/aicertify-animated.svg b/docs/demo/aicertify-animated.svg
new file mode 100644
index 0000000..5c4765e
--- /dev/null
+++ b/docs/demo/aicertify-animated.svg
@@ -0,0 +1,407 @@
+
diff --git a/docs/demo/aicertify-short.cast b/docs/demo/aicertify-short.cast
new file mode 100644
index 0000000..c827fe8
--- /dev/null
+++ b/docs/demo/aicertify-short.cast
@@ -0,0 +1,264 @@
+{"version": 2, "width": 100, "height": 30, "idle_time_limit": 3.0, "title": "AICertify: 150 declared, 14 measured"}
+[0.6, "o", "$ "]
+[0.6857, "o", "a"]
+[0.7481, "o", "i"]
+[0.8326, "o", "c"]
+[0.8772, "o", "e"]
+[0.9482, "o", "r"]
+[0.993, "o", "t"]
+[1.0405, "o", "i"]
+[1.1202, "o", "f"]
+[1.1796, "o", "y"]
+[1.2503, "o", " "]
+[1.3196, "o", "s"]
+[1.3809, "o", "c"]
+[1.45, "o", "o"]
+[1.5289, "o", "r"]
+[1.5857, "o", "e"]
+[1.6409, "o", "-"]
+[1.7185, "o", "c"]
+[1.8039, "o", "a"]
+[1.8679, "o", "r"]
+[1.9161, "o", "d"]
+[1.9636, "o", " "]
+[2.0197, "o", "b"]
+[2.0779, "o", "e"]
+[2.1447, "o", "r"]
+[2.2007, "o", "t"]
+[2.2876, "o", "-"]
+[2.3404, "o", "b"]
+[2.4044, "o", "a"]
+[2.4619, "o", "s"]
+[2.5101, "o", "e"]
+[2.5927, "o", "-"]
+[2.661, "o", "u"]
+[2.7296, "o", "n"]
+[2.7831, "o", "c"]
+[2.8522, "o", "a"]
+[2.8905, "o", "s"]
+[2.9325, "o", "e"]
+[2.9761, "o", "d"]
+[3.3961, "o", "\r\n"]
+[5.4561, "o", "\r\nbert-base-uncased\r\n completeness 0.49 (threshold 0.8)\r\n"]
+[5.5461, "o", " quality 0.66\r\n BELOW THRESHOLD\r\n\r\n"]
+[5.6361, "o", " intended_use 1.00 ####################\r\n training_data 0.67 #############\r\n caveats_recommendations 0.50 ##########\r\n"]
+[5.7261, "o", " factors 0.50 ##########\r\n metrics 0.50 ##########\r\n quantitative_analyses 0.50 ##########\r\n"]
+[5.8161, "o", " ethical_considerations 0.33 #######\r\n model_details 0.33 ######\r\n evaluation_data 0.00 \r\n"]
+[5.9061, "o", "\r\nA model card answers part of what Annex IV asks and then stops.\r\nThis is the documentation-completeness gap, not a compliance verdict.\r\n"]
+[5.9961, "o", "\r\nscored by GOPAL 2.0.0 (global/v1/documentation/model_card_score), via aicertify 0.7.0\r\n"]
+[6.8861, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0\u001b[0m\u001b8"]
+[6.9141, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0.\u001b[0m\u001b8"]
+[6.9421, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0.4\u001b[0m\u001b8"]
+[6.9701, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0.49\u001b[0m\u001b8"]
+[7.3381, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mB\u001b[0m\u001b8"]
+[7.3661, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBE\u001b[0m\u001b8"]
+[7.3941, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBEL\u001b[0m\u001b8"]
+[7.4221, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELO\u001b[0m\u001b8"]
+[7.4501, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW\u001b[0m\u001b8"]
+[7.4781, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW \u001b[0m\u001b8"]
+[7.5061, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW T\u001b[0m\u001b8"]
+[7.5341, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW TH\u001b[0m\u001b8"]
+[7.5621, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THR\u001b[0m\u001b8"]
+[7.5901, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRE\u001b[0m\u001b8"]
+[7.6181, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRES\u001b[0m\u001b8"]
+[7.6461, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESH\u001b[0m\u001b8"]
+[7.6741, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESHO\u001b[0m\u001b8"]
+[7.7021, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESHOL\u001b[0m\u001b8"]
+[7.7301, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESHOLD\u001b[0m\u001b8"]
+[13.4981, "o", "$ "]
+[13.5533, "o", "#"]
+[13.6042, "o", " "]
+[13.6675, "o", "s"]
+[13.7216, "o", "o"]
+[13.7668, "o", " "]
+[13.8414, "o", "w"]
+[13.901, "o", "h"]
+[13.98, "o", "a"]
+[14.036, "o", "t"]
+[14.2622, "o", " "]
+[14.3358, "o", "d"]
+[14.407, "o", "o"]
+[14.452, "o", "e"]
+[14.5034, "o", "s"]
+[14.656, "o", " "]
+[14.7291, "o", "t"]
+[14.8083, "o", "h"]
+[14.8639, "o", "e"]
+[14.9825, "o", " "]
+[15.0553, "o", "E"]
+[15.1175, "o", "U"]
+[15.2782, "o", " "]
+[15.3602, "o", "A"]
+[15.4087, "o", "I"]
+[15.4983, "o", " "]
+[15.5678, "o", "A"]
+[15.6207, "o", "c"]
+[15.7021, "o", "t"]
+[15.8514, "o", " "]
+[15.9182, "o", "a"]
+[15.9948, "o", "c"]
+[16.0738, "o", "t"]
+[16.1586, "o", "u"]
+[16.2018, "o", "a"]
+[16.2789, "o", "l"]
+[16.3431, "o", "k"]
+[16.7652, "o", "\b \b"]
+[16.9292, "o", "l"]
+[17.0206, "o", "y"]
+[17.1123, "o", " "]
+[17.1793, "o", "a"]
+[17.2189, "o", "s"]
+[17.2605, "o", "k"]
+[17.304, "o", " "]
+[17.3619, "o", "f"]
+[17.432, "o", "o"]
+[17.4911, "o", "r"]
+[17.5342, "o", "?"]
+[17.9542, "o", "\r\n"]
+[18.6142, "o", "$ "]
+[18.6746, "o", "a"]
+[18.7332, "o", "i"]
+[18.8196, "o", "c"]
+[18.8938, "o", "e"]
+[18.9528, "o", "r"]
+[19.0121, "o", "t"]
+[19.0838, "o", "i"]
+[19.1662, "o", "f"]
+[19.2485, "o", "y"]
+[19.4441, "o", " "]
+[19.5332, "o", "e"]
+[19.625, "o", "x"]
+[19.6876, "o", "p"]
+[19.7399, "o", "l"]
+[19.8257, "o", "a"]
+[19.8648, "o", "i"]
+[19.9051, "o", "n"]
+[19.9678, "o", " "]
+[20.0131, "o", "e"]
+[20.075, "o", "u"]
+[20.1434, "o", "_"]
+[20.1838, "o", "a"]
+[20.2341, "o", "i"]
+[20.2763, "o", "_"]
+[20.3213, "o", "a"]
+[20.3978, "o", "c"]
+[20.4606, "o", "t"]
+[20.5349, "o", " "]
+[20.587, "o", "|"]
+[20.7452, "o", " "]
+[20.7897, "o", "h"]
+[20.8367, "o", "e"]
+[20.8764, "o", "a"]
+[20.9218, "o", "d"]
+[20.9953, "o", " "]
+[21.0738, "o", "-"]
+[21.1456, "o", "1"]
+[21.2333, "o", "2"]
+[21.6533, "o", "\r\n"]
+[23.7133, "o", "\r\neu_ai_act \u2014 29 policies\r\n\r\n"]
+[23.8033, "o", "Fields you must declare (150)\r\n No evaluator can observe these. They are facts about your system,\r\n your process, or your paperwork, so you assert them in the contract.\r\n"]
+[23.8933, "o", "\r\n accuracy.declared_in_instructions technical_robustness\r\n accuracy.metrics_declared technical_robustness\r\n"]
+[23.9833, "o", " assessment.completed conformity_assessment\r\n assessment.harmonised_standards_applied conformity_assessment\r\n assessment.notified_body_involved conformity_assessment\r\n"]
+[24.7733, "o", "\u001b7\u001b[9A\r\u001b[25C\u001b[43;30m1\u001b[0m\u001b8"]
+[24.8013, "o", "\u001b7\u001b[9A\r\u001b[25C\u001b[43;30m15\u001b[0m\u001b8"]
+[24.8293, "o", "\u001b7\u001b[9A\r\u001b[25C\u001b[43;30m150\u001b[0m\u001b8"]
+[27.7973, "o", "$ "]
+[27.8433, "o", "a"]
+[27.9297, "o", "i"]
+[28.0189, "o", "c"]
+[28.07, "o", "e"]
+[28.1519, "o", "r"]
+[28.219, "o", "t"]
+[28.2995, "o", "i"]
+[28.3838, "o", "f"]
+[28.4619, "o", "y"]
+[28.519, "o", " "]
+[28.6104, "o", "e"]
+[28.6649, "o", "x"]
+[28.7462, "o", "p"]
+[28.7963, "o", "l"]
+[28.8422, "o", "a"]
+[28.9155, "o", "i"]
+[28.9987, "o", "n"]
+[29.1715, "o", " "]
+[29.2417, "o", "e"]
+[29.281, "o", "u"]
+[29.321, "o", "_"]
+[29.4052, "o", "a"]
+[29.447, "o", "i"]
+[29.4953, "o", "_"]
+[29.5566, "o", "a"]
+[29.5965, "o", "c"]
+[29.6743, "o", "t"]
+[29.8469, "o", " "]
+[29.929, "o", "|"]
+[30.0082, "o", " "]
+[30.0598, "o", "g"]
+[30.125, "o", "r"]
+[30.1909, "o", "e"]
+[30.2435, "o", "p"]
+[30.3152, "o", " "]
+[30.3924, "o", "-"]
+[30.4637, "o", "A"]
+[30.5366, "o", "8"]
+[30.5898, "o", " "]
+[30.6284, "o", "\""]
+[30.6883, "o", "p"]
+[30.7435, "o", "r"]
+[30.7848, "o", "o"]
+[30.8245, "o", "d"]
+[30.9021, "o", "u"]
+[30.9473, "o", "c"]
+[30.9886, "o", "e"]
+[31.0293, "o", "d"]
+[31.0939, "o", " "]
+[31.1713, "o", "b"]
+[31.2282, "o", "y"]
+[31.2966, "o", " "]
+[31.368, "o", "e"]
+[31.4421, "o", "v"]
+[31.5281, "o", "a"]
+[31.5911, "o", "l"]
+[31.6516, "o", "u"]
+[31.7086, "o", "a"]
+[31.7847, "o", "t"]
+[31.8314, "o", "o"]
+[31.8898, "o", "r"]
+[31.9389, "o", "s"]
+[32.004, "o", "\""]
+[32.424, "o", "\r\n"]
+[34.484, "o", "Fields produced by evaluators (14)\r\n Computed at evaluation time from your interactions. Do not hand-write\r\n these: asserting your own fairness score defeats the point.\r\n"]
+[34.574, "o", "\r\n metrics.content_safety.score eu_fairness [content_safety evaluator]\r\n metrics.emotion_recognition.combined_score emotion_recognition\r\n"]
+[34.664, "o", " metrics.emotion_recognition.educational_context emotion_recognition\r\n metrics.emotion_recognition.workplace_context emotion_recognition\r\n metrics.fairness.gender_bias eu_fairness [fairness evaluator]\r\n"]
+[35.454, "o", "\u001b7\u001b[9A\r\u001b[31C\u001b[43;30m1\u001b[0m\u001b8"]
+[35.482, "o", "\u001b7\u001b[9A\r\u001b[31C\u001b[43;30m14\u001b[0m\u001b8"]
+[35.85, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30md\u001b[0m\u001b8"]
+[35.878, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mde\u001b[0m\u001b8"]
+[35.906, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdef\u001b[0m\u001b8"]
+[35.934, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefe\u001b[0m\u001b8"]
+[35.962, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefea\u001b[0m\u001b8"]
+[35.99, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeat\u001b[0m\u001b8"]
+[36.018, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats\u001b[0m\u001b8"]
+[36.046, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats \u001b[0m\u001b8"]
+[36.074, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats t\u001b[0m\u001b8"]
+[36.102, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats th\u001b[0m\u001b8"]
+[36.13, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the\u001b[0m\u001b8"]
+[36.158, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the \u001b[0m\u001b8"]
+[36.186, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the p\u001b[0m\u001b8"]
+[36.214, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the po\u001b[0m\u001b8"]
+[36.242, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the poi\u001b[0m\u001b8"]
+[36.27, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the poin\u001b[0m\u001b8"]
+[36.298, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the point\u001b[0m\u001b8"]
+[42.066, "o", "$ "]
+[42.066, "o", "\u2588"]
+[42.596, "o", "\b \b"]
+[43.126, "o", "\u2588"]
+[43.656, "o", "\b \b"]
+[44.186, "o", "\u2588"]
+[44.716, "o", "\b \b"]
+[45.246, "o", "\u2588"]
+[45.776, "o", "\b \b"]
+[46.306, "o", "\u2588"]
+[46.836, "o", "\b \b"]
+[47.366, "o", "\u2588"]
+[47.896, "o", "\b \b"]
diff --git a/docs/demo/aicertify.cast b/docs/demo/aicertify.cast
new file mode 100644
index 0000000..84700db
--- /dev/null
+++ b/docs/demo/aicertify.cast
@@ -0,0 +1,534 @@
+{"version": 2, "width": 100, "height": 30, "idle_time_limit": 3.0, "title": "AICertify: what it measures, and what it cannot"}
+[0.6, "o", "$ "]
+[0.663, "o", "a"]
+[0.7371, "o", "i"]
+[0.8269, "o", "c"]
+[0.8667, "o", "e"]
+[0.9498, "o", "r"]
+[1.0368, "o", "t"]
+[1.1269, "o", "i"]
+[1.1942, "o", "f"]
+[1.2494, "o", "y"]
+[1.4363, "o", " "]
+[1.5075, "o", "s"]
+[1.5812, "o", "c"]
+[1.6367, "o", "o"]
+[1.7021, "o", "e"]
+[2.1254, "o", "\b \b"]
+[2.3135, "o", "r"]
+[2.3604, "o", "e"]
+[2.4345, "o", "-"]
+[2.5228, "o", "c"]
+[2.6116, "o", "a"]
+[2.7002, "o", "r"]
+[2.7515, "o", "d"]
+[2.8069, "o", " "]
+[2.8613, "o", "b"]
+[2.918, "o", "e"]
+[2.9962, "o", "r"]
+[3.037, "o", "t"]
+[3.0848, "o", "-"]
+[3.1355, "o", "b"]
+[3.1939, "o", "a"]
+[3.2771, "o", "s"]
+[3.3624, "o", "e"]
+[3.4141, "o", "-"]
+[3.4592, "o", "u"]
+[3.5188, "o", "n"]
+[3.588, "o", "c"]
+[3.6299, "o", "a"]
+[3.6839, "o", "s"]
+[3.7495, "o", "e"]
+[3.8127, "o", "d"]
+[4.2327, "o", "\r\n"]
+[6.2927, "o", "\r\nbert-base-uncased\r\n completeness 0.49 (threshold 0.8)\r\n"]
+[6.3827, "o", " quality 0.66\r\n BELOW THRESHOLD\r\n\r\n"]
+[6.4727, "o", " intended_use 1.00 ####################\r\n training_data 0.67 #############\r\n caveats_recommendations 0.50 ##########\r\n"]
+[6.5627, "o", " factors 0.50 ##########\r\n metrics 0.50 ##########\r\n quantitative_analyses 0.50 ##########\r\n"]
+[6.6527, "o", " ethical_considerations 0.33 #######\r\n model_details 0.33 ######\r\n evaluation_data 0.00 \r\n"]
+[6.7427, "o", "\r\nA model card answers part of what Annex IV asks and then stops.\r\nThis is the documentation-completeness gap, not a compliance verdict.\r\n"]
+[6.8327, "o", "\r\nscored by GOPAL 2.0.0 (global/v1/documentation/model_card_score), via aicertify 0.7.0\r\n"]
+[7.7227, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0\u001b[0m\u001b8"]
+[7.7507, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0.\u001b[0m\u001b8"]
+[7.7787, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0.4\u001b[0m\u001b8"]
+[7.8067, "o", "\u001b7\u001b[18A\r\u001b[16C\u001b[43;30m0.49\u001b[0m\u001b8"]
+[8.1747, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mB\u001b[0m\u001b8"]
+[8.2027, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBE\u001b[0m\u001b8"]
+[8.2307, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBEL\u001b[0m\u001b8"]
+[8.2587, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELO\u001b[0m\u001b8"]
+[8.2867, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW\u001b[0m\u001b8"]
+[8.3147, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW \u001b[0m\u001b8"]
+[8.3427, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW T\u001b[0m\u001b8"]
+[8.3707, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW TH\u001b[0m\u001b8"]
+[8.3987, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THR\u001b[0m\u001b8"]
+[8.4267, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRE\u001b[0m\u001b8"]
+[8.4547, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRES\u001b[0m\u001b8"]
+[8.4827, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESH\u001b[0m\u001b8"]
+[8.5107, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESHO\u001b[0m\u001b8"]
+[8.5387, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESHOL\u001b[0m\u001b8"]
+[8.5667, "o", "\u001b7\u001b[16A\r\u001b[2C\u001b[43;30mBELOW THRESHOLD\u001b[0m\u001b8"]
+[14.3347, "o", "$ "]
+[14.3728, "o", "#"]
+[14.4216, "o", " "]
+[14.4751, "o", "s"]
+[14.5457, "o", "o"]
+[14.6038, "o", " "]
+[14.6894, "o", "w"]
+[14.7391, "o", "h"]
+[14.8281, "o", "a"]
+[14.8792, "o", "t"]
+[14.9428, "o", " "]
+[15.0337, "o", "d"]
+[15.1247, "o", "o"]
+[15.2054, "o", "e"]
+[15.261, "o", "s"]
+[15.3275, "o", " "]
+[15.3931, "o", "t"]
+[15.4326, "o", "h"]
+[15.5002, "o", "e"]
+[15.5741, "o", " "]
+[15.6338, "o", "E"]
+[15.6756, "o", "U"]
+[15.7291, "o", " "]
+[15.8146, "o", "A"]
+[15.8902, "o", "I"]
+[15.9434, "o", " "]
+[16.0148, "o", "A"]
+[16.0948, "o", "c"]
+[16.146, "o", "t"]
+[16.1997, "o", " "]
+[16.2682, "o", "a"]
+[16.308, "o", "c"]
+[16.3465, "o", "t"]
+[16.3926, "o", "u"]
+[16.4409, "o", "a"]
+[16.5198, "o", "l"]
+[16.58, "o", "l"]
+[16.62, "o", "y"]
+[16.7305, "o", " "]
+[16.8186, "o", "a"]
+[16.8661, "o", "s"]
+[16.936, "o", "k"]
+[17.004, "o", " "]
+[17.0739, "o", "f"]
+[17.1166, "o", "o"]
+[17.1599, "o", "r"]
+[17.2375, "o", "?"]
+[17.6575, "o", "\r\n"]
+[18.3175, "o", "$ "]
+[18.3736, "o", "a"]
+[18.4514, "o", "i"]
+[18.5393, "o", "c"]
+[18.6178, "o", "e"]
+[18.6876, "o", "r"]
+[18.7719, "o", "t"]
+[18.8375, "o", "i"]
+[18.8849, "o", "f"]
+[18.948, "o", "y"]
+[19.12, "o", " "]
+[19.2032, "o", "e"]
+[19.2881, "o", "x"]
+[19.3525, "o", "p"]
+[19.4241, "o", "l"]
+[19.4642, "o", "a"]
+[19.5344, "o", "i"]
+[19.6001, "o", "n"]
+[19.6664, "o", " "]
+[19.727, "o", "e"]
+[19.7846, "o", "u"]
+[19.8725, "o", "_"]
+[19.9425, "o", "a"]
+[19.9966, "o", "i"]
+[20.0526, "o", "_"]
+[20.1064, "o", "s"]
+[20.5153, "o", "\b \b"]
+[20.7183, "o", "a"]
+[20.7907, "o", "c"]
+[20.8689, "o", "t"]
+[21.0413, "o", " "]
+[21.0899, "o", "|"]
+[21.169, "o", " "]
+[21.2214, "o", "h"]
+[21.2811, "o", "e"]
+[21.3388, "o", "a"]
+[21.3854, "o", "d"]
+[21.4432, "o", " "]
+[21.5249, "o", "-"]
+[21.5912, "o", "1"]
+[21.646, "o", "2"]
+[22.066, "o", "\r\n"]
+[24.126, "o", "\r\neu_ai_act \u2014 29 policies\r\n\r\n"]
+[24.216, "o", "Fields you must declare (150)\r\n No evaluator can observe these. They are facts about your system,\r\n your process, or your paperwork, so you assert them in the contract.\r\n"]
+[24.306, "o", "\r\n accuracy.declared_in_instructions technical_robustness\r\n accuracy.metrics_declared technical_robustness\r\n"]
+[24.396, "o", " assessment.completed conformity_assessment\r\n assessment.harmonised_standards_applied conformity_assessment\r\n assessment.notified_body_involved conformity_assessment\r\n"]
+[25.186, "o", "\u001b7\u001b[9A\r\u001b[25C\u001b[43;30m1\u001b[0m\u001b8"]
+[25.214, "o", "\u001b7\u001b[9A\r\u001b[25C\u001b[43;30m15\u001b[0m\u001b8"]
+[25.242, "o", "\u001b7\u001b[9A\r\u001b[25C\u001b[43;30m150\u001b[0m\u001b8"]
+[28.21, "o", "$ "]
+[28.2514, "o", "a"]
+[28.3117, "o", "i"]
+[28.3522, "o", "c"]
+[28.4074, "o", "e"]
+[28.4549, "o", "r"]
+[28.5189, "o", "t"]
+[28.5999, "o", "i"]
+[28.6522, "o", "f"]
+[28.7288, "o", "y"]
+[28.7878, "o", " "]
+[28.855, "o", "e"]
+[28.9184, "o", "x"]
+[28.9987, "o", "p"]
+[29.0767, "o", "l"]
+[29.1447, "o", "a"]
+[29.2237, "o", "i"]
+[29.272, "o", "n"]
+[29.4492, "o", " "]
+[29.5169, "o", "e"]
+[29.5878, "o", "u"]
+[29.6708, "o", "_"]
+[29.7474, "o", "a"]
+[29.8036, "o", "i"]
+[29.8704, "o", "_"]
+[29.9512, "o", "a"]
+[30.0082, "o", "c"]
+[30.0582, "o", "r"]
+[30.4082, "o", "\b \b"]
+[30.6003, "o", "t"]
+[30.687, "o", " "]
+[30.746, "o", "|"]
+[30.9139, "o", " "]
+[30.9762, "o", "g"]
+[31.0533, "o", "r"]
+[31.1333, "o", "e"]
+[31.1925, "o", "p"]
+[31.2527, "o", " "]
+[31.3087, "o", "-"]
+[31.3607, "o", "A"]
+[31.4381, "o", "8"]
+[31.6029, "o", " "]
+[31.6572, "o", "\""]
+[31.7332, "o", "p"]
+[31.8124, "o", "r"]
+[31.8912, "o", "o"]
+[31.9601, "o", "d"]
+[32.0083, "o", "u"]
+[32.0643, "o", "c"]
+[32.1034, "o", "e"]
+[32.1912, "o", "d"]
+[32.3943, "o", " "]
+[32.446, "o", "b"]
+[32.5336, "o", "y"]
+[32.765, "o", " "]
+[32.8458, "o", "e"]
+[32.9108, "o", "v"]
+[32.9632, "o", "a"]
+[33.0423, "o", "l"]
+[33.1011, "o", "u"]
+[33.1434, "o", "a"]
+[33.2204, "o", "t"]
+[33.289, "o", "o"]
+[33.3575, "o", "r"]
+[33.4416, "o", "s"]
+[33.5174, "o", "\""]
+[33.9374, "o", "\r\n"]
+[35.9974, "o", "Fields produced by evaluators (14)\r\n Computed at evaluation time from your interactions. Do not hand-write\r\n these: asserting your own fairness score defeats the point.\r\n"]
+[36.0874, "o", "\r\n metrics.content_safety.score eu_fairness [content_safety evaluator]\r\n metrics.emotion_recognition.combined_score emotion_recognition\r\n"]
+[36.1774, "o", " metrics.emotion_recognition.educational_context emotion_recognition\r\n metrics.emotion_recognition.workplace_context emotion_recognition\r\n metrics.fairness.gender_bias eu_fairness [fairness evaluator]\r\n"]
+[36.9674, "o", "\u001b7\u001b[9A\r\u001b[31C\u001b[43;30m1\u001b[0m\u001b8"]
+[36.9954, "o", "\u001b7\u001b[9A\r\u001b[31C\u001b[43;30m14\u001b[0m\u001b8"]
+[37.3634, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30md\u001b[0m\u001b8"]
+[37.3914, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mde\u001b[0m\u001b8"]
+[37.4194, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdef\u001b[0m\u001b8"]
+[37.4474, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefe\u001b[0m\u001b8"]
+[37.4754, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefea\u001b[0m\u001b8"]
+[37.5034, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeat\u001b[0m\u001b8"]
+[37.5314, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats\u001b[0m\u001b8"]
+[37.5594, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats \u001b[0m\u001b8"]
+[37.5874, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats t\u001b[0m\u001b8"]
+[37.6154, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats th\u001b[0m\u001b8"]
+[37.6434, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the\u001b[0m\u001b8"]
+[37.6714, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the \u001b[0m\u001b8"]
+[37.6994, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the p\u001b[0m\u001b8"]
+[37.7274, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the po\u001b[0m\u001b8"]
+[37.7554, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the poi\u001b[0m\u001b8"]
+[37.7834, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the poin\u001b[0m\u001b8"]
+[37.8114, "o", "\u001b7\u001b[7A\r\u001b[43C\u001b[43;30mdefeats the point\u001b[0m\u001b8"]
+[43.5794, "o", "$ "]
+[43.6385, "o", "a"]
+[43.6917, "o", "i"]
+[43.7727, "o", "c"]
+[43.8634, "o", "e"]
+[43.9259, "o", "r"]
+[43.9854, "o", "t"]
+[44.042, "o", "i"]
+[44.1211, "o", "f"]
+[44.16, "o", "y"]
+[44.3086, "o", " "]
+[44.3876, "o", "i"]
+[44.4676, "o", "n"]
+[44.5508, "o", "i"]
+[44.6033, "o", "t"]
+[44.6617, "o", "-"]
+[44.734, "o", "c"]
+[44.7916, "o", "o"]
+[44.8625, "o", "n"]
+[44.9534, "o", "t"]
+[45.0147, "o", "r"]
+[45.0767, "o", "a"]
+[45.1332, "o", "c"]
+[45.1976, "o", "t"]
+[45.2691, "o", " "]
+[45.3283, "o", "0"]
+[45.6653, "o", "\b \b"]
+[45.8623, "o", "-"]
+[45.9455, "o", "-"]
+[45.9921, "o", "p"]
+[46.0499, "o", "o"]
+[46.1131, "o", "l"]
+[46.1849, "o", "i"]
+[46.2709, "o", "c"]
+[46.3439, "o", "y"]
+[46.5273, "o", " "]
+[46.5701, "o", "e"]
+[46.6335, "o", "u"]
+[46.7152, "o", "_"]
+[46.807, "o", "a"]
+[46.8723, "o", "i"]
+[46.9585, "o", "_"]
+[46.9969, "o", "a"]
+[47.0447, "o", "c"]
+[47.0982, "o", "t"]
+[47.1756, "o", " "]
+[47.2153, "o", ">"]
+[47.2948, "o", " "]
+[47.351, "o", "c"]
+[47.4214, "o", "o"]
+[47.4799, "o", "n"]
+[47.5598, "o", "t"]
+[47.6278, "o", "r"]
+[47.6705, "o", "a"]
+[47.734, "o", "c"]
+[47.8129, "o", "t"]
+[47.8936, "o", "."]
+[47.9715, "o", "j"]
+[48.0437, "o", "s"]
+[48.1216, "o", "o"]
+[48.2074, "o", "n"]
+[48.6274, "o", "\r\n"]
+[51.5874, "o", "$ "]
+[51.6272, "o", "a"]
+[51.7191, "o", "i"]
+[51.7606, "o", "c"]
+[51.8272, "o", "e"]
+[51.8975, "o", "r"]
+[51.9475, "o", "t"]
+[52.0156, "o", "i"]
+[52.1, "o", "f"]
+[52.1779, "o", "y"]
+[52.3785, "o", " "]
+[52.4637, "o", "e"]
+[52.5155, "o", "v"]
+[52.5743, "o", "a"]
+[52.6317, "o", "l"]
+[52.6783, "o", "u"]
+[52.7438, "o", "a"]
+[52.8299, "o", "t"]
+[52.8931, "o", "e"]
+[52.9803, "o", " "]
+[53.0393, "o", "-"]
+[53.1041, "o", "-"]
+[53.1831, "o", "c"]
+[53.2314, "o", "o"]
+[53.2756, "o", "n"]
+[53.346, "o", "t"]
+[53.433, "o", "r"]
+[53.4931, "o", "a"]
+[53.5609, "o", "c"]
+[53.6446, "o", "t"]
+[53.7168, "o", " "]
+[53.7934, "o", "c"]
+[53.8616, "o", "o"]
+[53.9169, "o", "n"]
+[53.9696, "o", "t"]
+[54.0341, "o", "r"]
+[54.1098, "o", "a"]
+[54.1802, "o", "c"]
+[54.2676, "o", "t"]
+[54.3384, "o", "."]
+[54.4258, "o", "j"]
+[54.5065, "o", "s"]
+[54.563, "o", "o"]
+[54.6023, "o", "n"]
+[54.6764, "o", " "]
+[54.7407, "o", "-"]
+[54.7891, "o", "-"]
+[54.8412, "o", "p"]
+[54.8814, "o", "o"]
+[54.9483, "o", "l"]
+[54.9985, "o", "i"]
+[55.0459, "o", "c"]
+[55.1294, "o", "y"]
+[55.3245, "o", " "]
+[55.3683, "o", "e"]
+[55.4425, "o", "u"]
+[55.4895, "o", "_"]
+[55.5737, "o", "a"]
+[55.627, "o", "i"]
+[55.7134, "o", "_"]
+[55.7583, "o", "a"]
+[55.8154, "o", "c"]
+[55.8796, "o", "t"]
+[56.2996, "o", "\r\n"]
+[58.3596, "o", "Evaluation Summary:\r\nContract ID: b7e60f21-6717-4ebd-b83f-dd6927dd5c52\r\nApplication: your-application\r\n"]
+[58.4496, "o", "Report: ./reports/folder_report_your-application_2026-08-30_165312.md\r\nOPA Evaluation: Successful\r\n"]
+[59.3396, "o", "$ "]
+[59.4066, "o", "h"]
+[59.4879, "o", "e"]
+[59.5623, "o", "a"]
+[59.652, "o", "d"]
+[59.73, "o", " "]
+[59.7905, "o", "-"]
+[59.8812, "o", "8"]
+[59.962, "o", " "]
+[60.0213, "o", "e"]
+[60.3724, "o", "\b \b"]
+[60.5701, "o", "r"]
+[60.6452, "o", "e"]
+[60.7252, "o", "p"]
+[60.7674, "o", "o"]
+[60.8375, "o", "r"]
+[60.8975, "o", "t"]
+[60.9699, "o", "s"]
+[61.0196, "o", "/"]
+[61.08, "o", "*"]
+[61.138, "o", "."]
+[61.1952, "o", "m"]
+[61.287, "o", "d"]
+[61.707, "o", "\r\n"]
+[62.167, "o", "# Evaluation Report\r\n\r\n**Analysis Date:** 2026-08-30 11:23:12\r\n"]
+[62.257, "o", "\r\n## Evaluation Summary\r\n- **Total Policies:** 29\r\n"]
+[62.347, "o", "- **Green Count:** 0\r\n- **Red Count:** 29\r\n"]
+[63.137, "o", "\u001b7\u001b[1A\r\u001b[17C\u001b[43;30m2\u001b[0m\u001b8"]
+[63.165, "o", "\u001b7\u001b[1A\r\u001b[17C\u001b[43;30m29\u001b[0m\u001b8"]
+[63.533, "o", "\u001b7\u001b[2A\r\u001b[19C\u001b[43;30m0\u001b[0m\u001b8"]
+[69.301, "o", "$ "]
+[69.3882, "o", "#"]
+[69.4743, "o", " "]
+[69.5451, "o", "w"]
+[69.6227, "o", "h"]
+[69.7075, "o", "i"]
+[69.7818, "o", "c"]
+[69.828, "o", "h"]
+[69.9057, "o", " "]
+[69.9455, "o", "m"]
+[69.9909, "o", "e"]
+[70.0551, "o", "t"]
+[70.0977, "o", "r"]
+[70.1655, "o", "i"]
+[70.2543, "o", "c"]
+[70.2991, "o", "s"]
+[70.4012, "o", " "]
+[70.4616, "o", "c"]
+[70.5501, "o", "a"]
+[70.6038, "o", "n"]
+[70.6874, "o", " "]
+[70.7728, "o", "a"]
+[70.8183, "o", "n"]
+[70.9047, "o", " "]
+[70.9633, "o", "e"]
+[71.0446, "o", "v"]
+[71.1189, "o", "a"]
+[71.2031, "o", "l"]
+[71.2921, "o", "u"]
+[71.3711, "o", "a"]
+[71.4169, "o", "t"]
+[71.4596, "o", "o"]
+[71.5339, "o", "r"]
+[71.7245, "o", " "]
+[71.7665, "o", "s"]
+[71.8055, "o", "u"]
+[71.8918, "o", "p"]
+[71.9756, "o", "p"]
+[72.03, "o", "l"]
+[72.0698, "o", "y"]
+[72.1141, "o", " "]
+[72.1811, "o", "t"]
+[72.2331, "o", "o"]
+[72.3002, "o", "d"]
+[72.3897, "o", "a"]
+[72.4633, "o", "y"]
+[72.5286, "o", "?"]
+[72.9486, "o", "\r\n"]
+[73.6086, "o", "$ "]
+[73.6944, "o", "p"]
+[73.7683, "o", "y"]
+[73.8566, "o", "t"]
+[73.9233, "o", "h"]
+[74.0105, "o", "o"]
+[74.0865, "o", "n"]
+[74.2597, "o", " "]
+[74.3029, "o", "s"]
+[74.3827, "o", "c"]
+[74.4661, "o", "r"]
+[74.5439, "o", "i"]
+[74.6249, "o", "p"]
+[74.7139, "o", "t"]
+[74.797, "o", "s"]
+[74.8734, "o", "/"]
+[74.9156, "o", "m"]
+[74.9747, "o", "e"]
+[75.0364, "o", "t"]
+[75.1187, "o", "r"]
+[75.1817, "o", "i"]
+[75.261, "o", "c"]
+[75.3511, "o", "_"]
+[75.3951, "o", "g"]
+[75.4514, "o", "a"]
+[75.5039, "o", "p"]
+[75.594, "o", "_"]
+[75.6727, "o", "r"]
+[75.7639, "o", "e"]
+[75.8538, "o", "p"]
+[75.9081, "o", "o"]
+[75.9502, "o", "r"]
+[76.0355, "o", "t"]
+[76.0967, "o", "."]
+[76.1828, "o", "p"]
+[76.2418, "o", "y"]
+[76.3131, "o", " "]
+[76.3935, "o", "|"]
+[76.5115, "o", " "]
+[76.5943, "o", "t"]
+[76.665, "o", "a"]
+[76.7531, "o", "i"]
+[76.8212, "o", "l"]
+[76.8663, "o", " "]
+[76.9249, "o", "-"]
+[76.977, "o", "1"]
+[77.0476, "o", "2"]
+[77.4676, "o", "\r\n"]
+[79.5276, "o", " calc metrics.model_card.completeness computed by global.v1.documentation.model_card_score, no evaluator needed\r\n GAP metrics.model_card.compliance_level no evaluator declares this\r\n calc metrics.model_card.quality computed by global.v1.documentation.model_card_score, no evaluator needed\r\n"]
+[79.6176, "o", " calc metrics.model_card.section_scores computed by global.v1.documentation.model_card_score, no evaluator needed\r\n ok metrics.social_scoring.combined_score SocialScoringEvaluator\r\n ok metrics.social_scoring.detrimental_treatment SocialScoringEvaluator\r\n"]
+[79.7076, "o", " ok metrics.social_scoring.score SocialScoringEvaluator\r\n ok metrics.toxicity.max_toxicity ContentSafetyEvaluator\r\n\r\n"]
+[79.7976, "o", "TOTAL: 22 of 27 measured metrics can be supplied today.\r\nThe gaps are what you would need to write an evaluator for, or\r\nsupply by hand. See docs on writing one.\r\n"]
+[80.6876, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m2\u001b[0m\u001b8"]
+[80.7156, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22\u001b[0m\u001b8"]
+[80.7436, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22 \u001b[0m\u001b8"]
+[80.7716, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22 o\u001b[0m\u001b8"]
+[80.7996, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22 of\u001b[0m\u001b8"]
+[80.8276, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22 of \u001b[0m\u001b8"]
+[80.8556, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22 of 2\u001b[0m\u001b8"]
+[80.8836, "o", "\u001b7\u001b[3A\r\u001b[7C\u001b[43;30m22 of 27\u001b[0m\u001b8"]
+[86.6516, "o", "$ "]
+[86.6516, "o", "\u2588"]
+[87.1816, "o", "\b \b"]
+[87.7116, "o", "\u2588"]
+[88.2416, "o", "\b \b"]
+[88.7716, "o", "\u2588"]
+[89.3016, "o", "\b \b"]
+[89.8316, "o", "\u2588"]
+[90.3616, "o", "\b \b"]
+[90.8916, "o", "\u2588"]
+[91.4216, "o", "\b \b"]
+[91.9516, "o", "\u2588"]
+[92.4816, "o", "\b \b"]
diff --git a/scripts/record-demo.py b/scripts/record-demo.py
new file mode 100644
index 0000000..9bbfa6d
--- /dev/null
+++ b/scripts/record-demo.py
@@ -0,0 +1,415 @@
+#!/usr/bin/env python3
+"""Compose the AICertify demo asciicast: real output, typed at a human pace.
+
+Every command here is actually run and its output captured, so nothing on the
+screen is written by hand. What is synthesised is the *timing*. The four
+commands take about three minutes between them, most of it downloading model
+weights and waiting on OPA, and a cast at real speed is unwatchable. A `script`
+capture would also have no typing in it at all, and output that materialises
+instantly reads as a screenshot rather than a session.
+
+Being explicit about which half is real matters. The scores, the field counts,
+the verdicts and the report are what the commands printed. The rhythm is
+invented, the way any screencast's rhythm is invented by the person recording.
+
+Four acts, because AICertify does a different job from GOPAL and needs the
+room. GOPAL decides things, so its demo is one beat: swap the model, the build
+fails. AICertify gathers things, and the interesting thing about it is how
+honest it is about the part it cannot gather.
+
+ 1. score-card a real Hugging Face card, scored, in one screen
+ 2. explain 150 fields you must declare, 14 an evaluator computes
+ 3. init/eval scaffold a contract, run it, get 0 of 29
+ 4. gap report 22 of 27 metrics supplied, and the five that are not
+
+Act 3 ends on nothing passing. That is the point rather than an embarrassment:
+the scaffold asserts none of the 150 declarations, so none of the obligations
+that turn on them can be met. It is the same argument act 2 makes, arriving as
+a verdict instead of a number.
+
+Usage:
+ record-demo.py [short.cast]
+
+The short cast is acts 1 and 2 only, for the README, where a 90-second loop is
+too long and JavaScript does not run.
+"""
+
+from __future__ import annotations
+
+import json
+import random
+import math
+import shutil
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+REPO = HERE.parent
+PY = str(REPO / ".venv" / "bin" / "python")
+CLI = [PY, "-m", "aicertify.cli"]
+
+# Deterministic jitter. The keystrokes look uneven, and they are the same
+# uneven every time, so regenerating does not churn the file in review.
+RNG = random.Random(20260830)
+
+CPS_MIN, CPS_MAX = 0.038, 0.092 # seconds per keystroke
+REACH_FOR_ENTER = 0.42 # hand leaves the letters, finds the return key
+ENTER_SETTLES = 0.16 # and the press registers before anything happens
+FIRST_BYTE = 0.30 # a command does not answer instantly
+THINKING = 1.9 # a command that clearly did some work
+READ_SHORT = 2.6
+READ_LONG = 5.4 # long enough to read a verdict and understand it
+BLINK = 0.53
+BLINKS = 6
+CURSOR = "█"
+
+SWEEP = 0.028 # per character, the pace of a pen moving
+HL_ON = "\x1b[43;30m"
+HL_OFF = "\x1b[0m"
+SAVE, RESTORE = "\x1b7", "\x1b8"
+
+TYPO_CHANCE = 0.55 # per command, not per character
+NOTICE_TYPO = (0.22, 0.46)
+AFTER_FIX = (0.08, 0.19)
+
+NEIGHBOURS = {
+ "a": "sq",
+ "b": "vn",
+ "c": "xv",
+ "d": "sf",
+ "e": "wr",
+ "f": "dg",
+ "g": "fh",
+ "h": "gj",
+ "i": "uo",
+ "j": "hk",
+ "k": "jl",
+ "l": "k",
+ "m": "n",
+ "n": "bm",
+ "o": "ip",
+ "p": "o",
+ "q": "wa",
+ "r": "et",
+ "s": "ad",
+ "t": "ry",
+ "u": "yi",
+ "v": "cb",
+ "w": "qe",
+ "x": "zc",
+ "y": "tu",
+ "z": "x",
+ ".": "/",
+ "/": ".",
+ "-": "0",
+ "_": "-",
+}
+
+WIDTH, HEIGHT = 100, 30
+
+
+class Cast:
+ def __init__(self) -> None:
+ self.t = 0.0
+ self.events: list[list] = []
+
+ def wait(self, seconds: float) -> None:
+ self.t += seconds
+
+ def out(self, text: str) -> None:
+ self.events.append([round(self.t, 4), "o", text])
+
+ def type(self, text: str, typo_at: int | None = None) -> None:
+ """One keystroke at a time, with the pauses and the mistakes.
+
+ A typo is the neighbouring key, noticed a moment later, backspaced and
+ retyped. It is the single thing that separates a recording of somebody
+ working from text replayed at a plausible speed.
+ """
+ for i, ch in enumerate(text):
+ if i == typo_at:
+ wrong = NEIGHBOURS.get(ch.lower())
+ if wrong:
+ self.wait(RNG.uniform(CPS_MIN, CPS_MAX))
+ self.out(RNG.choice(wrong))
+ self.wait(RNG.uniform(*NOTICE_TYPO))
+ self.out("\b \b")
+ self.wait(RNG.uniform(*AFTER_FIX))
+ self.wait(RNG.uniform(CPS_MIN, CPS_MAX))
+ if ch == " " and RNG.random() < 0.35:
+ self.wait(RNG.uniform(0.05, 0.16))
+ self.out(ch)
+
+ def command(self, text: str) -> None:
+ self.out("$ ")
+ typo_at = None
+ if len(text) > 14 and RNG.random() < TYPO_CHANCE:
+ typo_at = RNG.randrange(6, len(text) - 2)
+ self.type(text, typo_at)
+ # Pressing return is two beats: reaching for the key, and the press
+ # landing. Without them a command line ends and output begins in the
+ # same instant, which nothing on a real keyboard does.
+ self.wait(REACH_FOR_ENTER)
+ self.out("\r\n")
+ self.wait(ENTER_SETTLES)
+
+ def block(self, text: str, first_byte: float = FIRST_BYTE) -> None:
+ """Output arriving in a few pieces, as a real command does."""
+ self.wait(first_byte)
+ lines = text.rstrip("\n").split("\n")
+ for i in range(0, len(lines), 3):
+ self.out("\r\n".join(lines[i : i + 3]) + "\r\n")
+ self.wait(0.09)
+
+ def blink(self, times: int = BLINKS) -> None:
+ self.out("$ ")
+ for _ in range(times):
+ self.out(CURSOR)
+ self.wait(BLINK)
+ self.out("\b \b")
+ self.wait(BLINK)
+
+ def highlight(self, rows_up: int, col: int, text: str) -> None:
+ """Draw a highlighter left to right over text already on screen."""
+ # CSI 0 C moves the cursor forward one column, not zero: ANSI reads a
+ # zero parameter as one. Emitting it for a phrase at column 0 draws the
+ # highlight one place right and leaves the original first letter behind.
+ forward = f"\x1b[{col}C" if col > 0 else ""
+ for i in range(1, len(text) + 1):
+ self.out(
+ f"{SAVE}\x1b[{rows_up}A\r{forward}"
+ f"{HL_ON}{text[:i]}{HL_OFF}{RESTORE}"
+ )
+ self.wait(SWEEP)
+
+ def sweep(self, block: str, pairs: list[tuple[str, str]]) -> None:
+ """Highlight each phrase in a block of output just written.
+
+ The cursor is moved up by *screen rows*, not by lines. A line longer
+ than the terminal is wrapped by the emulator and occupies several rows,
+ so counting lines puts the highlighter above where the text actually
+ is. Two 130-character warnings at the end of one command were enough to
+ draw `BELOW THRESHOLD` across an unrelated score three rows below it.
+ """
+ lines = block.rstrip("\n").split("\n")
+
+ def rows(line: str) -> int:
+ return max(1, math.ceil(len(line) / WIDTH))
+
+ for needle, phrase in pairs:
+ for idx, line in enumerate(lines):
+ if needle in line and phrase in line:
+ up = sum(rows(rest) for rest in lines[idx:])
+ self.highlight(up, line.index(phrase), phrase)
+ self.wait(0.34)
+ break
+
+ def write(self, dst: Path, title: str) -> None:
+ header = {
+ "version": 2,
+ "width": WIDTH,
+ "height": HEIGHT,
+ "idle_time_limit": 3.0,
+ "title": title,
+ }
+ with dst.open("w", encoding="utf-8") as fh:
+ fh.write(json.dumps(header) + "\n")
+ for event in self.events:
+ fh.write(json.dumps(event) + "\n")
+ print(f"Wrote {dst} ({len(self.events)} events, {self.t:.1f}s)")
+
+
+def run(args: list[str], cwd: Path | None = None, stderr: bool = True) -> str:
+ done = subprocess.run(
+ args, cwd=cwd or REPO, capture_output=True, text=True, timeout=900
+ )
+ out = done.stdout or ""
+ # stderr is dropped where it carries only environment noise, such as the
+ # Hugging Face hub advising you to set a token. It is real, but it is not
+ # the command answering the question, and it would leave the act ending on
+ # a warning rather than on the verdict.
+ return out + (done.stderr or "") if stderr else out
+
+
+def shell(cmd: str, cwd: Path | None = None) -> str:
+ done = subprocess.run(
+ cmd, cwd=cwd or REPO, shell=True, capture_output=True, text=True, timeout=900
+ )
+ return (done.stdout or "") + (done.stderr or "")
+
+
+def require(condition: bool, message: str) -> None:
+ if not condition:
+ print(f"refusing to record: {message}", file=sys.stderr)
+ raise SystemExit(1)
+
+
+def main() -> int:
+ if len(sys.argv) not in (2, 3):
+ print(__doc__.strip(), file=sys.stderr)
+ return 2
+ full_dst = Path(sys.argv[1])
+ short_dst = Path(sys.argv[2]) if len(sys.argv) == 3 else None
+
+ work = Path(tempfile.mkdtemp(prefix="aicertify-demo-"))
+ try:
+ print("running the commands (this takes a few minutes)...", file=sys.stderr)
+
+ card = run(CLI + ["score-card", "bert-base-uncased"], stderr=False)
+ declare = shell(
+ f"{PY} -m aicertify.cli explain eu_ai_act 2>/dev/null | head -12"
+ )
+ measure = shell(
+ f"{PY} -m aicertify.cli explain eu_ai_act 2>/dev/null"
+ ' | grep -A8 "produced by evaluators"'
+ )
+
+ contract = work / "contract.json"
+ shell(
+ f"{PY} -m aicertify.cli init-contract --policy eu_ai_act "
+ f"2>/dev/null > {contract}"
+ )
+ require(
+ contract.exists() and contract.stat().st_size > 1000,
+ "init-contract produced no usable contract",
+ )
+ # A declarations-only contract. The scaffold ships one placeholder
+ # interaction, and evaluating it pulls DeepEval into an LLM call that
+ # needs a provider key. The point of this act is the policy verdict,
+ # not the toxicity classifier, and act 4 covers the evaluators.
+ doc = json.loads(contract.read_text())
+ doc["interactions"] = []
+ contract.write_text(json.dumps(doc, indent=2))
+
+ evaluate = shell(
+ f"{PY} -m aicertify.cli evaluate --contract contract.json "
+ f"--policy eu_ai_act --report-format markdown --output-dir ./reports "
+ "2>&1 | tail -5",
+ cwd=work,
+ )
+ summary = shell("head -8 reports/*.md", cwd=work)
+ gaps = shell(f"{PY} scripts/metric_gap_report.py 2>/dev/null | tail -12")
+
+ # Guards. Each of these is a fact the cast puts on screen, so if the
+ # tool stops saying it the recording is a lie and must not be written.
+ require(
+ "0.49" in card and "BELOW THRESHOLD" in card,
+ "bert-base-uncased no longer scores 0.49 below the threshold",
+ )
+ require(
+ "Fields you must declare (150)" in declare,
+ "explain no longer reports 150 declared fields; it disagrees "
+ "with the published figure and one of them is wrong",
+ )
+ require(
+ "Fields produced by evaluators (14)" in measure,
+ "explain no longer reports 14 measured fields",
+ )
+ require(
+ "defeats the point" in measure,
+ "the line explaining why you must not hand-write metrics is gone",
+ )
+ require(
+ "Red Count:** 29" in summary and "Green Count:** 0" in summary,
+ "an empty contract no longer fails all 29 policies, so act 3 "
+ "has lost its point",
+ )
+ require(
+ "22 of 27" in gaps,
+ "the metric gap report no longer says 22 of 27; act 4's "
+ "contribution hook needs the real number",
+ )
+
+ acts = build(card, declare, measure, evaluate, summary, gaps)
+ acts.write(full_dst, "AICertify: what it measures, and what it cannot")
+
+ if short_dst:
+ short = build_short(card, declare, measure)
+ short.write(short_dst, "AICertify: 150 declared, 14 measured")
+ return 0
+ finally:
+ shutil.rmtree(work, ignore_errors=True)
+
+
+def act_one(cast: Cast, card: str) -> None:
+ """A famous model, scored, with no setup at all."""
+ cast.wait(0.6)
+ cast.command("aicertify score-card bert-base-uncased")
+ cast.block(card, first_byte=THINKING)
+ cast.wait(0.8)
+ cast.sweep(
+ card,
+ [
+ ("completeness", "0.49"),
+ ("BELOW THRESHOLD", "BELOW THRESHOLD"),
+ ],
+ )
+ cast.wait(READ_LONG)
+
+
+def act_two(cast: Cast, declare: str, measure: str) -> None:
+ """The split that the whole product turns on."""
+ cast.command("# so what does the EU AI Act actually ask for?")
+ cast.wait(0.5)
+ cast.command("aicertify explain eu_ai_act | head -12")
+ cast.block(declare, first_byte=THINKING)
+ cast.wait(0.7)
+ cast.sweep(declare, [("Fields you must declare", "150")])
+ cast.wait(READ_SHORT)
+
+ cast.command('aicertify explain eu_ai_act | grep -A8 "produced by evaluators"')
+ cast.block(measure, first_byte=THINKING)
+ cast.wait(0.7)
+ cast.sweep(
+ measure,
+ [
+ ("produced by evaluators", "14"),
+ ("defeats the point", "defeats the point"),
+ ],
+ )
+ cast.wait(READ_LONG)
+
+
+def build_short(card: str, declare: str, measure: str) -> Cast:
+ cast = Cast()
+ act_one(cast, card)
+ act_two(cast, declare, measure)
+ cast.blink()
+ return cast
+
+
+def build(card, declare, measure, evaluate, summary, gaps) -> Cast:
+ cast = Cast()
+ act_one(cast, card)
+ act_two(cast, declare, measure)
+
+ # Act 3: scaffold, run, and find out you have answered nothing.
+ cast.command("aicertify init-contract --policy eu_ai_act > contract.json")
+ cast.wait(THINKING + 0.9)
+ cast.command("aicertify evaluate --contract contract.json --policy eu_ai_act")
+ cast.block(evaluate, first_byte=THINKING)
+ cast.wait(0.8)
+
+ cast.command("head -8 reports/*.md")
+ cast.block(summary)
+ cast.wait(0.7)
+ cast.sweep(summary, [("Red Count", "29"), ("Green Count", "0")])
+ cast.wait(READ_LONG)
+
+ # Act 4: the tool enumerating its own gaps, which is a contributor's
+ # to-do list rather than a caveat.
+ cast.command("# which metrics can an evaluator supply today?")
+ cast.wait(0.5)
+ cast.command("python scripts/metric_gap_report.py | tail -12")
+ cast.block(gaps, first_byte=THINKING)
+ cast.wait(0.8)
+ cast.sweep(gaps, [("TOTAL", "22 of 27")])
+ cast.wait(READ_LONG)
+ cast.blink()
+ return cast
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())