02.03.01 — The Result[T] type

02.03.01 — The Result[T] type#

Source file: src/ocarina/railway/result.py — zero dependencies.

Code#

from dataclasses import dataclass, field
from typing import TypeGuard, final


class _BaseResult:
    """Base class ensuring both Ok and Fail have error attribute for type narrowing."""
    error: Exception | None


@final
@dataclass(frozen=True)
class Ok[T](_BaseResult):
    value: T
    error: None = None


@final
@dataclass(frozen=True)
class Fail(_BaseResult):
    error: Exception = field(default_factory=lambda: Exception("Unknown error"))


type Result[T] = Ok[T] | Fail


def is_ok[T](result: Result[T]) -> TypeGuard[Ok[T]]:
    return isinstance(result, Ok)


def is_fail[T](result: Result[T]) -> TypeGuard[Fail]:
    return isinstance(result, Fail)

Six design decisions, dissected#

1. _BaseResult as a common parent#

class _BaseResult:
    error: Exception | None

One job: let mypy read result.error without narrowing first. Without the base, you’d have to write:

03.01 — Effect, Thunk[T], Result[T]

03.01 — Effect, Thunk[T], Result[T]#

Three lines in custom_types/ + one in railway/. The whole DSL rides on them.

Triplet#

# src/ocarina/custom_types/effect.py
type Effect = Callable[[], None]
type Effects = tuple[Effect, ...]

# src/ocarina/custom_types/thunk.py
type Thunk[T] = Callable[[], T]

# src/ocarina/railway/result.py
type Result[T] = Ok[T] | Fail
TypeFP semanticsDefinition
EffectSide effect (deferred)() -> None
Thunk[T]Deferred computation with value() -> T (laziness + value)
Result[T]Computation that can failDiscriminated union Ok[T] | Fail

Effect vs Thunk distinction#

log_msg: Effect = lambda: print("hello")        # () -> None
get_42: Thunk[int] = lambda: 42                 # () -> int
fetch:  Thunk[Result[int]] = lambda: Ok(42)     # () -> Result[int]

Function returns something consumed elsewhere → Thunk[T].
Otherwise → Effect.

02.03.02 — The builder state machine

02.03.02 — The builder state machine#

Source file: src/ocarina/dsl/testing_with_railway/internals/action_chain.py

Here’s where the entire ROP mechanic lives: four successive typed states that literally (as in: the type-checker literally) refuse any syntactic improvisation. The DSL doubles as its own immune system.

Overview#

                      ┌─────────────────┐
   start(action)  →   │  ActionStart[T] │     an Action[T] = Thunk[Result[T]]
                      └────────┬────────┘
                               │ .failure(failure_handler)
                               ▼
                      ┌─────────────────┐
                      │ ActionFailure[T]│
                      └────────┬────────┘
                               │ .success(success_handler)
                               ▼
                      ┌─────────────────┐
                      │ ActionSuccess[T]│
                      └────────┬────────┘
                               │ .execute()   ── runs action(), fires the handler
                               ▼
                      ┌─────────────────┐
                      │ ActionChain[T]  │     holds (has_failed, result)
                      └──────┬──────────┘
                             │ .then(next_action_or_start)
              ┌──────────────┴──────────────┐
              │                             │
   has_failed=True                   has_failed=False
              │                             │
              ▼                             ▼
   ┌──────────────────┐         ┌───────────────────┐
   │ NeutralAction-   │         │ ActionStart[T]    │  → cycle starts over
   │ Start[T]         │         │  (the next action │
   │ (failure rail:   │         │   will execute)   │
   │  the whole chain │         └───────────────────┘
   │  becomes no-op,  │
   │  while keeping   │
   │  the fluent API) │
   └──────────────────┘
              │
              ▼ (.failure → .success → .execute, all no-op)
   ┌──────────────────┐
   │  ActionChain[T]  │   has_failed=True, result=<accumulated Fail>
   └──────────────────┘

The types Action, FailureHandler, SuccHandler#

