ATLAS OBSCURA BOOKS
A Visual Odyssey Through the Marvels of Life
Venture into Nature's Unseen Realms with Our New Book Atlas Obscura: Wild Life Order Now
Who does things like that?!?
Apparently: me.
I have my own project, written in Rust. Not a big one, mind you, maybe approx. 30k lines of code in total. Rust is verbose so it’s not really that impressive. I’ve put it aside for some time and was toying with local inference, LLMs, writing agents and my attention was brought to Ruby.
It’s been a while. So I had to take a look around to remind myself what Ruby and Ruby on Rails are doing nowadays. They’re doing quite well. There are some typing initiatives (Sorbet), and the language itself is terse as ever.
And then I had this thought… But an introduction is in order first: In my Rust app I have an isolated crate that’s pretty much a webapp written with Tera and Axum. 14,943 lines of Rust code in total, around 10s of compilation time (maybe the code isn’t big, but it pulls the whole universe behind itself) and then quite heavy E2E tests involving setting up Playwright and (because of the near-impossibility of mocking) an isolated database namespace and mocking services (along with a very special internal-api crate that allows Playwright to interact with the app in headless mode…).
So I thought “hmm, I wonder if I can get my Local Qwen3.6 to do a oneshot conversion”. But before I did so I researched first. I asked a few instances to analyse the project in terms of gains of complexity, stability, testability, etc., and while (obviously) stability would drop (no types in Ruby) it’s not that awful (Sorbet has types in Ruby!).
┌─────────────────────────────────┬──────────────────┬───────┬────────────────┐
│ Area │ Rust/Axum/Diesel │ Rails │ Rails + Sorbet │
├─────────────────────────────────┼──────────────────┼───────┼────────────────┤
│ Suitability for solo dev │ 60 │ 90 │ 85 │
├─────────────────────────────────┼──────────────────┼───────┼────────────────┤
│ Development speed │ 40 │ 90 │ 75 │
├─────────────────────────────────┼──────────────────┼───────┼────────────────┤
│ Safety │ 95 │ 55 │ 80 │
├─────────────────────────────────┼──────────────────┼───────┼────────────────┤
│ Development complexity │ 70 │ 90 │ 75 │
├─────────────────────────────────┼──────────────────┼───────┼────────────────┤
│ Performance │ 95 │ 50 │ 50 │
├─────────────────────────────────┼──────────────────┼───────┼────────────────┤
│ Boilerplate │ 30 │ 85 │ 80 │
├─────────────────────────────────┼──────────────────┼───────┼────────────────┤
│ E2E testing testability │ 40 │ 75 │ 75 │
├─────────────────────────────────┼──────────────────┼───────┼────────────────┤
│ Unit testability │ 20 │ 90 │ 90 │
├─────────────────────────────────┼──────────────────┼───────┼────────────────┤
│ Integration testing testability │ 30 │ 85 │ 85 │
├─────────────────────────────────┼──────────────────┼───────┼────────────────┤
│ Sum │ 480 │ 710 │ 695 │
└─────────────────────────────────┴──────────────────┴───────┴────────────────┘
So in the end it seems I have (licks finger and turns to the wind) 1.47x better outcomes if the app were a Ruby on Rails app instead.
I have a local LLM running on my (bought it for gaming pre-AI craze) 4090 Ti1 - I’m a free man with unlimited tokens2. So I thought: BRING IT ON!
Since it is a relatively small project the conversion took ~30 minutes. I have no idea if it works or not because I haven’t yet tried running it. But there is one thing I checked, and stared at in horror:
$ fd . -e rs -uu | xargs cat | wc -l
14943
$ fd . -e rb -uu | xargs cat | wc -l
3322
That’s right folks! 77% decrease in line count; 4.49 lines of Rust code for each line of Ruby.
I browsed the Ruby code and it looks… fine. There are probably some bugs (no bunnies) but I must say it’s looking clean and idiomatic for my dated eye. I’m going to examine it further with some things in mind:
I can add types using Agents, so probably type safety can be alleviated
Ruby/Rails is pretty much batteries+kitchen sink included, which beats 3GiB of compiled deps.
Testing will be SO MUCH EASIER
VCR.use_cassette("llm_call") do
result = LlmClient.match(entry, data_list)
expect(result.results.size).to eq(data_list.size)
end
vs
#[derive(Debug)]
pub struct MockProvider {
responses: Arc<RwLock<Vec<Response>>>,
call_count: Arc<AtomicUsize>,
}
impl Default for MockProvider {
fn default() -> Self {
Self {
responses: Arc::new(RwLock::new(vec![Response::default()])),
call_count: Arc::new(AtomicUsize::new(0)),
}
}
}
impl MockProvider {
pub fn new(responses: Vec<Response>) -> Self {
Self {
responses: Arc::new(RwLock::new(responses)),
call_count: Arc::new(AtomicUsize::new(0)),
}
}
}
#[async_trait]
impl Provider for MockProvider {
async fn match(&self, entry: &Entry, data_list: &[Data]) -> Result<MatchResult> {
self.call_count.fetch_add(1, Ordering::SeqCst);
let responses = self.responses.read().await;
Ok(MatchResult { results: responses.clone() })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mock_provider_returns_expected_results() {
let expected = vec![Response::default()];
let provider = MockProvider::new(expected.clone());
let result = provider.match(&Entry::default(), &[]).await.unwrap();
assert_eq!(result.results, expected);
assert_eq!(provider.call_count.load(Ordering::SeqCst), 1);
}
}
…you get the vibe, right?
The benefits of working on your own project are, well, you can make crazy decisions, and I’ll be looking at this one very closely.
🎯 BEMoji v1.0.0-beta
RFC DRAFT
Block · Element · Modifier · 🎉
BEMoji is a production-grade utility and component framework built entirely on emoji class names. Semantic, systematic, and completely illegible to anyone without the docs.
<!-- A card component in BEMoji --> <article class="🃏"> <div class="🃏__🖼️ 🃏__🖼️--🌟"> <img src="hero.jpg" alt="..."> </div> <div class="🃏__📝"> <h2 class="🃏__🔠">Card Title</h2> <p class="🃏__💬">Card description text</p> </div> <footer class="🃏__🦶"> <button class="🔘 🔘--🌟">Primary</button> <button class="🔘 🔘--👻">Disabled</button> </footer> </article> <!-- Responsive: 📱 = mobile-only --> <div class="📐💠 💻📐🔲"> Grid col-1 on mobile, col-2 on desktop </div>
/* ── Card Block ── */ .🃏 { border-radius: 8px; overflow: hidden; box-shadow: var(--🌑-md); background: var(--⬜); } /* Element: image */ .🃏__🖼️ { width: 100%; aspect-ratio: 16 / 9; overflow: hidden; } /* Modifier: featured */ .🃏__🖼️--🌟 { outline: 3px solid var(--⭐-gold); outline-offset: -3px; } /* Design tokens use emoji too */ :root { --⬜: #ffffff; --⬛: #0d0d0f; --🌑-sm: 0 1px 3px rgba(0,0,0,.12); --🌑-md: 0 4px 12px rgba(0,0,0,.15); --📏-1: 0.25rem; --📏-4: 1rem; --📏-8: 2rem; }
// bemoji.config.js export default { version: '1.0', /** Map readable names → emoji tokens */ blocks: { card: '🃏', navbar: '🧭', modal: '🪟', alert: '🔔', form: '📋', table: '📊', }, elements: { image: '🖼️', title: '🔠', body: '📝', footer: '🦶', button: '🔘', input: '📥', }, modifiers: { primary: '🌟', danger: '🔴', success: '🟢', ghost: '👻', loading: '⏳', }, separator: { element: '__', modifier: '--', } };
// With the Babel transform, write readable // BEM names — they compile to emoji at build time. import { bem } from 'bemoji/react'; const Card = ({ featured, children }) => ( <article className={bem('card')}> <div className={bem('card__image', { featured })}> {children.image} </div> <div className={bem('card__body')}> {children.body} </div> </article> ); // Compiles to: // className="🃏" // className="🃏__🖼️ 🃏__🖼️--🌟" (when featured) // className="🃏__📝"
01 — Core Concepts
BEMoji inherits BEM's three-layer architecture exactly. The only change is that every identifier — blocks, elements, modifiers, utilities, breakpoints, and design tokens — is an emoji.
🧱
Block
A standalone, self-contained component. The root of a component tree. Blocks should be reusable and carry no inherited context.
.🃏 .🧭 .📋 .🪟
🔩
Element
A part of a block that has no standalone meaning. Always expressed as block__element. Elements cannot exist outside their block.
.🃏__🖼️ .🧭__🔗 .📋__📥
🎨
Modifier
A flag that changes appearance or behavior. Applied alongside the base class using block--modifier or block__element--modifier.
.🃏--🌟 .🔘--🔴 .📥--👻
02 — Naming Anatomy
Every BEMoji class name follows a strict, parseable structure. The double-underscore and double-hyphen delimiters are preserved from BEM, making the format machine-readable even if it isn't human-readable.
Note on escaping: Modern browsers parse emoji in CSS class selectors without escaping. For maximum compatibility (older Webkit, certain CSS-in-JS parsers), the BEMoji compiler can output escaped unicode: .🃏 → .\01F0CF. The toolchain handles this automatically.
| Emoji | Role | Full class | Equivalent BEM |
|---|---|---|---|
| 🃏 | Block: card | .🃏 | .card |
| 🖼️ | Element: image | .🃏__🖼️ | .card__image |
| 🌟 | Modifier: featured | .🃏__🖼️--🌟 | .card__image--featured |
| 🔴 | Modifier: danger | .🔘--🔴 | .button--danger |
| 👻 | Modifier: disabled | .📥--👻 | .input--disabled |
| 💻🔲 | Responsive utility | .💻📐🔲 | .lg\:grid-cols-2 |
03 — Emoji Vocabulary
The BEMoji spec defines 143 reserved emoji tokens across blocks, elements, modifiers, states, and design tokens. Teams can extend this with custom emoji via the config, provided they don't collide with reserved tokens.
🧱 Blocks — Components
🃏cardpanel, tile
🧭navbarnav, header
🦶footer-
📋form-
🪟modaldialog, overlay
🔔alertnotification
🏷️badgetag, chip
💬tooltippopover
📊table-
📑tabs-
🎠carouselslider
🍞breadcrumb-
🔩 Elements — Parts
🖼️imageimg, media
🔠titleheading, h
📝bodycontent, text
🦶footeractions
🔘buttonbtn, cta
📥inputfield, control
🔗linkanchor, a
🏷️label-
🎭icon-
📄itemrow, entry
🖇️dividerseparator, hr
🔭prefixprepend
🎨 Modifiers — States
🌟primaryfeatured, hero
🔴dangererror, destructive
🟢successok, valid
🟡warningcaution
🔵info-
👻disabledghost, muted
✅activeselected, on
⏳loadingpending, busy
🔒lockedreadonly
💎premiumpro, upgrade
🆕newfresh, recent
🕶️darknight
🎛️ Modifiers — Size
🔬xsextra-small
🤏smsmall
⚖️mdmedium, default
🏋️lglarge
🏔️xlextra-large
🌍2xlfull, max
04 — Design Tokens
BEMoji's token system uses emoji as CSS custom property names. This means the theme layer is as illegible as the class layer — one consistent obfuscated aesthetic throughout your entire codebase.
Color Tokens
| ⬛ | --⬛ | Base dark / ink |
| ⬜ | --⬜ | Base light / paper |
| 🔴 | --🔴 | Danger / error |
| 🟢 | --🟢 | Success / ok |
| 🟡 | --🟡 | Warning |
| 🔵 | --🔵 | Info / accent |
| 🟣 | --🟣 | Purple / brand |
| 🟠 | --🟠 | Orange |
Spacing Scale
| 📏 | --📏-1 | 0.25rem (4px) |
| 📏 | --📏-2 | 0.5rem (8px) |
| 📏 | --📏-4 | 1rem (16px) |
| 📏 | --📏-6 | 1.5rem (24px) |
| 📏 | --📏-8 | 2rem (32px) |
| 📏 | --📏-12 | 3rem (48px) |
| 📏 | --📏-16 | 4rem (64px) |
Typography
| ✍️ | --✍️-xs | 0.75rem |
| ✍️ | --✍️-sm | 0.875rem |
| ✍️ | --✍️-base | 1rem |
| ✍️ | --✍️-lg | 1.125rem |
| ✍️ | --✍️-xl | 1.25rem |
| ✍️ | --✍️-2xl | 1.5rem |
| ✍️ | --✍️-4xl | 2.25rem |
Shadows & Radius
| 🌑 | --🌑-sm | Subtle shadow |
| 🌑 | --🌑-md | Medium shadow |
| 🌑 | --🌑-lg | Large shadow |
| 🌑 | --🌑-inner | Inset shadow |
| ⭕ | --⭕-sm | border-radius: 4px |
| ⭕ | --⭕-md | border-radius: 8px |
| ⭕ | --⭕-full | border-radius: 9999px |
05 — Responsive System
Responsive utilities use an emoji breakpoint prefix, separated from the utility by a zero-width joiner (U+200D). This keeps class names as a single unicode "word" from the browser's perspective, while remaining visually parseable.
📱
xs
0 – 639px
Mobile first (no prefix)
📐💠
📟
sm
640px+
Large mobile / phablet
📟📐🔲
📲
md
768px+
Tablet
📲📐🔲
💻
lg
1024px+
Laptop / desktop
💻📐🔳
🖥️
xl
1280px+
Wide desktop
🖥️📐⬛
/* Generated by the BEMoji compiler */ @media (min-width: 640px) { .\01F4DF \01F4D0 \01F532 { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 1024px) { .\01F4BB \01F4D0 \01F533 { grid-template-columns: repeat(3, 1fr); } } /* Or with modern emoji CSS (most environments) */ @media (min-width: 640px) { .📟📐🔲 { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 1024px) { .💻📐🔳 { grid-template-columns: repeat(3, 1fr); } }
06 — Component Library
BEMoji ships with 24 production-ready components pre-built in the framework stylesheet. Each follows the emoji naming convention throughout, with full modifier support.
🃏
Card Title
Supporting body text goes here
🃏 card — .🃏 .🃏__🖼️ .🃏__🔠 .🃏__📝
🌟 Primary 🔴 Danger 🟢 Success 🟡 Warning
🏷️ badge — .🏷️ .🏷️--🌟 .🏷️--🔴 .🏷️--🟢
🔘 button — .🔘 .🔘--🌟 .🔘--🔴 .🔘--👻
🔴
Something went wrong. Please try again or contact support.
🔔 alert — .🔔 .🔔--🔴 .🔔__💬 .🔔__🔘
📥 input — .📥 .📥--👻 .📥--🟢 .📥--🔴
🍞 Home / Products
🍞 Home / Cats / Tabby
🍞 breadcrumb — .🍞 .🍞__📄 .🍞__📄--✅
07 — Tooling Ecosystem
BEMoji ships with a full suite of build tools, editor integrations, and framework adapters. You write readable names in development; the compiler handles the emoji transformation at build time.
Transforms human-readable BEM class names in your CSS source to emoji equivalents at build time. Supports custom mappings, escaped output, and source maps.
// postcss.config.js
module.exports = {
plugins: [
require('bemoji-postcss')({
config: './bemoji.config.js',
escape: 'auto', // 'raw' | 'unicode'
sourceMap: true,
})
]
}
Zero-config Vite integration. Transforms class names in HTML, JSX, TSX, and Vue templates. Hot module reloading works transparently with emoji class names.
// vite.config.ts
import bemoji from 'vite-plugin-bemoji';
export default {
plugins: [
bemoji({
config: './bemoji.config.js',
include: ['**/*.{tsx,jsx,vue,html}'],
})
]
}
IntelliSense for BEMoji class names. Hover over an emoji class to see its human-readable equivalent. Autocomplete from your config, lint unknown tokens, and toggle between emoji/readable views.
// .vscode/settings.json
{
"bemoji.showTooltips": true,
"bemoji.autocomplete": "emoji+readable",
"bemoji.lintUnknownTokens": "warn",
"bemoji.hoverFormat": "both",
"bemoji.configPath": "./bemoji.config.js"
}
Enforces correct BEMoji patterns. Warns on unknown emoji tokens, invalid block–element–modifier hierarchies, missing block context on elements, and disallowed multi-emoji sequences.
// .eslintrc.js
{
"plugins": ["bemoji"],
"rules": {
"bemoji/no-unknown-tokens": "error",
"bemoji/no-orphan-elements": "warn",
"bemoji/no-modifier-without-base": "error",
"bemoji/prefer-semantic-emoji": "warn"
}
}
The bem() helper resolves readable block/element/modifier strings to emoji class names at runtime (or at compile time with the Babel transform). Works with conditional modifiers via object syntax.
import { bem } from 'bemoji/react';
// Runtime usage
bem('card') // → '🃏'
bem('card__image') // → '🃏__🖼️'
bem('card', { primary }) // → '🃏 🃏--🌟'
bem('card__image', {
featured: true,
loading: false // → '🃏__🖼️ 🃏__🖼️--🌟'
})
Scaffold new projects, compile CSS, audit for unused tokens, generate a Storybook story per component, and export the full emoji→readable mapping as a JSON reference sheet.
npx bemoji init # Scaffold + config npx bemoji compile # Transform CSS files npx bemoji audit # Check for unused tokens npx bemoji export --fmt json > tokens.json npx bemoji storybook # Generate component stories npx bemoji decode "🃏__🖼️--🌟" # → card__image--featured
08 — Design Philosophy
BEMoji is a genuinely considered tradeoff, not a joke with no payoff. Here's the honest case for and against.
01
Free obfuscation
Production CSS with emoji class names is meaningless to scrapers, competitors, and client-side inspectors. You get the obfuscation benefits of CSS modules or hashed class names with none of the build complexity — it's just the class names themselves.
02
Enforced vocabulary
Because every UI concept maps to a single, canonical emoji, naming arguments become arguments about which emoji is most semantically correct — which is a much shorter argument than any discussion of BEM naming conventions.
03
Config is the contract
The bemoji.config.js file is a living contract between your design system and your codebase. Changing a concept's emoji is a single-line config change that propagates everywhere, unlike refactoring a string-based class name across thousands of files.
04
Developer velocity
Emoji are faster to type than long BEM class strings once you have input method shortcuts. 🃏 takes two keystrokes on most OS emoji pickers. .card__image--featured takes 24. The math eventually works out.
Honest caveat: BEMoji is not appropriate for every team or project. If your team doesn't control the config file, emoji class names become a liability rather than an asset. It also requires editor tooling buy-in from every contributor. The VS Code extension and ESLint plugin exist specifically to make this tractable.
09 — Framework Comparison
| Feature | BEMoji | Tailwind CSS | BEM (vanilla) | CSS Modules |
|---|---|---|---|---|
| Zero runtime JS | ✓ | ✓ | ✓ | ✓ |
| Automatic obfuscation | ✓ | ✗ | ✗ | Build-time only |
| Semantic naming | ✓ | ✗ | ✓ | ✓ |
| Responsive utilities | ✓ | ✓ | ✗ | ✗ |
| Design token system | ✓ | ✓ | ✗ | ✗ |
| Human-readable source | Via build tool | ✓ | ✓ | ✓ |
| Pre-built components | ✓ | ✗ | ✗ | ✗ |
| Bundle size | ~4kb gzip | ~10kb purged | 0kb (DIY) | 0kb (DIY) |
| Colleague confusion | Maximum | Moderate | Minimal | Minimal |
10 — Get Started
Step 01
Install the package
npm install bemoji # or yarn add bemoji # or pnpm add bemoji
Installs the core framework, PostCSS plugin, and CLI. Peer deps are PostCSS 8+ and Node 18+.
Step 02
Initialise your config
npx bemoji init # Creates: # ├── bemoji.config.js # ├── bemoji.css (base styles) # └── postcss.config.js
The init wizard lets you pick a pre-built vocabulary or define your own token set from scratch.
Step 03
Import and build
/* main.css */
@import 'bemoji/base';
@import 'bemoji/tokens';
@import 'bemoji/components';
/* Then write your own BEM: */
.[card] { border-radius: var(--⭕-md); }
.[card__image--featured] {
outline: 2px solid var(--🟡);
}
The PostCSS plugin compiles [card] shorthand to emoji class names automatically at build time.
The universal architecture emerging across all frontier agentic systems.
Claude Code, OpenAI Codex, Gemini CLI, LangGraph, CrewAI, Google ADK, Amazon Bedrock — built by different companies, in different languages, under different constraints. They converged on the same design.
Not because they copied each other. Because the constraints are physics. Finite context windows. Tools that need a protocol. Safety that can’t depend on the model obeying. Tasks too complex for a single invocation. Any team that builds long enough arrives here.
The patterns in this guide apply universally, but their weight depends on which seam in the agent ecosystem you’re working on. Read in the order that matches your problem.
| If you are building… | You care most about… | Start with |
|---|---|---|
| A domain context substrate (an MCP server that gives any agent structured access to one domain: a codebase, a screen, a system) | Deterministic extraction, fixed ontology, behavior contracts installed at the user’s project | /tool-protocols, /instructions, /anti-patterns |
| A personal AI runtime (an agent that the user owns, that runs in the background, with long-running state) | Memory architecture, compaction-resident state, hooks, scheduler-gated background work | /memory, /enforcement, /multi-agent |
| A multi-agent shell (an orchestrator over other people’s agents, with chat-platform reach) | Adapter patterns, isolated sub-agent tool registries, settings architecture, cost controls | /multi-agent, /enforcement, /cost-management |
These categories aren’t airtight — many systems blur them. But knowing which one is your load-bearing concern keeps you from over-applying patterns that don’t fit your seam.
These are not suggestions. They are the load-bearing walls of every production agentic system. Violate them and you will rediscover why they exist.
| # | Postulate | What to do |
|---|---|---|
| 1 | Start with a persistent instruction file | Create a CLAUDE.md, AGENTS.md, or GEMINI.md before writing any agent config. Cover conventions, stack, testing, git, and security. Keep it under 200 lines. |
| 2 | Enforce safety outside the prompt | Put style preferences in the instruction file. Put linting in hooks. Put destructive command blocking in permissions. Never rely on the model remembering a safety rule. |
| 3 | Budget your context window | Reserve 10-15% for instructions, 30-40% for conversation, 20-30% for tool results. Compact at 70%. Clear at 80%. Separate cacheable content from compactable content. |
| 4 | Build tools on MCP | Use .mcp.json for tool connections. 97M+ downloads/month across every major platform. If you need agent-to-agent communication across systems, add A2A — but start with MCP. |
| 5 | Coordinate through shared state | Within a system, agents read from and write to shared state — not messages to each other. Between systems or organizations, use messaging protocols (A2A). Default to state; reach for messaging only when you must. |
| 6 | Decompose before you hit the cliff | Agent coherence degrades after extended sessions. The threshold moves with each model generation. Don’t find the limit — stay well under it. Break work into sub-tasks that complete in the safe zone. |
| 7 | Track cost per task from day one | Set token budgets per session. Route simple work to cheap models. Cache stable prompts. Monitor with alerts at 50%, 75%, and 90% of budget. Cost management is infrastructure, not optimization. |
| 8 | Add complexity in weekly increments | Week 1: instruction file. Week 2: hooks. Week 3: MCP tools. Week 4: skills. Month 2+: sub-agents. If your team has distributed systems experience, you can move faster — but still validate each layer before adding the next. |
| Role | What you get |
|---|---|
| Agent developers | Patterns for instruction files, hooks, MCP tools, and context management. |
| Platform engineers | Multi-agent architecture, shared state, delegation, and cost controls. |
| Infrastructure teams | Observability, token accounting, safety enforcement, and production runbooks. |
| Engineering managers | Adoption roadmaps, cost models, and risk frameworks. |
| Section | Key questions answered |
|---|---|
| Prompt | What does the agent read at session start? What does the harness compile around it? |
| Control | How do you bind the agent’s behavior outside the prompt? |
| Context | What does the agent remember? How do multiple agents coordinate? |
| Interface | How does the agent talk to tools, code, the web, and editors? |
| Operate | How do you run it in production — cost, observability, credentials, lifecycle? |
| Anti-Patterns | What failure looks like — named and citable. |
First agent? Start with Prompt → Control. Skip Context until one agent works reliably.
Scaling? Jump to Context and Operate. That’s where the failure modes live.
Production generative AI engineering, applied to the industries, jurisdictions, and use cases buyers actually face.
Building a Minsky Machine in Atlassian Automation
22nd May 2026
Engineering folklore holds that Jira (Atlassian's project-tracking tool) is Turing-complete. Existing claims point vaguely at automation features without exhibiting a reduction. This article supplies a proof, with setup instructions and execution trace.
A Minsky register machine needs only two unbounded counters and a finite set of labeled instructions:
INC r; goto SDEC r; if r == 0 goto S else goto S'Or, in plain English:
A Minsky program that adds register A into register B looks like:
1. DEC A; if A == 0 goto 3 else goto 2
2. INC B; goto 1
3. HALT
Minsky proved this model Turing-complete (1967). Exhibiting it in Jira's automation language therefore establishes the reduction. Here is how the model maps onto Jira:
| Minsky Machine | Jira |
|---|---|
| Register A | Count of linked issues of type Bug |
| Register B | Count of linked issues of type Task |
| Program Counter | Status of a single Epic issue |
| Dispatch Table | Jira Automation rules, one per instruction state |
| Clock | Automation-triggered transitions, or external re-triggering past chain caps |
The Epic's status encodes the current instruction.
Automation rules inspect the linked-issue counts and decide the next status.
INC and DEC are implemented as issue creation and deletion on the appropriate linked-issue type.
Conditional branching is implemented as a JQL-conditioned rule.
Here is a minimal working implementation using one Epic, five linked issues, and one Automation rule per instruction state (Space Settings > Automation).
1. Create Workflow
Create a Jira Workflow with statuses initial state BACKLOG, then TODO, DEV and PROD.
Any state can transition to any other.
Create an Epic in status BACKLOG.
2. Create Rule for TODO
DEC A; if A=0 halt, else goto DEV.
TODO.DEV.PROD (halt).3. Create Rule for DEV
INC B; goto TODO.
DEV.TODO.Both rules have "Allow rule to trigger other rules" enabled.
The screenshot below shows the two rules wired into the Epic's workflow.
4. Init Registers
Link 2 Bugs (A=2) and 3 Tasks (B=3) to the Epic.
5. Bootstrap the Machine.
Transition the Epic to TODO to start the cascade. Five transitions:
(2,3) TODO →
(1,3) DEV →
(1,4) TODO →
(0,4) DEV →
(0,5) TODO →
(0,5) PROD
Recorded on a real *.atlassian.net instance.
The Epic lands in PROD with 0 Bugs and 5 Tasks linked. We've just added 2 + 3 = 5.
The reduction above suffices to prove Turing-completeness. In addition to that, Jira's automation language can simplify Minsky operations. Convert Issue Type changes an issue's type instantly: Bug → Story, Story → Task, and so on.
CONVERT is expressible as DEC + INC. It doesn't extend Jira's computational power, but it shrinks the dispatch table dramatically for any move-loop, making non-trivial programs tractable.
Fibonacci as (A, B) → (B, A+B) collapses to three states with three registers (A=Bug, B=Task, C=Story), using TODO, QA (add it to the workflow), and DEV as the three instruction states:
TODO:
if any linked Task exists:
CONVERT Task → Story
INC Bug
transition to TODO
else:
transition to QA
QA:
if any linked Bug exists:
CONVERT Bug → Task
transition to QA
else:
transition to DEV
DEV:
if any linked Story exists:
CONVERT Story → Bug
transition to DEV
else:
transition to TODO
Initial state A=1, B=1, C=0. The sequence 1, 1, 2, 3, 5, 8, 13, … appears in B (Task count).
Unlike the addition machine, the Fibonacci machine has no halt state. It runs until Jira Cloud's chain-depth cap of 10 triggers, at which point the operator re-triggers the Epic to continue. A single status edit restarts the cascade.
The reduction still holds, the human just supplies the next clock tick.
Jira Data Center exposes the same as automation.rule.execution.timeout and related, configurable properties.
Jira's automation language can encode a two-counter machine given unbounded issue creation and rule execution. Every physical computer is finite, so Jira Cloud's finite quotas do not refute the construction. Under that standard convention, Jira is Turing-complete.
So, if complex Jira automations feel like programs, it is because they literally are.
Originally, only the wealthy could afford bed coverings. Maria Morri/ CC BY-SA 2.0
Late July. New York City. A bedroom on the top floor of a four-story building in which I installed an air conditioner with several thousand too few BTUs. I barely know what a BTU is. The temperature that day reached into the upper 90s Fahrenheit, with humidity just short of actual water. The tiny weak air conditioner struggled to cool the room down while a few feet away I struggled to fall asleep. And yet I was unable to sleep without some sort of covering. In this case it was the barest edge of my lightest sheet, touching the smallest possible part of my torso.
Why this compulsion to be covered, however minimally, in order to sleep?
A Red Cross nurse change the sheets on a patient’s bed, 1917. National Archives/ 20802254
Blankets are common, but not universal, to humans during sleep, at least in the modern day. But historically, the effort involved in weaving large sheets put blankets at much too high a price point for most to afford. From the linen bedsheets of Egypt around 3500 B.C. to wool sheets during the Roman empire straight through to cotton in medieval Europe, bed coverings were for the wealthy.
By the Early Modern period in Europe, which followed the Middle Ages, production had increased enough so that more middle-class people could afford bedding, though not easily. “The bed, throughout Western Europe at this time, was the most expensive item in the house,” says Roger Ekirch, a historian at Virginia Tech who has written extensively about sleep. “It was the first major item that a newly married couple, if they had the wherewithal, would invest in.” The bed and bedding could make up about a third of the total value of an entire household’s possessions, which explains why bedsheets frequently showed up in wills.
Your newsletter subscriptions with us are subject to Atlas Obscura's Privacy Policy and Terms and Conditions.
ATLAS OBSCURA BOOKS
A Visual Odyssey Through the Marvels of Life
Venture into Nature's Unseen Realms with Our New Book Atlas Obscura: Wild Life Order Now
A depiction of a 15th-century bed. Public Domain
In place of blankets and sheets, other sources of heat were common at night, usually from multiple people sharing a bed, or often livestock.
Today, there’s minimal anthropological work about bedding around the world. The best is a 2002 paper by Carol Worthman and Melissa Melby of Emory University, who compiled a study of sleeping arrangements in different parts of the world. “Recognition of the paucity of anthropological work on sleep is galvanizing: a significant domain of human behavior that claims a third of daily life remains largely overlooked by a discipline dedicated to the holistic study of the human condition,” they wrote. This passes for outrage in an academic paper.
The paper looked into some foraging and non-foraging peoples who live in hot climates near the equator, and found that only the nomadic foragers regularly sleep without bed coverings. Everyone else uses some form of covering, whether that’s plant matter or woven fabric, even in central Africa and Papua New Guinea, both tropical climates. Much more common than sheets or blankets are some form of padding; basically nobody sleeps simply on the ground as a matter of course.
As one more example of the goodness of blankets, there has also been a decent amount of research about the calming effect of weighted blankets, which can weigh up to 30 pounds. Studies indicate that they can curb anxiety and even be used in the treatment of autism.
A linen bed sheet from the early 1800s. Public Domain
“The requirement for blankets takes on two components to it,” says Dr. Alice Hoagland, the director of the insomnia clinic at the Unity Sleep Disorder Center in Rochester, New York. “There’s a behavioral component and a physiological component.” The latter is a little more clear-cut, so let’s dive into that first.
About 60 to 90 minutes before a usual bedtime, the body starts losing core temperature. There’s a physiological explanation for that: when the body is heated, we feel more alert. And conversely, when the body cools down, we tend to feel sleepier. Cooler internal body temperatures are correlated with a rise in melatonin, a hormone that induces sleepiness. A bunch of doctors tested this out by making people wear skinsuits—they kind of look like cycling outfits—that dropped their body temperature just a touch, one or two degrees Fahrenheit, to see if they’d sleep better. They did.
Your body’s ability to regulate its own heat gets way more complicated than that at night, though. Say you sleep for eight hours each night. In the first four hours, plus the hour or so before you fall asleep, your body temperature will drop a bit, from around 98 degrees Fahrenheit to around 96 or 97. But the second four hours are marked by periods of rapid eye movement (REM) sleep, a phenomenon in which most of our dreams take place, along with a host of physical changes.
One of those physical changes is an inability to thermoregulate. “You almost revert to a more, and this is my word, reptilian form of thermoregulation,” says Hoagland. She says “reptilian” because reptiles are unable to regulate their own body temperature the way we mammals can; instead of sweating and shivering, reptiles have to adjust their temperature through external means, like moving into the sun or into cooler shadows. And for those brief periods of REM sleep, we all turn into lizards.
A bed sheet drying in the sun. Linda/ CC BY-ND 2.0
Even in perpetually hot climates, nighttime temperatures drop, and the night is coldest, coincidentally, right at the time when our bodies are freaking out and unable to adjust to it. (The night is coldest right after dawn, in direct contradiction to aphorism.) So, like lizards, we have to have some way to externally regulate our body temperatures. You may think it’s unnecessary to use a blanket at 10 p.m., when it’s still hot, but by 4 a.m., when it’s colder and you’re unable to shiver? You might need it. So we may know from past experience that we’ll thank ourselves later for having a blanket, and thus force ourselves to use one (or at least have one nearby) when going to bed.
There’s more to it than that, though. Another strange thing that happens in the REM periods of sleep is that our bodies drastically lower their levels of serotonin, the neurotransmitter most associated with feelings of calm, happiness, and well-being. You know what’s associated with higher levels of serotonin? Blankets. Various studies have indicated that sleeping with a weighted blanket can trigger an uptick in the brain’s production of serotonin. So yet again, the blanket might be filling a need that our REM-addled brains create.
A bed with a bassinet. Christophe.Finot/ CC BY-SA 3.0
The other element that might explain our need for blankets is what Hoagland refers to as “pure conditioning.” “Chances are you were raised to always have a blanket on you when you went to sleep,” she says. “So that’s a version of a transitional object, in sort of Pavlovian way.” Basically, our parents always gave us blankets to sleep with—babies are a bit worse than adults at thermoregulation, meaning they get cold easily, meaning well-meaning adults put blankets on them—and so getting under a sheet or blanket is associated with the process of falling asleep. Instead of Pavlov’s dogs drooling at the sound of a bell, we get sleepy when covered with a sheet.
If you Google around for this question, you’ll end up with a bunch of theories about blankets simulating the warm, enclosed feeling we had in the womb. There could be some element of theoretical protection or security imbued by the blanket, which might be another bit of conditioning, but Hoagland thinks the womb comparison is pretty unlikely. “I’m very suspicious of anyone who implies that this goes back to the feeling of being in the womb,” she says. “I think that’s very far-fetched.”
Another possible reason is that blankets are soft and feel good. I could not find any studies that examine the question of whether people like blankets because they’re soft and feel good, so this may remain a great unanswered question.
In my last post about generality, I tried to show how our ambition to discover ideas that are all-encompassing and eternal makes our worldview crumble, leaving us unable to think clearly even about simple issues with obvious solutions. Today, I want to discuss another instance of the same problem, in a simpler and more direct way. You can think of this essay as a prequel to “When Universality Breaks.”
Every time someone asks you a yes/no question, you are being coerced into accepting a pattern of thought that we’ll call boolean thinking. The word “boolean” here is used in the sense of the Boolean logic, and the Boolean datatype in logic and programming — a type that admits only two values: true and false. By “boolean thinking,” I am referring to the precondition that every statement should necessarily be categorized as either true or false. This is a law in Boolean logic, known as the “law of excluded middle”).
“But every statement is either true or false,” some might object. This principle might not be entirely false, but it is also not entirely true (ba-dum-tss).
Context is key. By “context”, I mean the set of premises/postulates/axioms, which we presume in order to think. Depending on the context, a statement can be:
You are probably aware of such situations, but you might still not see them as contradicting the Boolean doctrine (boolean thinking, as we shall see, is precisely that—a doctrine). It’s a mode of thought that, although not universally valid, is often useful. For instance, you can’t make plans with someone who says there’s a 40% chance they’ll go out tonight, or that the question doesn’t make sense. Thus, you might be tempted to treat all imperfections of the Boolean model as imperfections of the world — or of thinking agents themselves:
People who think this way, I would say, suffer from a serious case of Boolean thinking. Fortunately, the condition is curable, provided that we understand its cause. As I mentioned, Boolean thinking always has to do with context. Generally speaking:
We’ve established that the truth or falsity of a statement depends on its context — that is, on the assumptions we take as true or false in order to justify it. Boolean thinking, boolean logic is applicable only if we agree on some universal context — a universal set of true statements on which every evaluation can rest.
Note that aside from being universal (valid for all statements) the context for boolean logic has to also be all-encompassing (relevant for every statement) i.e. the set of logical statements that form it should never, under no interpretation tell something invalid, and at the same time would let us deduce all that is valid. As I argue later, such context resembles what political philosophers call an authoritarian doctrine (although the phrase “authoritarian doctrines” is somewhat deceiving, because it isn’t the doctrines themselves that are authoritarian, but the role they play in people’s thinking patterns).
So, while boolean logic may be splendid when viewed by itself, when viewed in relation to the “real world” there is a huge issue with it, the namely that no logical context, no logical framework is strong enough to capture the things that we usually want to dissect, (the real world, if you must). Proving the claim above is a subject of a different text, for now it suffices to say that although it may not look logical or scientific, it is, however, very backed up by both logic and science. Rather than asking why this is the case, it is more appropriate to ask what makes us think the reverse, what makes us think that the real world may be captured by a boolean logical framework — I’d argue that the thought that it can be is an instance of the so called “is-ought fallacy” — the idea that something is true just because it will be good for us that it is true. But that’s a separate topic as well (see “When Universality Breaks”.
Now, we are ready to make the case against boolean logic:
Because boolean logic overlooks the importance of context (that each proposition can be true in one context, false in another, and also neither true nor false) it inspires dichotomous thinking, also known as black-and-white thinking.
i.e. boolean logic is apt for a world where there is a single unifying and also complete framework… a universal set of axioms. But that is not our world. Our world is a place where we constantly have to compare different frameworks and, different sets of axioms, which are all incomplete (here is the place where I should reference Godel’s theorem, but I am not going to do it, as it is too cliche, (pardon my lack of diacritics)).
In our world, we, for example, try to be happy with what we have, but at the same time strive to achieve more. We try to believe that people are good, but at the same time defend ourselves against evil etc. The boolean framework says it is either one and the other (e.g. people are either good or evil).
When we encounter something that doesn’t fit our Boolean framework, we have two options:
Non-Boolean thinking, in contrast, allows multiple frameworks to coexist—without one diminishing the others.
Criticizing something is (nearly) pointless unless we offer an alternative and I will do my best to do just that.
Most people are aware of non-classical logics, but they tend to regard them as curiosities rather than real alternatives to traditional boolean logic. One branch of non-Boolean logic, however — intuitionistic or constructive logic — is increasingly relevant to many fields. It is, for example, the logic that is at the heart of the so called “proof assistants”. Boolean logic is also a special case of intuitionistic logic (the only difference is that it lacks the law of excluded middle).
Rather than resting on truth and falsehood, intuitionistic logic revolves around the concept of a proof. In contrast with classical logic, where a proof is primarily a process, intuitionistic logic treats a proof as an object: a construction that demonstrates the truth of a statement (you can see how this is related to programming — in intuitionistic logic, proving thing is similar to transforming some objects from one format to another).
Each proof depends on a context – a set of premises or other proofs we assume to exist. So, before evaluating any statement, intuitionistic reasoning asks: “What is the context?” i.e. “Give me the set of premises from which we are operating.”
From there, we proceed to manipulate the proofs of the premises in order to construct a proof of our statement.
You might say that this is much like “normal” logic. But there is a difference — intuitionistic logic makes us more acutely aware of the context in which we are operating. And if we start paying attention, we would observe that when trying to prove a statement, instead of the two truth values (true/false), there are actually three possibilities:
And without the correct context, the statement might not make sense at all.
(Yes, in a perfect world, where we know everything, we would be able to prove or disprove every statement that we can formulate, but not in this world.
And with that realization, we are free from the Boolean prison — we realize that all thinking is relative there is no a single truth, (nor a single falsity).
For more on intuitionistic logic, see my book Category Theory Illustrated.
My reasons for criticising Boolean thinking are not merely academic. The way we think about logic shapes how we think about everything — and, ultimately, how we live our lives. This is why what I call “Boolean thinking” in logic has many names elsewhere. In philosophy, it’s called Platonism. In politics, it manifests as authoritarianism.
The second point is important. You might define authoritarian ideologies in many ways, but the key thing about them is that are based on a doctrine on which all people must abide to, in their thinking — a shared “context” or set of premises. The power of authoritarian rulers arises from the way they limit the things that people can think and say: rulers define what the premises are and from then on, then you are “free” to make the conclusions yourself. To rephrase Orwell’s famous slogan:
“Who controls the premises controls the conclusions.”
Authoritarian regimes rely on propaganda, and propaganda often uses Boolean thinking: the belief that if something doesn’t follow from the “official” premises, must be necessarily false (black and white thinking). Or that if two things seem opposed, one of them must be true and the other false (false dichotomies).
To combat such propaganda techniques always remember the two rules of the contexts: