<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Chapter 02 — Ocarina, the framework on Ocarina — manual</title>
    <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/</link>
    <description>Recent content in Chapter 02 — Ocarina, the framework on Ocarina — manual</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    <lastBuildDate>Thu, 04 Jun 2026 21:25:43 +0200</lastBuildDate>
    <atom:link href="https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>02.01 — Ocarina&#39;s technical identity</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/01-identity/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/01-identity/</guid>
      <description>&lt;h1 id=&#34;0201ocarinas-technical-identity&#34;&gt;02.01 — Ocarina&amp;rsquo;s technical identity&lt;a class=&#34;anchor&#34; href=&#34;#0201ocarinas-technical-identity&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;h2 id=&#34;pyprojecttoml&#34;&gt;&lt;code&gt;pyproject.toml&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#pyprojecttoml&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[project]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;name = &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;ocarina&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;version = &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;1.1.10&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;description = &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Websites test framework for Igor&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;requires-python = &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;gt;=3.14&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;authors = [{ name=&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Igor Casanova&amp;#34;&lt;/span&gt;, email=&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;[REDACTED]&amp;#34;&lt;/span&gt; }]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;license = &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;MIT&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;license-files = [&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;LICEN[CS]E*&amp;#34;&lt;/span&gt;]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;readme = &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;README.md&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;dependencies = [&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;python-docx&amp;gt;=1.2.0&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;version = &amp;quot;1.1.10&amp;quot;&lt;/code&gt;&lt;/strong&gt;: &lt;em&gt;stable 1.x&lt;/em&gt;, not a pre-release.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;requires-python = &amp;quot;&amp;gt;=3.14&amp;quot;&lt;/code&gt;&lt;/strong&gt;: PEP 695 generic typing everywhere.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;dependencies = [&amp;quot;python-docx&amp;gt;=1.2.0&amp;quot;]&lt;/code&gt;&lt;/strong&gt;: &lt;strong&gt;one&lt;/strong&gt; runtime dependency. The rest lives in &lt;code&gt;dev&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;license = &amp;quot;MIT&amp;quot;&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;description = &amp;quot;Websites test framework for Igor&amp;quot;&lt;/code&gt;&lt;/strong&gt;: not &amp;ldquo;for everyone,&amp;rdquo; not &amp;ldquo;for humans,&amp;rdquo; explicitly &amp;ldquo;for Igor.&amp;rdquo; Lines up with &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/01-philosophy/05-political-stance/&#34;&gt;&lt;code&gt;../01-philosophy/05-political-stance.md&lt;/code&gt;&lt;/a&gt;: &amp;ldquo;It&amp;rsquo;s &lt;em&gt;my&lt;/em&gt; car.&amp;rdquo;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;dev-dependencies&#34;&gt;&lt;code&gt;dev&lt;/code&gt; dependencies&lt;a class=&#34;anchor&#34; href=&#34;#dev-dependencies&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[dependency-groups]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;dev = [&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;ruff&amp;gt;=0.15.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;mypy&amp;gt;=1.17.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;mypy-extensions&amp;gt;=1.1.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;typing-extensions&amp;gt;=4.14.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;pytest&amp;gt;=9.0.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;pytest-cov&amp;gt;=7.0.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;hypothesis&amp;gt;=6.151.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;pytest-mypy-plugins&amp;gt;=3.1.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;allure-pytest&amp;gt;=2.15.3&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;allure-python-commons&amp;gt;=2.15.3&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;pre-commit&amp;gt;=4.5.1&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;syrupy&amp;gt;=5.1.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;selenium&amp;gt;=4.40.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;playwright&amp;gt;=1.60.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;twine&amp;gt;=6.2.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;build&amp;gt;=1.4.2&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;prysk&amp;gt;=0.20.0&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Tool&lt;/th&gt;&#xA;          &lt;th&gt;Role in Ocarina&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;ruff&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Linter + formatter (replaces flake8 + isort + black). &lt;code&gt;select = [&amp;quot;ALL&amp;quot;]&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;mypy&lt;/code&gt; (+ extensions)&lt;/td&gt;&#xA;          &lt;td&gt;Type-checker. &lt;code&gt;strict = true&lt;/code&gt; (see &lt;code&gt;mypy.ini&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;pytest&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Unit-test runner (the framework is itself tested).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;pytest-cov&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Test coverage (of the framework itself). Configured in &lt;code&gt;pyproject.toml#tool.coverage&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;hypothesis&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;Property-based testing (PBT)&lt;/em&gt; → &lt;code&gt;test_invariants_properties.py&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;pytest-mypy-plugins&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Static&lt;/strong&gt; typing tests (checks expected &lt;em&gt;mypy&lt;/em&gt; errors and type-inference tests with &lt;code&gt;reveal_type&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;allure-pytest&lt;/code&gt; / &lt;code&gt;allure-python-commons&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Allure report (deployed to GH Pages).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;pre-commit&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Local git hooks (ruff-format).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;syrupy&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;Snapshot testing&lt;/em&gt; (output of &lt;code&gt;pretty_print_results&lt;/code&gt;, &lt;code&gt;results_to_json&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;selenium&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Present in &lt;code&gt;dev&lt;/code&gt; because Ocarina &lt;strong&gt;doesn&amp;rsquo;t depend&lt;/strong&gt; on Selenium to work; it just ships a Selenium &lt;em&gt;adapter&lt;/em&gt; so the framework is immediately operational out of box.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;playwright&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Same logic: a second shipped &lt;em&gt;adapter&lt;/em&gt; (since &lt;code&gt;1.1.3&lt;/code&gt;). Ocarina now drives &lt;strong&gt;both&lt;/strong&gt; Selenium and Playwright out of the box, behind the same ports.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;twine&lt;/code&gt; / &lt;code&gt;build&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;PyPI publishing.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;prysk&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;CLI tests in the &lt;em&gt;cram&lt;/em&gt; (&lt;code&gt;.t&lt;/code&gt;) format. Successor to &lt;code&gt;cram&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;mypyini&#34;&gt;&lt;code&gt;mypy.ini&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#mypyini&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-ini&#34; data-lang=&#34;ini&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;[mypy]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#50fa7b&#34;&gt;python_version&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;3.14&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#50fa7b&#34;&gt;strict&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;true&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# * ... Allow missing annotations (type inference is cool)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#50fa7b&#34;&gt;disallow_incomplete_defs&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;false&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# * ... Allow missing annotations (type inference is cool)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#50fa7b&#34;&gt;disallow_untyped_defs&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;false&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;strict = true&lt;/code&gt;&lt;/strong&gt;: enables &lt;code&gt;--warn-redundant-casts&lt;/code&gt;, &lt;code&gt;--warn-unused-ignores&lt;/code&gt;, &lt;code&gt;--no-implicit-optional&lt;/code&gt;, &lt;code&gt;--check-untyped-defs&lt;/code&gt;, etc.&lt;/li&gt;&#xA;&lt;li&gt;Two exceptions: &lt;code&gt;disallow_incomplete_defs&lt;/code&gt; and &lt;code&gt;disallow_untyped_defs&lt;/code&gt; are off. &lt;em&gt;mypy&lt;/em&gt;&amp;rsquo;s &lt;strong&gt;type inference&lt;/strong&gt; is good enough when explicit annotations don&amp;rsquo;t add value. Annotations live where they actually &lt;strong&gt;matter&lt;/strong&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;pyprojecttomltoolruff&#34;&gt;&lt;code&gt;pyproject.toml#tool.ruff&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#pyprojecttomltoolruff&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[tool.ruff.lint]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;select = [&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;ALL&amp;#34;&lt;/span&gt;]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ignore = [&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;ANN002&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;ANN003&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;ANN201&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;TRY003&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;C901&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;D203&amp;#34;&lt;/span&gt;,    &lt;span style=&#34;color:#6272a4&#34;&gt;# conflict with D211&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;D213&amp;#34;&lt;/span&gt;,    &lt;span style=&#34;color:#6272a4&#34;&gt;# conflict with D212&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;COM812&amp;#34;&lt;/span&gt;,  &lt;span style=&#34;color:#6272a4&#34;&gt;# conflict with the ruff formatter (auto-fix)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[tool.ruff]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;exclude = [&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;**/.venv/**&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;**/bin/**&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;**/__init__.py&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;**/__bypass_linter__&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;select = [&amp;quot;ALL&amp;quot;]&lt;/code&gt;&lt;/strong&gt;: every &lt;em&gt;ruff&lt;/em&gt; rule on (~800).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Short ignore list&lt;/strong&gt;: six rules, each one justified.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;B008&lt;/code&gt; must NEVER be ignored&lt;/strong&gt; (&amp;quot;&lt;em&gt;&lt;code&gt;# &amp;quot;B008&amp;quot;,  # * ... NEVER ignore this rule without knowing very well what you are doing: https://docs.astral.sh/ruff/rules/function-call-in-default-argument/&lt;/code&gt;&lt;/em&gt;&amp;quot;). Note left so the past mistake doesn&amp;rsquo;t repeat.&lt;/li&gt;&#xA;&lt;li&gt;**&lt;code&gt;**/**bypass_linter**&lt;/code&gt;**: directory convention for explicitly un-linted code (handy for internal experimental modules).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;pyprojecttomltoolpytestini_options&#34;&gt;&lt;code&gt;pyproject.toml#tool.pytest.ini_options&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#pyprojecttomltoolpytestini_options&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;testpaths = [&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;tests&amp;#34;&lt;/span&gt;]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python_files = [&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;test_*.py&amp;#34;&lt;/span&gt;]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;norecursedirs = [&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;.*&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;__pycache__&amp;#34;&lt;/span&gt;]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;log_cli = &lt;span style=&#34;color:#ff79c6&#34;&gt;true&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;log_cli_level = &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;DEBUG&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;log_cli_format = &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;%(asctime)s [%(levelname)s] %(name)s: %(message)s&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;log_cli_date_format = &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;%Y-%m-%d %H:%M:%S&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;addopts = &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;--cov=src&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;--cov-branch&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;--cov-report=term-missing&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;--cov-report=html&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;--cov-report=xml:coverage.xml&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Setting&lt;/th&gt;&#xA;          &lt;th&gt;Effect&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;log_cli = true&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;All test logs surface in the CLI (useful for scenarios that go through &lt;code&gt;ILogger&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;--cov=src&lt;/code&gt; + &lt;code&gt;--cov-branch&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Branch coverage, not just line coverage.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;--cov-report=html&lt;/code&gt; + &lt;code&gt;xml&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Three outputs: terminal, local HTML, XML for CI ingestion.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;pyprojecttomltoolcoverage&#34;&gt;&lt;code&gt;pyproject.toml#tool.coverage&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#pyprojecttomltoolcoverage&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Full detail at &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/04-internal-tests/07-coverage-policy/&#34;&gt;&lt;code&gt;../04-internal-tests/07-coverage-policy.md&lt;/code&gt;&lt;/a&gt;. Short version: anything that&amp;rsquo;s pure &lt;em&gt;shape&lt;/em&gt; (custom_types, ports, errors), anything that needs a real browser (Selenium adapters), and anything covered by cram tests (cli/store, cli/builder) is explicitly omitted. Otherwise the metrics &lt;strong&gt;lie&lt;/strong&gt;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.02 — The ocarina module tree</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/02-module-tree/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/02-module-tree/</guid>
      <description>&lt;h1 id=&#34;0202the-ocarina-module-tree&#34;&gt;02.02 — The &lt;code&gt;ocarina&lt;/code&gt; module tree&lt;a class=&#34;anchor&#34; href=&#34;#0202the-ocarina-module-tree&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;p&gt;The Python source module ships &lt;strong&gt;134 &lt;code&gt;.py&lt;/code&gt; files&lt;/strong&gt; across four conceptual layers.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;src/ocarina/&#xA;├── __init__.py                              # empty — no global &amp;#34;facade&amp;#34;, everything is imported precisely&#xA;├── py.typed                                 # PEP 561: declares the package as &amp;#34;typed&amp;#34;&#xA;│&#xA;├── railway/                                 # Layer 1 — FP primitives&#xA;│   ├── __init__.py&#xA;│   └── result.py                            # Ok[T] / Fail / Result[T] / is_ok / is_fail&#xA;│&#xA;├── custom_types/                            # Layer 2 — types and aliases&#xA;│   ├── __init__.py&#xA;│   ├── effect.py                            # type Effect = Callable[[], None]&#xA;│   ├── thunk.py                             # type Thunk[T] = Callable[[], T]&#xA;│   ├── tpom.py                              # TypeVar TPOM bound POMBase&#xA;│   ├── supports_write.py                    # Protocol SupportsWrite[T], mirrors a type not exposed by Python stdlib&#xA;│   ├── built_web_driver.py                  # type BuiltWebDriver[Driver] = tuple[Driver, Effect]&#xA;│   ├── scenario.py                          # Scenario[Driver] (frozen dataclass)&#xA;│   ├── test_components.py                   # TestChain / TestSetup / TestTeardown / TestWatchers&#xA;│   ├── test_runner.py                       # TestRunner[Driver]&#xA;│   ├── oc_test.py                           # TestName / TestScenario / TestScenarioFragment&#xA;│   ├── oc_test_layers.py                    # TestResult / TestSuiteResult / TestCampaignResults / TestCycleResults&#xA;│   ├── selenium/&#xA;│   │   ├── built_web_driver.py              # BuiltSeleniumWebDriver&#xA;│   │   ├── oc_test_scenario.py&#xA;│   │   ├── supported_browsers.py            # type SupportedSeleniumBrowser = Literal[&amp;#34;chrome&amp;#34;, &amp;#34;firefox&amp;#34;, &amp;#34;edge&amp;#34;, &amp;#34;safari&amp;#34;]&#xA;│   │   └── web_drivers_pool.py              # type SeleniumWebDriversPool = WebDriversPool[WebDriver]&#xA;│   └── playwright/&#xA;│       ├── built_web_driver.py              # type BuiltPlaywrightDriver = BuiltWebDriver[PlaywrightDriver]&#xA;│       ├── oc_test_scenario.py              # PlaywrightTestScenario / PlaywrightTestScenarioFragment&#xA;│       ├── supported_browsers.py            # type SupportedPlaywrightBrowser = Literal[&amp;#34;chromium&amp;#34;, &amp;#34;firefox&amp;#34;, &amp;#34;webkit&amp;#34;]&#xA;│       └── web_drivers_pool.py              # type PlaywrightDriversPool = WebDriversPool[PlaywrightDriver]&#xA;│&#xA;├── custom_errors/                           # Layer 2: errors&#xA;│   ├── __init__.py&#xA;│   └── test_framework/&#xA;│       ├── __init__.py&#xA;│       ├── no_matching_branch.py            # NoMatchingBranchError&#xA;│       ├── pages.py                         # PageVerificationError&#xA;│       ├── driver_died.py                   # DriverDiedError&#xA;│       └── campaigns.py&#xA;│&#xA;├── custom_invariants/                       # Layer 2: ready-to-use invariants&#xA;│   ├── __init__.py&#xA;│   └── testing/&#xA;│       ├── __init__.py&#xA;│       ├── workers.py                       # validate_workers_amount&#xA;│       ├── oc_test_runners_ids.py           # validate_test_runners_ids&#xA;│       ├── oc_test_runners_names.py         # validate_test_runners_names (unique + valid filename)&#xA;│       ├── oc_test_suites_names.py&#xA;│       ├── oc_test_campaigns_names.py&#xA;│       └── oc_test_cycles_names.py&#xA;│&#xA;├── ports/                                   # Layer 3: ports (interfaces)&#xA;│   ├── __init__.py&#xA;│   ├── ilogger.py                           # ILogger (ABC)&#xA;│   └── itake_screenshot.py                  # ITakeScreenshot[Driver] (Protocol)&#xA;│&#xA;├── pom/                                     # Layer 3: Page Object Model&#xA;│   ├── __init__.py&#xA;│   ├── base.py                              # POMBase (ABC: verify + get_current_title)&#xA;│   ├── selenium/&#xA;│   │   ├── __init__.py&#xA;│   │   └── muted.py                         # MutedPOM utility&#xA;│   └── playwright/&#xA;│       ├── __init__.py&#xA;│       └── muted.py                         # MutedPlaywrightPOM utility&#xA;│&#xA;├── aggregates/                              # Layer 3: result aggregates&#xA;│   ├── __init__.py&#xA;│   └── tests_layers.py                      # is_test_result_ok / _fail / _skipped (TypeGuard)&#xA;│&#xA;├── dsl/                                     # Layer 4: Ocarina DSL&#xA;│   ├── __init__.py&#xA;│   ├── invariants/&#xA;│   │   ├── __init__.py&#xA;│   │   ├── validate.py                      # entry-point validate(), BusinessInvariantValidator, FrameworkInvariantValidator&#xA;│   │   ├── assertions.py                    # is_str / is_email / is_positive / is_in / has_unique_elements / each / ...&#xA;│   │   ├── errors.py                        # InvariantViolationError / DuplicatesError / AggregateInvariantViolationError&#xA;│   │   └── internals/&#xA;│   │       ├── __init__.py&#xA;│   │       └── validation_chain.py          # ValidationStartBlock / ValidationAssertBlock / _ValidationChain / _any_of / chain_validations&#xA;│   ├── testing/&#xA;│   │   ├── __init__.py&#xA;│   │   ├── oc_test.py                       # class Test[Driver]&#xA;│   │   ├── oc_test_suite.py                 # class TestSuite[Driver]&#xA;│   │   ├── oc_test_campaign.py              # class TestCampaign[Driver] + campaign_has_failed&#xA;│   │   ├── oc_test_cycle.py                 # class TestCycle[Driver] + type Mode + has_test_cycle_failed&#xA;│   │   ├── filter_tests_by_ids.py&#xA;│   │   ├── watcher.py                       # class Watcher[Driver]&#xA;│   │   ├── internals/&#xA;│   │   │   ├── __init__.py&#xA;│   │   │   ├── test_executor.py             # class TestExecutor[Driver] + ExecutionOutcome (frozen, slots)&#xA;│   │   │   └── test_flow.py                 # class TestFlow[Driver]&#xA;│   │   ├── selenium/&#xA;│   │   │   ├── __init__.py&#xA;│   │   │   ├── create_test.py               # create_selenium_test(...)&#xA;│   │   │   └── create_watcher.py            # create_selenium_watcher(...) + type SeleniumWatcher&#xA;│   │   └── playwright/&#xA;│   │       ├── __init__.py&#xA;│   │       ├── create_test.py               # create_playwright_test(...)&#xA;│   │       └── create_watcher.py            # create_playwright_watcher(...) + type PlaywrightWatcher&#xA;│   └── testing_with_railway/&#xA;│       ├── __init__.py&#xA;│       ├── chain_actions.py                 # class ChainRunner[T] + chain_actions&#xA;│       ├── match_page.py                    # When + _match_page_builder + create_match_page&#xA;│       ├── constructors/&#xA;│       │   ├── __init__.py&#xA;│       │   └── create_act.py                # low-level primitive (to be wrapped on the project side)&#xA;│       └── internals/&#xA;│           ├── __init__.py&#xA;│           └── action_chain.py              # ActionStart → ActionFailure → ActionSuccess → ActionChain&#xA;│                                            # + Neutral{Start,Failure,Success}  (failure rail)&#xA;│&#xA;├── infra/                                   # Layer 5: infrastructure&#xA;│   ├── __init__.py&#xA;│   ├── drivers_pool.py                      # class WebDriversPool[Driver] + WarmupTimeoutError&#xA;│   ├── driver_builder.py                    # class DriverBuilder[Driver]&#xA;│   ├── screenshotter.py                     # class Screenshotter[TDriver] + ScreenshotterConfig&#xA;│   ├── act_counter.py                       # class ActCounter (interface)&#xA;│   ├── selenium/&#xA;│   │   ├── __init__.py&#xA;│   │   ├── create_driver.py                 # _build_firefox / _build_chrome / _build_edge / _build_safari&#xA;│   │   ├── create_drivers_pool.py           # create_selenium_drivers_pool&#xA;│   │   ├── create_screenshotter.py          # create_selenium_screenshotter&#xA;│   │   ├── driver_healthcheck.py            # driver_healthcheck (pings driver.title)&#xA;│   │   └── mixins.py                        # SeleniumTitleMixin (typing keyway)&#xA;│   └── playwright/&#xA;│       ├── __init__.py&#xA;│       ├── create_driver.py                 # create_playwright_driver (chromium / firefox / webkit)&#xA;│       ├── create_drivers_pool.py           # create_playwright_drivers_pool&#xA;│       ├── create_screenshotter.py          # create_playwright_screenshotter + _playwright_save_full_page&#xA;│       ├── driver.py                        # class PlaywrightDriver (sync wrapper)&#xA;│       ├── driver_healthcheck.py            # playwright_driver_healthcheck (pings driver.title)&#xA;│       └── mixins.py                        # PlaywrightTitleMixin (typing keyway)&#xA;│&#xA;└── opinionated/                             # Layer 6: opt-in, everything that is &amp;#34;nice but replaceable&amp;#34;&#xA;    ├── __init__.py&#xA;    ├── consts/loggers_choices.py            # LOGGERS_CHOICES = (&amp;#34;terminal&amp;#34;, &amp;#34;file&amp;#34;, &amp;#34;terminal+file&amp;#34;, &amp;#34;muted&amp;#34;)&#xA;    ├── infra/&#xA;    │   ├── __init__.py&#xA;    │   └── act_counter.py                   # ThreadsBasedActCounter (thread-local counter)&#xA;    ├── cli/&#xA;    │   ├── __init__.py&#xA;    │   ├── builder.py                       # CliBuilder + CliArg + _SilentArgumentParser&#xA;    │   ├── store.py                         # CliStore[TKeys] + _CliField[T] + field(...)&#xA;    │   ├── phantoms.py                      # phantom_validate (no-op predicate)&#xA;    │   ├── selenium/&#xA;    │   │   ├── __init__.py&#xA;    │   │   ├── cli_store_singleton.py       # SeleniumCliStoreSingleton (push / get)&#xA;    │   │   └── create_cli_store.py          # create_selenium_{auto,win,macos,linux}_cli_store&#xA;    │   └── playwright/&#xA;    │       ├── __init__.py&#xA;    │       ├── cli_store_singleton.py       # PlaywrightCliStoreSingleton (push / get)&#xA;    │       └── create_cli_store.py          # create_playwright_{,auto_}cli_store&#xA;    ├── dsl/&#xA;    │   ├── __init__.py&#xA;    │   └── drive_page.py                    # drive_page = chain_actions, a semantic alias&#xA;    ├── launcher/&#xA;    │   ├── __init__.py&#xA;    │   └── bootstrap.py                     # bootstrap(...) + run_plugins(*plugins, exceptions_logger)&#xA;    ├── loggers/&#xA;    │   ├── __init__.py&#xA;    │   ├── create_matching_logger.py        # create_matching_logger(&amp;#34;terminal&amp;#34;|&amp;#34;file&amp;#34;|...) + get_default_log_dir&#xA;    │   ├── print_logger.py                  # PrintLogger (ANSI)&#xA;    │   ├── file_logger.py                   # FileLogger (taxonomy -&amp;gt; file tree)&#xA;    │   ├── print_and_file_logger.py&#xA;    │   ├── muted_logger.py&#xA;    │   ├── custom_types/supported_loggers.py&#xA;    │   └── utils/&#xA;    │       ├── __init__.py&#xA;    │       └── format_metadata_str.py       # format_utc_date_metadata_str / format_current_thread_metadata_str / concat_metadata&#xA;    └── plugins/&#xA;        ├── __init__.py&#xA;        └── reports/&#xA;            ├── __init__.py&#xA;            ├── pretty_print_results.py      # hierarchical ANSI report&#xA;            ├── results_to_json.py           # JSON generator&#xA;            ├── docx_tests_proofs.py         # DOCX generator (consumes the log tree)&#xA;            └── timing.py                    # context manager `with timing(prefix=&amp;#34;Duration:&amp;#34;)`&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;layered-diagram&#34;&gt;Layered diagram&lt;a class=&#34;anchor&#34; href=&#34;#layered-diagram&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────────────────┐&#xA;│ USER PROJECT (e.g. ocarina-example, ocarina-with-ai-example)             │&#xA;│  ┌────────────────────┐  ┌────────────────────┐  ┌─────────────────────┐ │&#xA;│  │ pages/ (POMBase)   │  │ scenarios/ (Test)  │  │ adapters/ (act, ...)│ │&#xA;│  └────────────────────┘  └────────────────────┘  └─────────────────────┘ │&#xA;└─────────────────────────────────┬────────────────────────────────────────┘&#xA;                                  │ imports&#xA;┌─────────────────────────────────▼────────────────────────────────────────┐&#xA;│ LAYER 6 — opinionated/  (CLI, loggers, plugins, bootstrap)               │&#xA;│   Opt-in. Can be replaced wholesale.                                     │&#xA;└─────────────────────────────────┬────────────────────────────────────────┘&#xA;                                  │&#xA;┌─────────────────────────────────▼────────────────────────────────────────┐&#xA;│ LAYER 5 — infra/  (pool, builder, screenshotter, Selenium adapters)      │&#xA;│   I/O. No DSL logic.                                                     │&#xA;└─────────────────────────────────┬────────────────────────────────────────┘&#xA;                                  │&#xA;┌─────────────────────────────────▼────────────────────────────────────────┐&#xA;│ LAYER 4 — dsl/  (testing, invariants, testing_with_railway)              │&#xA;│   Pure DSL, no I/O. All the grammar.                                     │&#xA;└─────────────────────────────────┬────────────────────────────────────────┘&#xA;                                  │&#xA;┌─────────────────────────────────▼────────────────────────────────────────┐&#xA;│ LAYER 3 — ports/  +  pom/  +  aggregates/                                │&#xA;│   Interfaces and abstractions.                                           │&#xA;└─────────────────────────────────┬────────────────────────────────────────┘&#xA;                                  │&#xA;┌─────────────────────────────────▼────────────────────────────────────────┐&#xA;│ LAYER 2 — custom_types/  +  custom_errors/  +  custom_invariants/        │&#xA;│   Shapes, aliases, types, exceptions, ready-to-use invariants.           │&#xA;└─────────────────────────────────┬────────────────────────────────────────┘&#xA;                                  │&#xA;┌─────────────────────────────────▼────────────────────────────────────────┐&#xA;│ LAYER 1 — railway/  (Result, Ok, Fail, is_ok, is_fail)                   │&#xA;│   The functional primitive. No dependencies.                             │&#xA;└──────────────────────────────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This &lt;strong&gt;strict stratification&lt;/strong&gt; rules out &lt;em&gt;import cycles&lt;/em&gt; and lets you swap any upper layer without touching the ones below:&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.06 — Scenario[Driver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/06-scenario/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/06-scenario/</guid>
      <description>&lt;h1 id=&#34;0206scenariodriver&#34;&gt;02.06 — &lt;code&gt;Scenario[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#0206scenariodriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/custom_types/scenario.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/custom_types/scenario.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;dataclass&#34;&gt;Dataclass&lt;a class=&#34;anchor&#34; href=&#34;#dataclass&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@dataclass(frozen&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;Scenario&lt;/span&gt;[Driver]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    test_chain: TestChain&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    setup: TestSetup &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; field(default&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    teardown: TestTeardown &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; field(default&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    watchers: TestWatchers[Driver] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; field(default&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# src/ocarina/custom_types/test_components.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; TestChain &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Sequence[ChainRunner[Any]]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; TestSetup &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Effect &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; TestTeardown &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Effect &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; TestWatchers[Driver] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Sequence[Watcher[Driver]] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;lifecycle-per-attempt&#34;&gt;Lifecycle (per attempt)&lt;a class=&#34;anchor&#34; href=&#34;#lifecycle-per-attempt&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;1. setup()           — optional, free-form Effect (DB, API, …)&#xA;       → raises    :  test_chain skipped, jump to teardown,&#xA;                      return Outcome(setup_failed=True, should_retry=True)&#xA;       → ok        :  continue to test_chain&#xA;&#xA;2. test_chain        — the actual chain (Sequence[ChainRunner])&#xA;       (watchers run during this time)&#xA;&#xA;3. teardown()        — optional, always executed&#xA;       → raises    :  log warning + ignore (does not affect the verdict)&#xA;&#xA;If EVERY attempt raises during setup:&#xA;    → test marked SKIPPED (not FAILED)&#xA;    → warning log « setup keeps failing »&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;setup-and-teardown&#34;&gt;&lt;code&gt;setup&lt;/code&gt; and &lt;code&gt;teardown&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#setup-and-teardown&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;setup&lt;/code&gt; and &lt;code&gt;teardown&lt;/code&gt; are &lt;strong&gt;driver-free and injection-free by design.&lt;/strong&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.07 — Watcher[Driver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/07-watcher/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/07-watcher/</guid>
      <description>&lt;h1 id=&#34;0207watcherdriver&#34;&gt;02.07 — &lt;code&gt;Watcher[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#0207watcherdriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing/watcher.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing/watcher.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Parallelized observer running as a &lt;em&gt;daemon thread&lt;/em&gt; alongside the &lt;code&gt;test_chain&lt;/code&gt;. Built to catch &lt;strong&gt;unpredictable frictions with no direct impact on the scenario&lt;/strong&gt; that pop up &lt;em&gt;while&lt;/em&gt; the scenario runs: random error toasts, parasitic validations, unexpected popups.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;a class=&#34;anchor&#34; href=&#34;#the-problem&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Holy Book quote (&lt;em&gt;First real-world hurdles&lt;/em&gt;):&lt;/p&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Even more surprisingly: apps displaying error toasts for no apparent reason, or forms flagging validation errors on inputs that are actually correct, without blocking the journey.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.08 — POMBase</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/08-pom-base/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/08-pom-base/</guid>
      <description>&lt;h1 id=&#34;0208pombase&#34;&gt;02.08 — &lt;code&gt;POMBase&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#0208pombase&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/pom/base.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/pom/base.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Framework-agnostic&lt;/strong&gt; abstract base for the Page Object Model. Two required methods. Zero mention of Selenium.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;POMBase&lt;/span&gt;(ABC):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;verify&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, timeout: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;float&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Self:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;get_current_title&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Six bytes of contract (the two signatures). That&amp;rsquo;s all.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-two-methods&#34;&gt;Why two methods&lt;a class=&#34;anchor&#34; href=&#34;#why-two-methods&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;verifytimeoutnone---self&#34;&gt;&lt;code&gt;verify(timeout=None) -&amp;gt; Self&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#verifytimeoutnone---self&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Purpose: &lt;strong&gt;prove we&amp;rsquo;re on the right page&lt;/strong&gt;. Otherwise, raise.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;timeout&lt;/code&gt; lets you wait &lt;em&gt;up to a given time&lt;/em&gt; for the page to load (handy for Selenium &lt;code&gt;WebDriverWait&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;None&lt;/code&gt; default lets the implementation decide (typically reading &lt;code&gt;get_timeout()&lt;/code&gt; which reads the CLI).&lt;/li&gt;&#xA;&lt;li&gt;Returning &lt;code&gt;Self&lt;/code&gt; enables fluent &lt;em&gt;method chaining&lt;/em&gt; (&lt;code&gt;MyPage(...).open().verify().click()&lt;/code&gt;).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The only method test-side that&amp;rsquo;s truly needed to guarantee &amp;ldquo;I&amp;rsquo;m on the page I want.&amp;rdquo; Everything else (&lt;code&gt;click_xxx&lt;/code&gt;, &lt;code&gt;enter_xxx&lt;/code&gt;) is on the subclass.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.09 — Ports: ILogger, ITakeScreenshot</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/09-ports/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/09-ports/</guid>
      <description>&lt;h1 id=&#34;0209ports-ilogger-itakescreenshot&#34;&gt;02.09 — Ports: &lt;code&gt;ILogger&lt;/code&gt;, &lt;code&gt;ITakeScreenshot&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#0209ports-ilogger-itakescreenshot&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source folder: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/tree/main/src/ocarina/ports&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/ports/&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Two ports. That&amp;rsquo;s it. Everything else (&lt;code&gt;WebDriversPool&lt;/code&gt;, &lt;code&gt;Screenshotter&lt;/code&gt;, etc.) lives under &lt;code&gt;infra/&lt;/code&gt;, not &lt;code&gt;ports/&lt;/code&gt;. The line is sharp: a &lt;em&gt;port&lt;/em&gt; is an abstraction the DSL sits on; an &lt;em&gt;infra&lt;/em&gt; is the implementation of &lt;em&gt;adapters&lt;/em&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;ilogger&#34;&gt;&lt;code&gt;ILogger&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#ilogger&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;ILogger&lt;/span&gt;(ABC):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;set_prefix&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, prefix_thunk: Thunk[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;]) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Self: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;set_domain_taxonomy&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, taxonomy: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;tuple&lt;/span&gt;[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;]) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Self: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;raw&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;object&lt;/span&gt;, stream: SupportsWrite[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;**&lt;/span&gt;kwargs: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;object&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;critical&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, msg: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args, exc: Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;**&lt;/span&gt;kwargs) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;error&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, msg: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args, exc: Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;**&lt;/span&gt;kwargs) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;warning&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, msg: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args, exc: Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;**&lt;/span&gt;kwargs) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;info&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, msg: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args, exc: Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;**&lt;/span&gt;kwargs) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;debug&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, msg: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args, exc: Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;**&lt;/span&gt;kwargs) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;test_name&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, msg: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args, exc: Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;**&lt;/span&gt;kwargs) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;success&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, msg: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args, exc: Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;**&lt;/span&gt;kwargs) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;exception&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, msg: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;args, exc: Exception &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;, &lt;span style=&#34;color:#ff79c6&#34;&gt;**&lt;/span&gt;kwargs) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @abstractmethod&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;cleanup&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Method&lt;/th&gt;&#xA;          &lt;th&gt;Semantics&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;set_prefix(thunk)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;strong&gt;Lazy&lt;/strong&gt; prefix applied on every log (for timestamps, threads…). Returns &lt;code&gt;Self&lt;/code&gt; for chaining.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;set_domain_taxonomy(taxonomy)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Sets the domain hierarchy (&lt;code&gt;(cycle, campaign, suite, test)&lt;/code&gt;). Critical for &lt;code&gt;FileLogger&lt;/code&gt;, which builds a file tree.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;raw(*args, stream=None)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Raw write, no format (used by the report plugins).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;critical / error / warning / info / debug&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Standard levels. Every method accepts &lt;code&gt;exc=&lt;/code&gt; to pass an exception and format it.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;test_name(msg)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Custom level: announces the running test (&lt;code&gt;logger.test_name(test.name)&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;success(msg)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Custom level: passed assertion (used by the &lt;code&gt;.success(...)&lt;/code&gt; handlers).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;exception(msg, exc=)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Logs an exception with the full traceback.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;cleanup()&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;End-of-life: flush + close (for &lt;code&gt;FileLogger&lt;/code&gt;). Recycles the log file between retries.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;three-things-that-are-not-in-ilogger&#34;&gt;Three things that are &lt;em&gt;not&lt;/em&gt; in &lt;code&gt;ILogger&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#three-things-that-are-not-in-ilogger&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;No &lt;code&gt;set_level()&lt;/code&gt;&lt;/strong&gt;: level handling is the implementation&amp;rsquo;s call (e.g., &lt;code&gt;MutedLogger&lt;/code&gt; filters everything).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;No &lt;code&gt;add_handler()&lt;/code&gt;&lt;/strong&gt;: no &lt;code&gt;logging.Logger&lt;/code&gt;-style stdlib API. Composition happens by instantiating different loggers (&lt;code&gt;PrintAndFileLogger&lt;/code&gt; wraps both).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;No &lt;code&gt;child(name)&lt;/code&gt;&lt;/strong&gt;: taxonomy is passed &lt;em&gt;as a block&lt;/em&gt; via &lt;code&gt;set_domain_taxonomy&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;set_prefixthunk&#34;&gt;&lt;code&gt;set_prefix(thunk)&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#set_prefixthunk&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;logger&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;set_prefix(&lt;span style=&#34;color:#ff79c6&#34;&gt;lambda&lt;/span&gt;: &lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;[&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;datetime&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;now()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;isoformat()&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;]&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A &lt;code&gt;str&lt;/code&gt; would be computed &lt;em&gt;at &lt;code&gt;set_prefix&lt;/code&gt; time&lt;/em&gt; and frozen. A &lt;code&gt;Thunk&lt;/code&gt; recomputes the prefix &lt;em&gt;on every log call&lt;/em&gt;. Perfect for timestamps.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.12 — Custom types &amp; custom errors</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/12-custom-types-errors/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/12-custom-types-errors/</guid>
      <description>&lt;h1 id=&#34;0212custom-types--custom-errors&#34;&gt;02.12 — Custom types &amp;amp; custom errors&lt;a class=&#34;anchor&#34; href=&#34;#0212custom-types--custom-errors&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Ocarina&amp;rsquo;s &lt;em&gt;shape&lt;/em&gt; layer. No logic — types, aliases, exceptions, protocols. What makes the DSL typed without adding runtime logic.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;custom_types&#34;&gt;&lt;code&gt;custom_types/&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#custom_types&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;File&lt;/th&gt;&#xA;          &lt;th&gt;Contents&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;effect.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;type Effect = Callable[[], None]&lt;/code&gt; + &lt;code&gt;type Effects = tuple[Effect, ...]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;thunk.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;type Thunk[T] = Callable[[], T]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;tpom.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TypeVar TPOM bound POMBase&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;supports_write.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Protocol SupportsWrite[T]&lt;/code&gt; (&lt;code&gt;write(s: T) -&amp;gt; Any&lt;/code&gt;)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;built_web_driver.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;type BuiltWebDriver[Driver] = tuple[Driver, Effect]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;scenario.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Scenario[Driver]&lt;/code&gt; (frozen dataclass)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;test_components.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestChain&lt;/code&gt;, &lt;code&gt;TestSetup&lt;/code&gt;, &lt;code&gt;TestTeardown&lt;/code&gt;, &lt;code&gt;TestWatchers[Driver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;test_runner.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestRunner[Driver]&lt;/code&gt; (frozen dataclass)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;oc_test.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestName&lt;/code&gt;, &lt;code&gt;TestScenario[Driver]&lt;/code&gt;, &lt;code&gt;TestScenarioFragment[Driver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;oc_test_layers.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestId&lt;/code&gt;, &lt;code&gt;TestResult&lt;/code&gt;, &lt;code&gt;TestSuiteResult&lt;/code&gt;, &lt;code&gt;TestSuiteResults&lt;/code&gt;, &lt;code&gt;TestCampaignResults&lt;/code&gt;, &lt;code&gt;TestCycleResults&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;selenium/built_web_driver.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;BuiltSeleniumWebDriver = BuiltWebDriver[WebDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;selenium/oc_test_scenario.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;SeleniumTestScenario = TestScenario[WebDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;selenium/supported_browsers.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;type SupportedSeleniumBrowser = Literal[&amp;quot;chrome&amp;quot;, &amp;quot;firefox&amp;quot;, &amp;quot;edge&amp;quot;, &amp;quot;safari&amp;quot;]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;selenium/web_drivers_pool.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;type SeleniumWebDriversPool = WebDriversPool[WebDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;All these files are &lt;em&gt;excluded from coverage&lt;/em&gt; (&lt;code&gt;pyproject.toml#tool.coverage&lt;/code&gt;):&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