type Action[T]        = Thunk[Result[T]]      # () -> Result[T]
type FailureHandler   = Callable[[Exception], None]
type SuccHandler      = Effect                # () -> None
  • Action[T] is a Thunk. Calling ActionStart(action) does not run anything. The action just gets captured.
  • FailureHandler gets the exception, not the Fail or the Result. Deliberate: 99% of handlers want to log the exception, take a screenshot, and move on.
  • SuccHandler is an Effect (no argument). Same idea — a success handler logs a static message, snaps a screenshot, doesn’t need the result.

Typestate Pattern: why you can’t get it wrong#

The chain is strictly linear. Each method returns a different type that only exposes the next legal step:

01.03 — KISS and ostentatious complexity

01.03 — KISS and ostentatious complexity#

The criticism that landed#

First public feedback Ocarina got, systematic per the author:

The first “criticisms” were often the same: “You’re bragging about something that’s very simple.”

The Holy Book takes its time answering. Three pillars: the operating nature of simplicity, the simple ≠ unsophisticated distinction, and the rejection of every “creative” counter-proposal that came in.

Simple ≠ unsophisticated#

This is also why KISS (Keep it simple, stupid) is so misunderstood in the industry. Many assume “simple” means “unsophisticated.” It would be hard to misread a principle more thoroughly.

Chapter 02 — Ocarina, the framework

Chapter 02 — Ocarina, the framework#

Unpacks the whole ocarina framework (Python 3.14+, v1.1.10) and Railway Oriented Programming straight through to the reporting plugins. Structured as a layered walk: deepest (the Result[T] type) to most visible (the bootstrap that boots everything).

Chapter plan#

#SubjectFile or folder
01Technical identity, dependencies, toolchain01-identity.md
02Full module tree + layered diagram02-module-tree.md
03Railway Oriented Programming03-railway/
04Invariants (validate, assertions, chain_validations)04-invariants/
05Orchestration (Test → TestSuite → TestCampaign → TestCycle)05-orchestration/
06Lifecycle of a Scenario06-scenario.md
07Watcher07-watcher.md
08POMBase08-pom-base.md
09Ports & adapters (ILogger, ITakeScreenshot)09-ports.md
10Infrastructure (pool, builder, screenshotter, act counter, Selenium adapters)10-infra/
11Opinionated layer (CLI, loggers, plugins, bootstrap)11-opinionated/
12Custom types & custom errors12-custom-types-errors.md

Chapter 02.03 — Railway Oriented Programming

Chapter 02.03 — Railway Oriented Programming#

Ocarina’s beating heart. The whole DSL rides on this mechanic: success and failure as two parallel rails. A failure switches the train onto the failure rail and locks it there (short-circuit), but composition stays linear so the scenario still reads top-to-bottom.

Plan#

#FileSubject
0101-result.mdResult[T] = Ok[T] | Fail, is_ok/is_fail
0202-action-chain-states.mdState machine ActionStart → ActionFailure → ActionSuccess → ActionChain.
0303-neutral-rail.mdFailure rail: NeutralActionStart / NeutralActionFailure / NeutralActionSuccess.
0404-chain-actions-fold.mdChainRunner[T] (thunk) + chain_actions (fold).
0505-create-act-hooks.mdcreate_act and its hooks (on_failure, on_run_effect, act_counter_effect).
0606-drive-page.mddrive_page, a semantic alias.
0707-match-page-when.mdmatch_page / when + exception policy.

Lineage#

Direct inspiration: Railway Oriented Programming, popularized by Scott Wlaschin (F#).

01.04 — Citations and claimed influences

01.04 — Citations and claimed influences#

The references cited in the Holy Book aren’t neutral — they set Ocarina’s theoretical frame. We cut them by axis here, then offer the overall reading.

Technical axis — Functional programming, types, languages#

InfluenceOriginRelation to Ocarina
Railway Oriented Programming (ROP)Scott Wlaschin (F#), a well-known pattern in functional programmingThe heart of the framework. Result[T] = Ok[T] | Fail, short-circuit, fluent builder. See ../02-ocarina/03-railway/.
λ-calculus (1930)Alonzo ChurchCited as the invention that makes possible the “clean formalization” Ocarina pursues: “What has been on our minds since the 1930s, since the invention of λ-calculus, is finally scalable to the degree we always wanted it to be.
McCulloch & Pitts (1943)First formalization of the artificial neuronCited as a reminder that the real progress in AI is old and continuous, against the recent hype.
Python type system (PEP 695)PEP 695 (Python 3.12+)Prerequisite for the generic parametric typing that structures Ocarina (TestSuite[Driver], Result[T], etc.). See ../03-functional/06-pep-695-generics.md.

Cultural axis — Anti-narcissism, simplicity, KISS done right#

InfluenceCitationWhy
Terry Davis (“the smartest programmer that’s ever lived”)“An idiot admires complexity, a genius admires simplicity, a physicist tries to make it simple…”Anti–ostentatious complexity argument; cornerstone of the “First feedbacks” chapter.
Lao-Tzu“To attain knowledge, add things every day. To attain wisdom, remove things every day.”A subtractive method: what remains after elimination.
Antoine de Saint-Exupéry“Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.”Western restatement of the previous one. Closes the “First scenarios” chapter.
Alan Watts“Muddy water is best cleared by leaving it alone.”Cited at the end of the “First jutsus” chapter: legitimizes a design that refuses certain features (reactivity, async). Legitimizes stepping out of the “geek tricks” ecosystem.
Rainer Maria Rilke“Live the questions now. Perhaps then, someday far in the future, you will gradually, without even noticing it, live your way into the answer.”Closes the “First steps” chapter. Stance toward practice: mastery comes from use.
Marcel Proust“For the writer as well as for the painter, style is not a question of technique, but of vision.”Closes the “Extensibility” chapter. Justifies the refusal to impose a DSL: vision (POMs + actions) trumps technique (a format).

Political axis — Code, sovereignty, anti–startup nation#

InfluenceOriginRelation to Ocarina
Code is LawLawrence Lessig (1999), reclaimed by Ethereum (2015) as a positive idealCited as the stance against “democratic governance of code”. The project claims it: “Once again, with AI, that sorely missing digital body, let us scream it, as loud as we screamed ‘HACK THE PLANET’ back in ‘99: CODE IS LAW.
DHH — I won’t let you pay me for my open sourceDavid Heinemeier Hansson, personal blogCited to legitimize refusing unaligned contributions (“Fuck You” references DHH’s famous slide).
Paul Graham — HatersPG essay (paulgraham.com/fh.html)Grounds the decision to “never waste time arguing when it isn’t worth it”.
Yegor Bugayenko — PromptThe quote “Retry flaky blocks.Grounds the transient_errors + linear retry policy (TestFlow). Read also: Angry Tests.
The DAO hack (2016)Hack of The DAO on EthereumCited to point out that it was bugs (“damned bugs, because of damned devs”) that set Code is Law back, hence the importance of typing and rigor.
Lee Robinson (ex-Vercel)December 2025: Cursor replacing Sanity with Markdown files + AIBacks the “return to raw data”.
Cluely (Roy Lee)Cited as an illustration of entrepreneurial bullshit to avoidExplicit contrast with the Ocarina philosophy.

Identity / underground axis#

InfluenceOrigin
LulzSec — “In the Lulzboat, salute, bitch, and show some respect.”AntiSec (YTCracker, 2011)
“We Can Do All What You Can’t Do”YogyaCarderLink
YTCracker — Robots Will Definitely Take Your JobSoundCloud link, background music of the “First feedbacks” chapter
“HACK THE PLANET” (1999)Emblematic slogan of the 1990s hacker scene + AntiSec (YTCracker, 2011)
Zone-HHistoric web defacement archive
r/unixporn, r/AntiTaffSubreddits

AI / modern tooling axis#

InfluenceOriginRelation to Ocarina
Claude CodeAnthropicReference tooling for the AI project (ocarina-with-ai-example). Cited several times as the “bridge” replacing DSLs.
IntelliCode (2018)MicrosoftCited as a temporal landmark (“we had already tasted it, well before IntelliCode”).
R., the silo’s mad scientistPersonal anecdote (a 2013 colleague, a private Emacs AI plugin)Reminder that generative AI (“15,000 lines to delete to avoid writing 1,000”) existed before the public hype.
Prisma, Vercel, CursorSaaS ecosystemCited as practical cases of an AI-assisted return to raw data.

Overall coherence#

All these influences converge on three axes:

02.03.04 — chain_actions and ChainRunner

02.03.04 — chain_actions and ChainRunner#

Source file: src/ocarina/dsl/testing_with_railway/chain_actions.py

The primitive that makes the DSL “flat.” Without it, an N-step scenario is a staircase of .then(...).failure(...).success(...).execute(). With it, it’s a list.

“Parenthesis hell”#

Solution:

# Flat, readable syntax
runner = chain_actions(
    action1.failure(h1).success(h2),
    action2.failure(h3).success(h4),
    action3.failure(h5).success(h6)
)
chain = runner.run()  # Execute when ready

Benefits:

  • Lazy evaluation: Build chain without executing
  • Flat syntax: No deep nesting
  • Automatic short-circuiting: Stops on first failure
  • Composability: ChainRunner is a value

(Excerpt from the docstring.)

Chapter 02.04 — Invariants

Chapter 02.04 — Invariants#

Ocarina’s sub-DSL for typed, composable invariants. Used everywhere: CLI, POMs, the custom_invariants/testing/ that check suite consistency before any test runs.

Plan#

#FileSubject
0101-validate-flow.mdThe full flow validate(value) → assert_that → execute → raise_if_invalid + the _ValidationChain class.
0202-assertions.mdCatalog of builtin assertions (is_str, is_email, is_positive, is_in, has_unique_elements, each, is_valid_filename, …).
0303-otherwise-any-of.md.otherwise(...) and the OR combination via _any_of.
0404-then-chain-of-validations.md.then(new_value) and chain_validations(...) for composition.
0505-business-vs-framework-validator.mdBusinessInvariantValidator vs FrameworkInvariantValidator
0606-invariant-errors.mdInvariantViolationError, DuplicatesError, AggregateInvariantViolationError.

Why the invariants DSL#

  1. Pre-execution guard. TestSuite.__init__ checks — before launching anything — that test names / IDs are unique, max_workers >= 1, etc.
  2. CLI validation. Every flag carries its validate=lambda chain: chain.assert_that(...) inside the CliStore. Errors aggregate into one message.
  3. POM-side validation. POM actions validate their parameters ("retries must be positive") via validate(retries, name="retries").assert_that(is_positive).execute().raise_if_invalid().

The Holy Book sums it up:

02.03.06 — drive_page

02.03.06 — drive_page#

Source file: src/ocarina/opinionated/dsl/drive_page.py

Code#

def drive_page(
    first: ActionSuccess[TPOM], *rest: ActionSuccess[TPOM]
) -> ChainRunner[TPOM]:
    return chain_actions(first, *rest)

Literally chain_actions.

Why this alias?#

1. Semantics: “I’m taking control of a page”#

Holy Book quote (“First scenarios”):

drive_page expresses the fact that you are taking control of one page. Any transition becomes explicit through the opening of a new drive_page.

return [
    drive_page(
        act(on_homepage, open_homepage)...,
        act(on_homepage, verify_homepage)...,
        act(on_homepage, click_cta)...,
    ),                                          # ⬅ closing control of the homepage
    drive_page(                                 # ⬅ opening control of the next page
        act(on_target_page, verify_target_page)...,
    ),
]

2. Discipline#

Mixing acts from two different POMs inside one drive_page is a mypy error (see 02-action-chain-states.md, “narrowing via act()” section). The semantics get enforced concretely.