Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

ThoughtML is a plain-text language for reasoning you can check.

You write down what you believe and why — claims, evidence, who holds what, how confident, as of when — and ThoughtML reads it back as a typed, dated, defeasible graph. A second, mechanical reading can then tell you where your own structure disagrees with what you said.

ThoughtML is a mirror, not an oracle: it shows you the conflict; it does not make the call.

A first taste

focus cache-is-safe
  kind claim
  The new cache layer is safe to ship today.

focus load-test-passed
  kind observation
  Load test at 2x peak traffic passed with no errors.

focus stale-reads
  kind observation
  Staging showed stale reads under cache eviction.

link load-test-passed supports cache-is-safe
link stale-reads opposes cache-is-safe

ops-agent holds cache-is-safe
  confidence 0.9 assumed
  note Shipping — the load test passed.

This document is clean — no errors, no warnings. But the mirror flags a conflict: the agent holds cache-is-safe at 0.9, while its own recorded evidence (stale-reads opposes cache-is-safe) defeats that claim. It wrote down the counter-observation, then shipped anyway. ThoughtML surfaces that disagreement; it doesn’t decide for you. (And the 0.9 declares its basis — assumed, not measured — provenance you can see.)

Why it exists

Prose hides the shape of an argument. A bullet list flattens it. ThoughtML keeps the shape: every claim is typed, every link has a direction and a meaning, beliefs carry confidence and a date, and evidence can be defeated by other evidence. Because the structure is explicit, a machine can read it a second way — and where the two readings disagree, that gap is worth your attention.

It’s built for an age where an AI agent can emit this structure at no cost, and a human (or another agent, or CI) audits it. The point isn’t to compute the answer. It’s to make the reasoning legible enough that its flaws can’t hide.

How this book is organized

  • Getting Started — install the parser, run the playground, write your first document.
  • Tutorial — learn the language step by step, building one document up from a single focus to a full audited argument.
  • Language Reference — the authoritative description of every record, relation, posture, field, and diagnostic.
  • The Mirror — the opt-in evaluation layer: derived confidence, argument status, conflict reports, and the compute layer.
  • Guides — when to reach for ThoughtML, how to drive it from an AI agent, the CLI, and the playground.
  • Appendix — glossary, example gallery, FAQ.

A note on stability

This documentation describes v0.1.0, the first public release. The language is real and usable, but its surface may still move (hence 0.x, not 1.0). Where a feature is opt-in or advanced, this book says so plainly.

The single source of truth is the reference parser in crates/thoughtml. Everything in this book is derived from it. If the two ever disagree, the parser wins — and that’s a documentation bug worth reporting.

Installation

ThoughtML is a language. Like any language, it has a reference implementation — the program that reads ThoughtML source and tells you what it means. For ThoughtML that’s a parser written in Rust, plus a browser playground built on the same parser. You don’t need to know any Rust to use the language; you only need to run the implementation.

There are two ways to run it:

  • The CLI — read a .thml file, emit canonical JSON and diagnostics. This is the source of truth for the language.
  • The playground — a live editor with a graph view, for exploring visually.

Installing the CLI

The CLI is one self-contained binary named thoughtml. Pick whichever fits — none of them require you to know any Rust:

pip — if you have Python (3.8+):

pip install thoughtml

npm — on any platform with Node.js (14+):

npm install -g thoughtml

Both download the prebuilt binary for your platform and put thoughtml on your PATH — no Python or Node code runs, and no compiler is needed. (npx thoughtml <file> works too, without a global install.)

Cargo — if you have a Rust toolchain (rustup.rs):

cargo install thoughtml

Prebuilt binary — download the archive for macOS, Linux, or Windows from the latest release, unpack it, and put the thoughtml binary on your PATH.

Once it’s installed, run it on a document — canonical JSON goes to stdout, diagnostics to stderr:

thoughtml examples/choose-datastore.thml

See the CLI reference for every flag and subcommand.

Building from source

To hack on the parser itself, build the workspace:

git clone https://github.com/Fatin-Ishraq/ThoughtML.git
cd ThoughtML
cargo build --release   # parser + wasm crate; binary at target/release/thoughtml
cargo test              # every bundled example is strict-clean

From a source checkout you can run without installing — -p thoughtml selects the parser crate:

cargo run -p thoughtml -- examples/triage-742.thml

Export a standalone view

Turn any document into a single self-contained interactive HTML file — no server, opens in any browser:

thoughtml examples/choose-datastore.thml --html -o datastore-decision.html

It carries the interactive graph with the model baked in (no wasm). See The standalone viewer.

Running the playground

Building the playground locally needs Node.js 20+, a Rust toolchain with the wasm32-unknown-unknown target, and wasm-pack (or just try it live — no install — at the hosted playground).

cd web
npm install
npm run wasm        # compile the parser to wasm (uses the rustup toolchain)
npm run dev         # start the dev server, then open the printed URL

The playground runs the exact same parser as the CLI, compiled to WebAssembly — the browser and the command line can never drift. It also turns the mirror’s opt-in readings on by default, so you see derived confidence, argument status, and conflicts live as you type. See Using the playground.

wasm toolchain gotcha. npm run wasm must use the rustup toolchain. If wasm-pack picks up a standalone MSVC Rust instead, the build fails. Make sure ~/.cargo/bin (where rustup installs) is early on your PATH.

Reading this book offline

This book is written for mdBook. To render it as a searchable site:

cargo install mdbook
cd docs
mdbook serve --open    # live-reloading local site
mdbook build           # static site in docs/book/

Every page is also plain Markdown, so you can read it directly on GitHub without building anything.

Next

Write your first document.

Your first document

A ThoughtML document is a plain-text file (.thml) describing a piece of reasoning. Let’s write the smallest complete one and run it.

Write it

Create hello.thml:

focus metric-shift
  Activation rose 12% after the Tuesday deploy.

focus deploy-change
  The Tuesday deploy changed the sign-up flow.

link deploy-change causes metric-shift

team holds metric-shift
  confidence 0.8

Three things are happening:

  1. Two foci. A focus is a thing you’re reasoning about — here, an observation and a possible cause. The indented line under each is its prose body.
  2. A link. deploy-change causes metric-shift records a typed, directed relationship between them.
  3. A stance. team holds metric-shift says who believes what, and the confidence 0.8 says how strongly.

Run it

thoughtml hello.thml

You’ll get canonical JSON on stdout — the normalized object model, the interchange form every implementation emits. Abbreviated:

{
  "objects": [
    { "type": "focus", "id": "metric-shift", "body": "Activation rose 12% after the Tuesday deploy." },
    { "type": "focus", "id": "deploy-change", "body": "The Tuesday deploy changed the sign-up flow." },
    { "type": "link", "id": "deploy-change-causes-metric-shift",
      "from": "deploy-change", "relation": "causes", "to": "metric-shift" },
    { "type": "stance", "id": "team-holds-metric-shift",
      "agent": "team", "posture": "holds", "target": "metric-shift",
      "confidence": { "kind": "number", "value": 0.8 } }
  ]
}

Notice the parser gave every record an id (metric-shift, deploy-change-causes-metric-shift, …). Ids are how records reference each other.

Check it

The document above is clean — no diagnostics. Break it on purpose: change the link’s target to a focus that doesn’t exist.

link deploy-change causes metric-shfit

Run it again and the parser warns on stderr:

warning: link.to of `deploy-change-causes-metric-shfit` is an unresolved reference `metric-shfit`

This is the everyday loop: write reasoning, run it, fix what the parser flags. Diagnostics catch the structural mistakes — dangling references, contradictions, cycles, orphans.

See it as a graph

Open the same file in the playground and it renders as a graph: foci as nodes (shaped by kind), links as labeled arrows, stances attached to their targets. The whole point of ThoughtML is that you can read the argument straight from the picture.

Next

Start the tutorial, which builds one real document up from a single focus to a full argument the mirror can audit.

Tutorial

This tutorial teaches the language by building one real document up from a single line to a complete argument the mirror can audit.

The scenario: an engineering team is deciding whether to ship a new cache layer today. By the end you’ll have written the bundled example ship-the-hotfix.thml — a document that is diagnostically clean yet hides a real contradiction, which the mirror surfaces.

The chapters build on each other:

  1. Foci — name the things you’re reasoning about.
  2. Links — connect them with typed, directed relations.
  3. Stances — record who believes what, and how.
  4. Questions — mark what’s still open and what it blocks.
  5. Numbers — confidence, weight, and where numbers come from.
  6. Time and revision — date beliefs and let them change.
  7. The mirror — read your structure back and find the conflict.

Each chapter is short. Type the examples into a file and run them with thoughtml <file>.thml (see Installation), or paste them into the playground to see the graph.

Conventions. Identifiers are lowercase kebab-case (cache-is-safe). Indentation is two spaces — tabs are an error. A # starts a comment line.

1. Foci — the things you reason about

A focus is the basic unit of a ThoughtML document: a thing you’re reasoning about. An observation, a claim, an option, a goal — anything you might later support, attack, question, or believe.

You declare one with the focus keyword and an id, then add prose on the next indented line:

focus cache-is-safe
  The new cache layer is safe to ship today.

The id (cache-is-safe) is how everything else in the document refers to this focus. The indented sentence is its body — free text, for humans.

Kinds

A focus has a kind — its semantic category. You set it with a kind field:

focus cache-is-safe
  kind claim
  The new cache layer is safe to ship today.

focus load-test-passed
  kind observation
  Load test at 2x peak traffic passed with no errors.

There are ten kinds:

KindWhat it is
observationSomething seen or measured
claimAn assertion put forward as true
hypothesisA proposed explanation, not yet settled
optionA choice on the table
decisionA choice to be made (or made)
outcomeA result an option can lead to
goalSomething you want
assumptionSomething taken as given
memoryA recollection carried forward
actionSomething you do — a plan, intervention, mitigation

Kinds are optional but recommended: they make the graph readable at a glance (the playground gives each kind its own node shape) and they let the language catch category mistakes.

You don’t always write focus

Most of the time you won’t declare foci with the bare focus keyword. The readable posture syntax creates them for you, inferring the kind:

analyst noticed load-test-passed
  Load test at 2x peak traffic passed with no errors.

noticed creates the focus load-test-passed and gives it the kind observation automatically. You’ll meet the full set of postures in chapter 3. For now, the rule of thumb: declare a focus explicitly with focus when you want to set its kind precisely or when several agents will refer to it; let a posture create it when it belongs to one agent’s action.

Our document so far

focus cache-is-safe
  kind claim
  The new cache layer is safe to ship today.

focus load-test-passed
  kind observation
  Load test at 2x peak traffic passed with no errors.

focus stale-reads
  kind observation
  Staging showed stale reads under cache eviction.

Three foci, no connections yet. Right now ThoughtML will warn that they’re orphans — nothing relates them. We fix that next, with links.

2. Links — how they relate

A link connects two records with a typed, directed relation. This is what turns a list of foci into a graph you can reason over.

The syntax is link <from> <relation> <to>:

link load-test-passed supports cache-is-safe
link stale-reads opposes cache-is-safe

Read left to right: load-test-passed supports cache-is-safe; stale-reads opposes cache-is-safe. The direction matters — a supports b is not the same as b supports a.

The relations

There are twelve relations, in three families.

Evidence — the defeasible core. These feed the mirror’s derived confidence and argument status:

RelationMeaning
supportsThe source is evidence for the target
opposesThe source is evidence against the target (a rebuttal)
undercutsThe source attacks an inference, not the claim itself

Structural / causal — how things relate in the world or the plan:

RelationMeaning
causesThe source brings about the target
enablesThe source makes the target possible
preventsThe source stops the target
depends-onThe target is needed for the source
blocksThe source holds the target up (see until in chapter 4)
answersThe source resolves a question
revisesThe source replaces the target (see chapter 6)

Decision — for expected-value analysis (see the compute layer):

RelationMeaning
leads-toAn option leads to an outcome (carries a probability)
option-ofAn option belongs to a decision

opposes vs. undercuts. opposes rebuts a node (“that claim is wrong”). undercuts attacks an inference (“that reasoning doesn’t follow”) — its target is usually a link. The distinction matters to the mirror: an undercut weakens a connection rather than the claim. There is deliberately no separate rejects relation — a hard rejection is just opposes, and defending X is just attacking X’s attacker.

Aliases and prose

Give a link an alias (its own id) by prefixing name: — useful when you want to attack the link itself, or reference it later:

link cache-hypothesis: cache-eviction causes latency-spike
  The proposed mechanism: evicted hot keys force slow cold reads.

link dashboard-bug undercuts cache-hypothesis

The indented sentence under a link is its body — prose explaining why the relation holds. Here dashboard-bug undercuts cache-hypothesis attacks the inference by name.

A link’s endpoints may be foci, questions, or other links. Pointing a link at a stance or a scope is an error. Pointing it at an id that doesn’t exist is a warning (a dangling reference — usually a typo).

Our document so far

focus cache-is-safe
  kind claim
  The new cache layer is safe to ship today.

focus load-test-passed
  kind observation
  Load test at 2x peak traffic passed with no errors.

focus stale-reads
  kind observation
  Staging showed stale reads under cache eviction.

link load-test-passed supports cache-is-safe
link stale-reads opposes cache-is-safe

No more orphans: every focus is connected. Now — who actually believes the claim? That’s a stance.

3. Stances — who believes what

Foci and links describe the content of an argument. A stance records an agent’s relationship to it — who holds, doubts, chooses, or rejects something, and how confidently.

The readable form is <agent> <posture> <target>:

ops-agent holds cache-is-safe
  confidence 0.9
  note Shipping — the load test passed.

This says the agent ops-agent holds the focus cache-is-safe, at confidence 0.9, with a note recording the rationale. (confidence and note are covered fully in chapter 5 — for now, just know they ride on the stance.)

Postures

A posture is the verb. There are twelve:

PostureMeaning
noticedRegistered an observation
considersPut an option on the table
suspectsProposed a tentative link (a hypothesis)
infersDrew a conclusion from sources
asksRaised a question
holdsCommits to / believes
choosesSelected an option
rejectsRuled something out
revisesReplaced a previous stance
remembersCarried a fact forward
doubtsHolds with low credence
acceptsAgrees with

Some postures create foci for you

Five postures bring a new focus into being and infer its kind, so you don’t have to declare it separately:

PostureCreates a focus of kind
noticedobservation
considersoption
holds / choosesdecision
remembersmemory
infersclaim

So this:

analyst noticed metric-shift
  Activation rose after the deploy.

creates the focus metric-shift (kind observation) and a stance (analyst noticed metric-shift) in one line. If a focus already exists with an explicit kind, that kind wins — a posture’s inferred kind never overrides one you stated outright.

The other postures (doubts, accepts, asks, rejects, revises) reference an existing target rather than creating one.

Two postures take a richer form

  • suspects proposes a link and takes a stance on it:

    analyst suspects ai-automation causes job-displacement as displacement-hypothesis
      confidence 0.45..0.70
    

    This creates the two foci, a causes link aliased displacement-hypothesis, and a stance in which the analyst suspects that link. (Note the confidence is a range — see chapter 5.)

  • infers draws a conclusion from one or more sources, wiring a supports link from each:

    analyst infers adaptation-too-slow from ai-capability-surge, reskilling-lag
      confidence 0.60
    

note vs. body

For a posture that creates a focus, the indented prose becomes that focus’s body. For one that doesn’t, the prose becomes a note on the stance. Either way, an explicit note field always attaches to the stance — so even holds and chooses can carry rationale on the stance itself.

Multiple agents

Different agents can take different stances on the same target — that’s how you record a disagreement:

worker accepts displacement-hypothesis
  confidence 0.80
economist doubts displacement-hypothesis
  confidence 0.35

If one agent takes contradictory postures on the same target (e.g. accepts and rejects), ThoughtML warns — see Diagnostics.

Our document so far

focus cache-is-safe
  kind claim
  The new cache layer is safe to ship today.

focus load-test-passed
  kind observation
  Load test at 2x peak traffic passed with no errors.

focus stale-reads
  kind observation
  Staging showed stale reads under cache eviction.

link load-test-passed supports cache-is-safe
link stale-reads opposes cache-is-safe

ops-agent holds cache-is-safe
  confidence 0.9
  note Shipping — the load test passed.

This is already a complete, meaningful document. Before we hand it to the mirror, two more building blocks: questions and numbers.

4. Questions — what’s still open

Reasoning isn’t only assertions. Often the most important thing in a document is what you don’t know yet. A question records an open issue.

question throughput-benchmark
  Can Postgres sustain 50k events per second on target hardware?
  expects number
  status open
  • The indented sentence is the question’s body.
  • expects says what kind of answer would settle it (number, option, forecast, …) — free-form, for the reader.
  • status is typically open or settled.

What is the question about?

Use about to link a question to the foci it concerns:

question new-jobs-in-time
  Will new jobs arrive fast enough to offset the losses this decade?
  about job-displacement, technology-creates-jobs
  expects forecast
  status open

Answering a question

A link with the answers relation, or an answers field on a stance, records that something resolves the question:

team chooses postgres-option
  answers which-datastore

Blocking on an open question

Here’s the useful part. A decision often can’t be made until a question is answered. The until field on a stance expresses exactly that:

team holds datastore-decision
  Commit to a datastore for the event log.
  until throughput-benchmark answered
  note Provisionally Postgres, but not signed off until the benchmark lands.

until throughput-benchmark answered desugars to a link: throughput-benchmark blocks datastore-decision (with the status answered preserved on it). So the graph literally shows the benchmark holding the decision up — and when you read it back, the blockers are explicit, not buried in prose.

Our cache document doesn’t need a question — the team has already shipped. But this pattern is the backbone of decision records. Next: the numbers that make beliefs precise.

5. Numbers — confidence, weight, provenance

ThoughtML lets you attach numbers to beliefs — but it’s careful about them. There is one way to express each thing, and numbers can declare where they came from.

Confidence — on a stance

confidence says how strongly an agent holds a target. It can be:

  • a scalar in 0..1 — confidence 0.9
  • a range (lo..hi) for honest uncertainty — confidence 0.45..0.70
  • the unknown marker ?confidence ? (held, but credence not stated)
ops-agent holds cache-is-safe
  confidence 0.9

weight (0..1) says how strongly a relation holds — how much this piece of evidence counts:

link firms-cutting-headcount supports displacement-hypothesis
  weight 0.85

link technology-creates-jobs undercuts displacement-hypothesis
  weight 0.5

There is deliberately no strongly / weakly adverb. Earlier versions had them; each smuggled in a magic number the author never chose. Strength is the explicit numeric weight, or nothing.

For decision analysis, a leads-to edge carries the probability of that outcome:

link harvard leads-to harvard-thrive
  probability 0.7

(weight and probability are distinct: weight is evidential strength; probability is outcome likelihood. Putting weight on a leads-to edge, or probability on anything else, is ignored with a warning.)

Quantities — on a focus

A focus can carry a typed measure with a unit, classified into a dimension:

focus aid-offer
  quantity 78000 USD
  Annual grant aid offered — grants, not loans.

Units are recognized across dimensions (time, information, currency, count, rate, ratio). Fused forms work too — 200ms, 1.5GB, 30%. See Numbers, units, provenance for the full model.

Provenance — where a number came from

Any authored number can declare its basis inline — one of measured, estimated, assumed:

ops-agent holds cache-is-safe
  confidence 0.9 assumed
focus disk-budget
  quantity 30 GB measured

This is the honest core of the language: a 0.9 that says it’s assumed tells you something a bare 0.9 hides. Provenance is optional — but you can make it mandatory:

thoughtml --strict-provenance doc.thml

With --strict-provenance, any authored quantity, confidence, weight, or probability that omits a basis gets a warning. It’s off by default, so existing documents stay clean.

Our document so far

We add the provenance to our stance — the 0.9 is assumed, not measured:

ops-agent holds cache-is-safe
  confidence 0.9 assumed
  note Shipping — the load test passed.

That single word is what makes the final mirror reading land. But first, one more dimension: time.

6. Time and revision

Beliefs change. ThoughtML treats time as first-class: records can be dated, and a later belief can revise an earlier one without erasing it — the history stays inspectable.

Time is optional. You never have to timestamp anything. A document with no dates parses and renders fine — the viewer simply reveals it in document order (narrative replay) instead of by date. Reach for dates only when when actually matters; don’t invent them to force an ordering.

Dating records

Three timestamp fields, all ISO-8601:

  • observed-at — when something was seen.
  • asserted-at — when a belief was put on the record.
  • valid-during start..end — a span over which something holds.
analyst noticed early-burndown
  The first sprint cleared 40% of the backlog — ahead of plan.
  observed-at 2026-06-01

From all the timestamps in a document, ThoughtML derives a timeline. It’s not just the earliest and latest instants: it carries an ordered events array — every dated record as { at, seq, id, kind } (plus agent for a stance) — sorted by valid-time, with a seq tiebreak for events that share an instant. That ordering is the document’s reasoning as a sequence of moments, independent of the order you happened to type it in; it’s what the viewer replays. Dates can be partial (2026, 2026-06) and carry a zone (2026-06-14T14:05+05:00); they’re compared correctly regardless.

A belief’s lifecycle

A focus can record where it stands with a first-class status:

focus webgl-renderer
  kind option
  A shader-based renderer for particle juice.
  status abandoned
  Canvas 2D already holds 60fps — this was over-engineering.

The four values are open (live), settled (resolved), superseded (replaced by a later belief — see revises below), and abandoned (a dead end). The point is the same as with revision: an abandoned or superseded branch is kept with its reason, not deleted, so the path not taken stays inspectable. The viewer folds those branches by default and dims them in replay.

Revising a belief

There are two ways to mark that something has been superseded — and nothing is deleted either way.

The revises relation supersedes a node:

focus june-30-target
  Original commitment: ship on June 30.

focus july-14-target
  Revised commitment: ship on July 14, absorbing the new scope.
  asserted-at 2026-06-08

link july-14-target revises june-30-target
  The added scope pushed the committed date out by two weeks.

After this, june-30-target carries superseded_by: july-14-target.

The revises posture supersedes the same agent’s previous stance on a target:

analyst suspects early-burndown causes on-track as on-track-claim
  confidence 0.70
  asserted-at 2026-06-01

analyst revises on-track-claim
  confidence 0.40
  asserted-at 2026-06-08
  note The new scope cancels out the fast start.

The earlier stance is marked superseded by the later one. ThoughtML also sanity-checks the order: if a revision is asserted earlier than the thing it revises, you get a warning.

Why keep the old belief?

Because the history is the point. A superseded belief no longer counts as live evidence (the mirror ignores it when deriving confidence), but it’s still in the graph. In the playground, the as-of bar lets you replay the document moment by moment: drag it back and the later beliefs disappear, the earlier ones un-dim. You can watch the reasoning evolve.

The same projection is available from the CLI, so you can ask “what did this document believe as of a date?” in a script:

thoughtml --as-of 2026-06-08 doc.thml      # the model as it stood on that day
thoughtml --as-of-seq 3 doc.thml           # …as of the 3rd recorded event

--as-of filters on valid-time (the default axis); --as-of-seq filters on transaction order. Either way, links and stances that would dangle once a node drops out are cascaded away, so the projected model is always coherent.

The bundled launch-readiness.thml is built entirely around this — a launch date that slips twice as evidence arrives.

Our cache document is a single moment in time, so it needs no revision. We now have every piece. Time to read it back — the mirror.

7. The mirror — reading the conflict

Here is our finished document. It’s the bundled example ship-the-hotfix.thml:

focus cache-is-safe
  kind claim
  The new cache layer is safe to ship today.

focus load-test-passed
  kind observation
  Load test at 2x peak traffic passed with no errors.

focus stale-reads
  kind observation
  Staging showed stale reads under cache eviction.

link load-test-passed supports cache-is-safe
link stale-reads opposes cache-is-safe

ops-agent holds cache-is-safe
  confidence 0.9 assumed
  note Shipping — the load test passed.

It is clean

Run it normally:

thoughtml ship-the-hotfix.thml

No errors. No warnings. Every reference resolves, nothing contradicts at the form level, nothing is orphaned. By every structural check, this document is fine.

But the structure disagrees with the author

Now turn on the mirror — the opt-in second reading:

thoughtml --audit ship-the-hotfix.thml

The canonical JSON now carries an audit section:

"audit": {
  "conflicts": [
    {
      "kind": "confidence-vs-status",
      "severity": "error",
      "subjects": ["ops-agent-holds-cache-is-safe", "cache-is-safe"],
      "message": "`ops-agent` asserts confidence 0.90 in `cache-is-safe`, but your own structure defeats it (argument status: out)"
    }
  ]
}

Read what happened. The agent holds cache-is-safe at 0.9. But the document also records stale-reads opposes cache-is-safe. When the mirror computes the argument status, cache-is-safe comes out out — defeated by its own recorded counter-evidence. The agent wrote down the objection, then shipped anyway.

That’s the conflict: high confidence in a claim the structure defeats. And the 0.9 declared itself assumed — so the mirror shows not just how sure the agent is, but on what footing.

The mirror reports; it does not decide

Notice what ThoughtML did not do. It didn’t lower the confidence. It didn’t veto the ship. It didn’t tell the team they were wrong — maybe the stale reads are acceptable, maybe the opposition is weak. It surfaced the disagreement between what was said (0.9) and what the structure implies (defeated), and left the call to a human.

This is the whole philosophy in one example: a mirror, not an oracle.

The rest of the second reading

--audit is one of several opt-in readings. The catch-all flag turns them all on:

thoughtml --compute ship-the-hotfix.thml

That adds derived confidence (how strong each claim is, propagated through the evidence), argument status on every node, per-edge leverage, and — for documents with decisions — expected value. The playground turns these on by default, so you see them live.

Where to go next

  • The Language Reference documents every record, relation, posture, field, and diagnostic precisely.
  • The Mirror explains how each reading is computed.
  • The Use cases guide shows where this pays off: decision records, design reviews, agent reasoning a human can audit.

Language Reference

This is the authoritative description of ThoughtML as of v0.1.0. It is derived from the reference parser; where this book and the parser disagree, the parser is correct.

The pipeline

Every ThoughtML document goes through the same stages, in both the CLI and the (wasm-compiled) playground:

source text
  → lines        classify each line: blank, comment, header, block
  → surface AST  parse headers and fields into records
  → canonical    desugar the readable surface into normalized objects
  → validate     resolve references; run semantic lints
  → derive       (opt-in) the mirror's second readings
  → canonical JSON

The canonical object model is the interchange form — a flat, ordered array of typed objects. Everything downstream (the graph, the mirror, any other tool) reads canonical objects, not source text.

Two surfaces, one model

ThoughtML has two ways to write the same thing:

  • The canonical corefocus, link, stance, question, scope records written directly.
  • The readable action surface<agent> <posture> <target> lines that desugar into the core (creating foci, links, and stances for you).

They produce the same objects. The bundled merge-conflict-beliefs.thml writes the same reasoning as hiring-panel.thml using the bare core, to show the equivalence.

How to read these pages

Lexical structure

A ThoughtML document is a sequence of lines. Each line is classified before anything else happens.

Line kinds

LineRule
BlankEmpty or whitespace-only. Ignored.
CommentFirst non-space character is #. Ignored.
HeaderZero indentation, non-blank. Starts a new record.
BlockIndented (≥ 1 space), non-blank. Belongs to the open record.

A leading UTF-8 BOM is stripped. Both \n and \r\n line endings work.

Indentation

  • Indentation is spaces. A tab in the leading whitespace is an error (tab indentation is invalid; v0 requires spaces).
  • Block lines should be indented two or more spaces; one space earns a warning.
  • A column-0 line closes every open record and starts a new top-level one.
  • A more-indented line nests under the line above it; a less-indented line closes records back to the matching level. (Nesting only carries meaning inside a scope — see there.)
focus a            # header, column 0
  kind claim       # block line, indent 2 — belongs to `a`
  Some prose.      # block line — also belongs to `a`
focus b            # header again — closes `a`, opens `b`

Comments

A # at the start of a line (after optional indentation) makes the whole line a comment. There are no end-of-line comments — a # partway through a line is just text.

# This whole line is a comment.
focus a
  This sentence has a # but it is part of the body, not a comment.

Identifiers

Identifiers (record ids, relation names, references) are lowercase kebab-case: they start with a lowercase letter and contain only lowercase letters, digits, and hyphens.

cache-is-safe       ✓
conversation-2026-06-09   ✓
Metric              ✗  (uppercase)
-x                  ✗  (leading hyphen)
9x                  ✗  (leading digit)

For imports, an identifier may be namespace-qualified with dots: base.capacity-budget — each dot-separated segment is itself a valid kebab identifier.

Values

The value after a field keyword is classified into one of a small fixed set of forms:

FormExampleNotes
Number0.72, 1, -3Plain base-10. No exponents, inf, or nan.
Range0.25..0.70low..high, inclusive.
Unknown?The explicit “not stated” marker.
Time2026-06-09, 2026-06-14T14:05+00:00Loose ISO-8601.
Refcache-is-safeA bare identifier — a reference to another record.
Symbolopen, highSame lexical form as a ref; intent depends on the field.
URIuri:https://example.org/xA uri:-prefixed value.
Lista, b, cComma-separated identifiers/symbols.
Text"quoted string", or any multi-word valueFree text. A quoted string is always text.

A single-token value is classified by the table above; a multi-word value (more than one whitespace-separated token) is Text, unless it’s a comma list of identifiers. Quotes force Text.

Why this matters for provenance. A value like 0.9 assumed is two tokens, so it would classify as Text. ThoughtML peels a trailing basis keyword (measured/estimated/assumed) off first, then re-classifies the remaining 0.9 as a Number. That’s how confidence 0.9 assumed parses correctly.

Records and the canonical model

A document’s source is written as records (headers + indented blocks). The parser desugars them into canonical objects — a flat, insertion-ordered array of typed objects, serialized as JSON.

Record headers

HeaderFormBecomes
focusfocus <id>a Focus
linklink [alias:] <from> <relation> <to>a Link
stancestance [alias:] <agent> <posture> <target>a Stance
questionquestion <id>a Question
scopescope <id>a Scope
profileprofile <name>a Profile
importimport <name> as <ns>(resolved as a project; no object)
action<agent> <posture> …desugars to foci / links / stances

Anything whose first token isn’t a reserved keyword is parsed as an action header<agent> <posture> <target> — which is how the readable surface works. If the second token isn’t a known posture, you get unknown record kind or header.

The canonical objects

The JSON model is { "objects": [ … ], "timeline"?, "audit"? }. Each object has a type tag. There are seven:

Focus

{ "type": "focus", "id": "cache-is-safe", "kind": "claim",
  "body": "The new cache layer is safe to ship today." }

Optional fields: kind, quantity, formula, body, free-form fields, and the opt-in derived ones (computed_quantity, superseded_by, derived_confidence, argument_status, expected_value, decision).

{ "type": "link", "id": "stale-reads-opposes-cache-is-safe",
  "from": "stale-reads", "relation": "opposes", "to": "cache-is-safe" }

Optional: weight, probability, basis, body, fields, plus derived superseded_by, derived_confidence, leverage, argument_status.

Stance

{ "type": "stance", "id": "ops-agent-holds-cache-is-safe",
  "agent": "ops-agent", "posture": "holds", "target": "cache-is-safe",
  "confidence": { "kind": "number", "value": 0.9 }, "basis": "assumed" }

Question

{ "type": "question", "id": "throughput-benchmark",
  "body": "Can Postgres sustain 50k events/s?", "expects": "number", "status": "open" }

Plus asks_about (a list of ids).

Scope

{ "type": "scope", "id": "incident-742", "includes": ["metric-shift", "…"] }

Profile

{ "type": "profile", "name": "risk-analysis",
  "kinds": ["risk"], "relations": ["aggravates"], "postures": ["flags"] }

Act

Provenance objects for readable actions, emitted only under --acts. Each records the verb, args, and the canonical ids it expands_to.

Ids

Every object has an id, used for cross-references.

  • For focus, question, scope, profile — you write the id in the header.
  • For link and stance — you can supply an alias (link foo: a causes b), or let the parser generate one:
    • a link → <from>-<relation>-<to> (e.g. deploy-change-causes-metric-shift)
    • a stance → <agent>-<posture>-<target>
    • on collision, a -2 suffix is appended.

Reusing an id across records is flagged (duplicate id / id is reused across records). A focus mentioned twice is merged, not duplicated — see Foci.

Order is preserved

The objects array is in declaration order, and field maps keep author order. A document without any opt-in derivations serializes byte-for-byte stably — which is what lets the test suite assert that every bundled example is strict-clean.

Foci and kinds

A focus is a node you reason about. Declared directly:

focus cache-is-safe
  kind claim
  The new cache layer is safe to ship today.

or, more concisely, with a typed header — a built-in kind used as the header word, which desugars to exactly the same focus + kind:

claim cache-is-safe
  The new cache layer is safe to ship today.

Either form works anywhere a focus does (including nested in a scope). A focus can also be created implicitly by a focus-creating posture.

Anatomy

PartSourceNotes
idthe header (focus <id>)lowercase kebab-case
kinda kind field, or inferredsee below
bodythe indented prosefirst mention wins on merge
quantitya quantity fielda typed measure
formulaa = <expr> lineopt-in compute
statusa status fieldbelief lifecycle, see below
includesnested child recordsthought-tree members, see below
fieldsany other field linesfree-form, order-preserved

Kinds

The ten kinds:

KindMeaning
observationSomething seen or measured
claimAn assertion put forward as true
hypothesisA proposed explanation, not yet settled
optionA choice on the table
decisionA choice to be made or recorded
outcomeA result an option can lead to
goalA desired end
assumptionSomething taken as given
memoryA recollection carried forward
actionA thing one does — plan, intervention, mitigation

An unknown kind warns (unknown focus kind), unless a profile declares it.

How a kind is set

In priority order:

  1. Explicit kind field — authoritative and sticky. Once set explicitly, nothing overrides it silently.
  2. Posture inference — a focus-creating posture implies a kind: noticedobservation, considersoption, holds/choosesdecision, remembersmemory, infersclaim. This is soft: a later posture can refine it.
  3. Decision-graph inference — the endpoints of leads-to / option-of edges get provisional kinds: the source of either is an option; the target of leads-to is an outcome; the target of option-of is a decision. Only applied to foci that still have no kind.

If two explicit kinds disagree on the same focus, the first is kept and a warning is emitted. An explicit kind always beats an inferred one; an inferred kind can be refined by a later posture (e.g. considers X then chooses X moves X from option to decision).

Status — the belief lifecycle

A status field records where a focus stands:

StatusMeaning
openlive — still in play (the default if unstated)
settledresolved
supersededreplaced by a later belief (cf. the revises relation)
abandoneda dead end

Status is a fold marker: a settled / superseded / abandoned focus (and the thought-tree it opens) is kept with its reasoning intact but folds by default in the viewer. Nothing is deleted — the path not taken stays inspectable.

Thought-trees — nesting

A focus (like a scope, and like a question) can contain other records by nesting them under it:

focus ship-decision
  kind decision
  Ship the new cache layer this week.

  focus load-test-passed
    kind observation
    p99 held under 2× peak traffic.

  focus rollback-ready
    kind assumption
    One-command rollback is wired up.

The members are recorded on the container’s includes, in document order, and inherit the container’s provenance and temporal context (member-wins: an explicit value on the child is kept). This turns a flat list of foci into a thought-tree — a claim and the reasoning that hangs off it, as one unit.

Merging

A focus id mentioned more than once refers to one focus — the mentions merge:

  • body, quantity, and formula are first-wins (a later mention doesn’t overwrite a value already stated).
  • fields accumulate.
  • kind follows the priority rules above.

This is what lets you declare a focus once with full detail and then refer to it freely by id elsewhere, even from another agent’s stance.

Divergence is kept, not dropped. If a later mention states a different body, quantity, or formula (not just a repeat), the alternative is not silently discarded — it’s retained on the focus’s divergent list and the mirror raises a definition-divergence conflict. Concurrent authors (or agents) can each write their version; reconciliation is surfaced, never forced.

Links and relations

A link is a typed, directed edge.

link [alias:] <from> <relation> <to>
link load-test-passed supports cache-is-safe
link cache-hypothesis: cache-eviction causes latency-spike
  The proposed mechanism: evicted hot keys force slow cold reads.
  weight 0.85

Anatomy

PartSource
idthe alias:, or generated <from>-<relation>-<to>
from, relation, tothe header
weighta weight field (0..1 evidential strength)
probabilitya probability field (0..1, leads-to only)
basisa provenance keyword on the number
bodythe indented prose (why the relation holds)

Evidence bundles (authoring shortcut)

When many sources share one relation and target — the common “several observations → one claim” shape — a bundle collapses the repetition. A bare <relation> <target> header lists its sources, one per line:

supports customer-pain-points
  late-fees-problem      weight 0.90 assumed
  limited-availability   weight 0.85
  long-travel-time

Each line desugars to an ordinary link <source> <relation> <target> carrying its optional weight and provenance — identical to writing the links out longhand. A bare source (no weight) is a normal, full-strength link.

The relations

Evidence (defeasible)

RelationPolarityMeaning
supports+source is evidence for target
opposessource rebuts target (a node attack)
undercutssource attacks an inference (usually a link target)

These three drive the mirror’s derived confidence and argument status. opposes and undercuts are the two attacks.

Structural and causal

RelationMeaning
causessource brings about target
enablessource makes target possible
preventssource stops target
depends-ontarget is required for source
blockssource holds target up (the desugaring of until)
answerssource resolves a question
revisessource supersedes target (see Time / tutorial ch. 6)

causes and depends-on are expected to be acyclic — a cycle among them is flagged (an impossible circular dependency).

Decision

RelationMeaning
leads-toan option leads to an outcome; carries probability
option-ofan option belongs to a decision

These power decision expected value.

Membership and candidacy (non-evidential)

RelationMeaning
part-ofsource is one item in the target (a collection member)
candidate-forsource is a proposed answer to a question — not a resolved one

These exist to keep enumerations out of the evidence graph. Using supports for the items of a list — SWOT strengths, competitors, success factors — silently inflates the target’s derived confidence: the mirror counts each list item as evidence, so “Netflix has strengths” reads as 100%-certain just for naming them. part-of says “this is one of the things,” not “this is evidence it’s true” — it has no evidence polarity, so it never touches confidence, argument status, or leverage. candidate-for does the same for a question’s options: a candidate is a proposal, not the resolved answers.

Both compose with the bundle shortcut:

part-of netflix-strengths
  strong-global-brand
  large-subscriber-base
  advanced-algorithms

What can be linked

A link’s from and to may resolve to a focus, question, or link. Targeting a stance or a scope is an error. Targeting a non-existent id is a warning (unresolved reference).

opposes vs. undercuts, and what’s not here

  • opposes rebuts a node: “that claim is false.”
  • undercuts attacks an inference: “that step doesn’t follow.” When its target is a link, the mirror weakens that connection rather than the claim.

There is no rejects relation and no mitigates relation — both were removed in v0.1.0 because they only duplicated opposes. A hard rejection is opposes; defending X is attacking X’s attacker (guard opposes risk), which the grounded argument status reinstates uniformly. (Note rejects still exists as a posture — an agent ruling something out — just not as a relation.)

Weight and probability are not interchangeable

  • weight is evidential strength (any evidence/structural link).
  • probability is outcome likelihood (leads-to only).

Putting probability on a non-leads-to link, or weight on a leads-to link, is ignored with a warning.

Stances and postures

A stance records an agent’s relationship to a target.

stance [alias:] <agent> <posture> <target>      # canonical core form
<agent> <posture> <target>                       # readable action form

Both produce a Stance object with agent, posture, target, an optional confidence (+ basis), and any other fields. The action form additionally creates foci and links, depending on the posture.

The twelve postures

PostureCreates a focus?Inferred kindNotes
noticedyesobservationregistered an observation
considersyesoptionput an option forward
holdsyesdecisioncommits to / believes
choosesyesdecisionselects an option
remembersyesmemorycarries a fact forward
infersyesclaimconcludes from sources (special form)
suspectsproposes a link (special form)
asksnoraises a question
doubtsnolow credence
acceptsnoagrees
rejectsnorules out
revisesnosupersedes a prior stance

An unknown posture warns (unknown posture) unless a profile declares it.

The three forms

Single — <agent> <posture> <target>

The common case. Creates a stance on target; for a focus-creating posture, also creates the focus (with the inferred kind and the block body).

team considers postgres-option
  A single Postgres instance with a partitioned events table.

→ a focus postgres-option (kind option) + a stance team considers postgres-option.

suspects<agent> suspects <from> <relation> <to> [as <alias>]

Proposes a relationship and takes a stance on it. Creates both endpoint foci, a link (aliased if you give as <alias>), and a stance whose target is the link — so the suspicion is about the inference itself.

analyst suspects ai-automation causes job-displacement as displacement-hypothesis
  confidence 0.45..0.70

infers<agent> infers <target> from <id-list>

Draws a conclusion. Creates target (kind claim) and one supports link from each source; a weight field applies to all of them.

analyst infers adaptation-too-slow from ai-capability-surge, reskilling-lag
  confidence 0.60

Fields on a stance

  • confidence — a number, a range (lo..hi), or ?. Out-of-form values error.
  • until <ref> [status] — desugars to a <ref> blocks <target> link (see Questions).
  • note — free-text rationale; always attaches to the stance, for every posture.
  • any other field — attaches to the stance.

Body routing

  • A focus-creating posture: the indented prose becomes the focus’s body.
  • A non-creating posture: the prose becomes a note on the stance.

So team chooses postgres-option\n Start with Postgres. puts the sentence on the focus, while economist doubts displacement-hypothesis\n Betting on precedent. puts it on the stance as a note.

Disagreement and contradiction

Multiple agents taking different postures on one target is normal — that’s a recorded disagreement. But a single agent taking mutually incompatible postures on the same target is flagged. The incompatible pairs are: accepts/rejects, accepts/doubts, chooses/rejects, holds/rejects. See Diagnostics.

Questions

A question is an open issue in the reasoning.

question which-datastore
  Which datastore should back the event log?
  expects option
  status open

Anatomy

PartSourceMeaning
idthe header (question <id>)
bodyindented prosethe question itself
expectsexpects <symbol>what kind of answer settles it (option, number, forecast, …)
statusstatus <symbol>typically open, or settled
asks_aboutabout <id-list>the foci the question concerns
fieldsother field linesfree-form

A question should carry at least a body or an expects field — a bare question with neither warns. Duplicate expects or status fields warn (the last wins).

about

question new-jobs-in-time
  Will new jobs arrive fast enough to offset the losses this decade?
  about job-displacement, technology-creates-jobs
  expects forecast

Each id in about is resolved; an unresolved one warns.

Answering

Two ways to record that a question is resolved:

  • a link <source> answers <question> edge, or
  • an answers <question> field on the stance that settles it:
team chooses postgres-option
  answers which-datastore

Blocking — until

A stance can declare it’s blocked until a question is answered, via the until field:

team holds datastore-decision
  until throughput-benchmark answered

until <ref> [status] desugars to a link <ref> blocks <target>, preserving the optional status word as a field on that link. The result: the blocking question appears in the graph as an edge holding the decision up. This is the idiom for “we can’t decide yet, and here’s exactly what we’re waiting on.”

Fields

A field is an indented name value line inside a record’s block. Known fields have defined meanings; unknown but well-formed fields are preserved (and warned about under strict mode).

A field line

<name> <value...>

The name is the first token; the rest is the value, classified into a number, range, ref, list, time, text, etc. A line whose first token is a known field name is always a field; an unknown lowercase identifier followed only by value-shaped tokens is treated as a field too.

Known fields

FieldAttaches toValuePurpose
kindfocussymbolthe focus’s kind
quantityfocus<number> <unit>a typed measure
confidencestancenumber / range / ?credence in the target
weightlink0..1evidential strength
probabilitylink (leads-to)0..1outcome likelihood
notestancetextrationale (always on the stance)
becausestancerefthe supporting reason
answersstancerefthe question this settles
untilstance<ref> [status]desugars to a blocks link
aboutquestionid-listwhat the question concerns
expectsquestionsymbolthe kind of answer wanted
statusquestion / linksymbolopen, answered, …
sourceanytext / uri:provenance of the record
observed-atanytimewhen observed
asserted-atanytimewhen asserted
valid-duringanystart..endthe span it holds over

A few more are recognized so they parse cleanly as fields: noted-by, noticed-by, suspected-by, chosen-by, blocked-by, undercut-by.

Reference fields

Four fields hold a reference to another record and are resolved: because, answers, blocked-by, undercut-by. If the referenced id doesn’t exist, you get a warning. (References in these fields also count for the orphan check — a focus reached only via a because is not an orphan.)

Provenance basis

quantity, confidence, weight, and probability may carry a trailing basis keyword — measured, estimated, or assumed:

  confidence 0.9 assumed
  quantity 30 GB measured

See Numbers, units, provenance.

Profile-only fields

Inside a profile record, four list-valued fields declare custom vocabulary: kinds, relations, fields, postures. They’re only meaningful there — see Profiles.

Inherited fields

Inside a scope, four fields cascade to members that don’t set their own: asserted-at, observed-at, source, valid-during. This lets a scope stamp a whole investigation with one date and source.

Scopes

A scope groups related records and can cascade context onto them.

scope incident-742
  source on-call-log
  observed-at 2026-06-17

  focus metric-shift
    Activation rose after the deploy.

  analyst noticed metric-shift

Membership by nesting

Records written indented inside a scope become its members. The scope’s includes array lists their ids, in order. Nesting is the only place indentation changes meaning: outside a scope, an indented header is unusual and warns (it’s still desugared, at the top level, so nothing is lost).

Scopes nest to any depth:

scope college-choice
  scope what-i-want
    focus goal-research
      kind goal
      Do undergraduate research with leading faculty.
  scope the-decision
    focus where-to-go
      kind decision
      Which offer do I commit to?

The bundled ship-or-hold.thml uses four sub-scopes (goals / evidence / decision / second-thoughts) to organize a real decision.

Inheritance

A scope cascades four context fields onto every member that doesn’t set its own: asserted-at, observed-at, source, valid-during. The rule is member-wins — a member’s own value is never overwritten — and innermost wins, so a sub-scope’s default overrides an outer scope’s.

This means you can stamp an entire investigation with one date and source at the top, and only override where a specific record differs:

scope launch-readiness
  asserted-at 2026-06-01        # every member inherits this date…
  analyst suspects early-burndown causes on-track
    asserted-at 2026-06-01
  analyst revises on-track
    asserted-at 2026-06-08      # …except where one says otherwise

A scope is organizational. Links and stances can’t target a scope — only foci, questions, and links are valid endpoints. Use a scope to group, not to relate.

Profiles, imports, namespaces

Advanced. These features let ThoughtML scale beyond a single document and a single vocabulary. They’re fully implemented and tested, but most documents never need them — reach for them when you do.

Profiles — custom vocabulary

The core vocabulary (kinds, relations, postures, fields) is deliberately small. A profile lets a document’s dialect declare extra terms so strict validation accepts them instead of warning.

profile risk-analysis
  kinds risk, mitigation
  relations aggravates
  postures flags
  fields likelihood

# now these are first-class in this document:
focus port-strike
  kind risk

link weak-monitoring aggravates port-strike

ops flags port-strike
  likelihood high

A profile declares four list-valued fields — kinds, relations, fields, postures — and any term it lists stops triggering the “unknown kind/relation/ field/posture” warnings. The bundled threat-model.thml is a complete example. The profile itself is recorded as a Profile object (document metadata, not a referenceable node).

Imports — multiple documents

A document can pull in another and reference its records under a namespace:

import shared-defs as base

link my-plan depends-on base.capacity-budget
  • import <name> as <ns> makes the records of document <name> available under the prefix <ns>..
  • A reference like base.capacity-budget resolves to the capacity-budget record in the imported shared-defs document.
  • Imports resolve recursively (an imported doc may import others), and import cycles are detected, reported, and broken.

How imports are resolved

Imports are a project-level concern — the host has to supply the other documents’ sources:

  • CLI: when an entry file contains import lines, the parser reads the sibling files <name>.thml from the entry’s directory.
  • Playground: it resolves imports against its bundled examples.

Every id and structural reference from an imported document is prefixed with the namespace, so two documents can use the same local id without collision. The bundled compliance-rollout.thml (which imports control-library.thml) must be run as a project to resolve — open it in the playground, or run it through the CLI from the examples directory.

v1 limitations. References inside a formula string are not namespace- rewritten, and agent names stay global (not prefixed).

Numbers, units, provenance

ThoughtML is careful with numbers: one encoding per concept, typed units, and optional provenance.

The four authored numbers

NumberWhereRange / form
confidencestancea scalar, a range lo..hi, or ?
weightlink0..1 (clamped, with a warning, if outside)
probabilitylink (leads-to)0..1 (clamped, with a warning, if outside)
quantityfocus<number> <unit>

confidence is the only one that accepts a range or ?. A confidence range must be ordered (0.45..0.70, not 0.70..0.45).

Quantities and units

A quantity is a number plus a unit, classified into a dimension:

focus aid-offer
  quantity 78000 USD

focus p99-latency
  quantity 200 ms

focus disk-budget
  quantity 1.5 GB
  • Both spaced (200 ms) and fused (200ms, 1.5GB, 30%) forms parse.
  • Recognized dimensions include time, information, currency, count, rate, and ratio.
  • Where a unit is convertible, the quantity is also normalized to its dimension’s base unit (so 1.5 GB and 500 MB can be compared, and formulas can compute over them). The canonical JSON keeps both the authored value/unit and the normalized/base_unit.
  • A malformed quantity (no leading number + unit) warns and is dropped — the rest of the focus is unaffected.

Quantities are authored, never derived. A value computed by a formula lands in a separate computed_quantity, so the two never get confused.

Provenance

Any authored number may declare its basis — how it was arrived at — as a trailing keyword:

BasisMeaning
measuredobserved / counted directly
estimatedreasoned approximation
assumedtaken as given, not checked
ops-agent holds cache-is-safe
  confidence 0.9 assumed

focus disk-budget
  quantity 30 GB measured

link firms-cutting-headcount supports displacement-hypothesis
  weight 0.85 measured

The basis is stored on the record (stance.basis, quantity.basis, link.basis). A computed value has no basis — provenance is for authored numbers only.

Making provenance mandatory

By default a number with no basis is simply silent about it (documents stay clean). Opt into enforcement:

thoughtml --strict-provenance doc.thml

This warns on any quantity, confidence, weight, or probability that omits a basis. It’s the honest-numbers discipline turned up to a hard check — useful in CI for documents where every number should say where it stands.

This closes the gap the old strongly / weakly adverbs left: a number no longer passes as fact without saying on what footing it stands.

Diagnostics

Diagnostics are how ThoughtML tells you something’s wrong with a document’s form. They come in two severities:

  • Error — the document is malformed. The CLI exits non-zero.
  • Warning — suspicious but parseable. The CLI still exits zero, unless you pass --strict (which makes warnings fail too).

Diagnostics go to stderr; the JSON model goes to stdout. They are distinct from the mirror’s conflict report, which judges a document’s coherence, not its form, and never fails parsing.

The strict-clean invariant. Every bundled example parses with zero errors and zero warnings under default options. A test (bundled_examples_are_strict _clean) enforces it, so the corpus can’t silently rot.

Codes and machine-readable output

thoughtml check --json emits each diagnostic with a stable code, its severity, line, message, and — where one can be computed — a suggested help fix:

{ "code": "TML102", "severity": "warning", "line": 5,
  "message": "unknown relation `supprts`",
  "help": "did you mean `supports`? (relations are a closed set)" }

Codes are grouped so an agent or editor can route on the family:

RangeFamilyExamples
TML1xxvocabularyTML101 unknown kind, TML102 unknown relation, TML103 unknown posture, TML104 unknown field
TML2xxreferencesTML201 unresolved reference, TML202 illegal link endpoint
TML3xxgraph coherenceTML301 orphan, TML302 contradictory stances, TML303 cycle, TML304 revision before its target, TML305 decision-graph
TML4xxnumbersTML401 missing basis, TML402 out-of-range clamp
TML5xxlints (opt-in)TML501 supports used as a list

For the “unknown <thing>” family the help is a nearest-spelling suggestion from the relevant closed vocabulary — it catches typos like supprtssupports. Codes are part of the tool’s contract and are not renumbered once assigned.

Errors

Message (abbreviated)Cause
tab indentation is invalid; v0 requires spacesa tab in leading whitespace
indented block line before any record headera block line with no open record
<kw> header expects exactly one identifiermalformed focus/scope/question/profile header
link header expects [alias:] from relation towrong link arity
stance header expects [alias:] agent posture targetwrong stance arity
import header expects import <name> as <namespace>malformed import
unknown record kind or header …first token isn’t a keyword, second isn’t a posture
<posture> action expects a single target identifierwrong arity for a simple action
suspects expects from relation to [as alias]malformed suspects
infers expects target from id-list / requires at least one sourcemalformed infers
invalid identifier/symbol … (expected lowercase kebab-case)bad token
confidence must be a number, range, or ?non-numeric confidence
confidence range must be ordered low..highreversed range
weight/probability must be a number in 0..1non-numeric weight/probability
link.from/to … targets a <kind>; links may only connect foci, questions, or linksa link pointing at a stance or scope

Warnings

Indentation & structure

  • block lines should be indented by two or more spaces
  • only a scope may contain nested objects; desugaring them at the top level

Ids & kinds

  • duplicate id / id is reused across records
  • focus … was declared as kind X but redeclared as Y; keeping X
  • duplicate <field> field; using the last (kind, confidence, weight, probability, expects, status, formula)
  • unknown focus kind / unknown posture / unknown relation / unknown field (unless a profile declares it)

Values

  • quantity should be <number> <unit>
  • weight/probability should be in 0..1; clamping
  • probability on a <rel> link is ignored / a weight on a leads-to link is ignored
  • kind requires a value / until requires a reference
  • question should include body text or an expects field / about expects one or more ids

Reference resolution

  • … is an unresolved reference (link endpoints, stance targets, about, because/answers/blocked-by/undercut-by, formula refs)

Semantic lints (need the whole graph)

  • agent … takes contradictory stances on … — incompatible posture pairs: accepts/rejects, accepts/doubts, chooses/rejects, holds/rejects.
  • cyclic dependency: a → b → a — a cycle among causes / depends-on edges.
  • focus … is not connected to anything — an orphan: nothing links it, no stance targets it, no field references it.

Opinionated lints (only with thoughtml check --lint)

  • focus … gathers N supports links and no counter-evidence (TML501) — a claim used as an enumeration. Evidence relations inflate derived_confidence; if these are list items, use part-of instead. Off by default so strict-clean documents are unaffected.

Decision graph

  • leads-to edge … points outcome … at itself
  • option … has no leads-to outcomes, but its sibling options do — it’d be silently missing from the EV ranking.

Temporal

  • … revises … but is asserted earlier
  • valid-during … ends before it starts

Compute (only with the relevant opt-in flags)

  • formula: parse error, unresolved/quantity-less reference, dimension mismatch, dependency cycle.
  • decision EV: missing probability/payoff, mixed dimensions, probability mass > 1.

Provenance (only with --strict-provenance)

  • … declares no basis (add measured/estimated/assumed)

Imports

  • unknown import … / import cycle through …; skipped

The Mirror

Everything so far describes what you author — foci, links, stances, numbers. The mirror is what ThoughtML computes back: a second, mechanical reading of your structure. Where that reading disagrees with what you said, you have something worth looking at.

This is the heart of the language’s philosophy:

A mirror, not an oracle. The engine produces a second reading that can disagree with the author — but it reports the disagreement; it never overrides the author or hands down a verdict.

It’s all opt-in

None of the mirror runs by default. The base pipeline (parse → desugar → validate) emits stable canonical JSON with nothing computed. You turn readings on with flags:

FlagReading
--derivedDerived confidence — propagate evidence into a per-claim strength
--statusArgument status — grounded in/out/undecided
--auditConflict report — where confidence disagrees with status
--sensitivityper-edge leverage
--formulasevaluate = expr foci into computed_quantity
--decisionsdecision expected value
--actsemit Act provenance objects for readable actions
--computeall of the above
thoughtml --compute doc.thml      # the full second reading

The playground turns the display-relevant readings on by default — so what you see in the browser is the mirror, live.

Why opt-in

Two reasons:

  1. Stable output. A document without derivations serializes byte-for-byte identically every time, which is what keeps the example corpus strict-clean and makes the CLI safe to diff in CI.
  2. Computed ≠ authored. Every derived value lives in its own field, beside (never replacing) what you wrote. derived_confidence sits next to your authored confidence; computed_quantity next to your quantity. The mirror adds a reading; it never edits yours.

The four readings, in one line each

  • Derived confidencehow strong is this claim, given its evidence?
  • Argument statusdoes it survive every attack?
  • Conflict report — where do your stated beliefs and your structure disagree?
  • The compute layer — quantities, formulas, and expected value as a second reading of your numbers.

The following pages explain how each is computed.

Derived confidence

Flag: --derived (or --compute).

Derived confidence answers “how strongly does the evidence back this claim?” — computed by propagating belief through the evidence graph, independent of any confidence you authored.

The model

Every evidence edge — supports, opposes, undercuts — pointing at a target contributes to its strength. For a target with incoming edges:

sum     = Σ  polarity · weight · believedness(source)
derived = logistic(2 · sum)

where:

  • polarity is +1 for supports, −1 for opposes / undercuts.
  • weight is the link’s weight, or 0.5 if none is given.
  • believedness(source) is how much the source itself is believed: its own derived confidence if it has one (so belief propagates transitively), else its authored confidence, else 1.0 (an unqualified assertion counts as given).
  • logistic(x) = 1 / (1 + e⁻ˣ), squashing the sum into 0..1.

The gain constant 2 is chosen so that a single strong support (weight 0.85, fully-believed source) lands the target at ≈0.85. A target with no net evidence sits at logistic(0) = 0.5 — the neutral point.

Propagation order

Belief flows in topological order (Kahn’s algorithm) over the evidence graph, so a conclusion is computed after its premises — it sees their derived strength, not just their authored confidence. Any nodes left on an evidence cycle are resolved once, in declaration order, as a documented best-effort.

The computation is pure and deterministic: same inputs, same output, every time.

Authored belief

The “believedness” of an authored node is the mean midpoint of the non-superseded stances that target it and carry a confidence (a range counts at its midpoint). A belief that’s been revised no longer counts as live evidence.

Undercutting an inference

undercuts has a power opposes doesn’t. When an undercuts edge targets a link (an inference rather than a claim), it doesn’t push the node down — instead it weakens that connection. Each undercut leaves the inference at a fraction of its strength: an undercut with weight 0.85 leaves 1 − 0.85 = 0.15 of it. Multiple undercuts multiply. With no inference-undercut present, every weight is untouched and the output is identical to the simple model above.

Where it appears

derived_confidence is set on every focus and link that is the target of evidence, rounded to three decimals, e.g.:

{ "type": "focus", "id": "displacement-hypothesis",
  "derived_confidence": 0.94 }

In the playground, it shows in the detail panel beside your authored confidence — two bars, never merged. On the bundled hiring-panel.thml, the displacement hypothesis lands ≈0.94 while the optimist’s rebuttal comes out ≈0.22, several hops deep.

Argument status

Flag: --status (or --compute).

Where derived confidence asks “how strong?”, argument status asks a sharper question: “does this claim survive every attack?” The answer is one of three labels.

LabelMeaning
inaccepted — every attacker is defeated
outdefeated — at least one attacker is accepted
undecidedneither — e.g. a mutual attack with no resolution

This is the grounded extension from Dung’s argumentation framework (1995).

The attack graph

Only two relations count as attacks:

  • opposes — rebuts a node.
  • undercuts — defeats an inference.

There’s no separate “defends” relation, because defense falls out for free: defending X means attacking X’s attacker. If risk is attacked by port-strike, and guard opposes port-strike, then port-strike goes out and risk is reinstated to in — automatically.

The labelling

Computed to the least fixpoint:

  1. Start every contested node undecided.
  2. Repeatedly:
    • label a node in if all of its attackers are already out (a node with no attackers is in immediately);
    • label a node out if any attacker is in.
  3. Stop when nothing changes.

The result is unique and deterministic. Nodes caught in an unbroken mutual attack stay undecided.

Worked example

From the tutorial’s ship-the-hotfix.thml:

link load-test-passed supports cache-is-safe
link stale-reads opposes cache-is-safe

stale-reads has no attackers → in. It opposes cache-is-safe, and that attacker is in, so cache-is-safeout. (The supports edge doesn’t enter the status calculation — support isn’t an attack; it feeds derived confidence instead.)

That out is exactly what the conflict report compares against the agent’s authored 0.9.

Where it appears

argument_status is set on every focus and link that takes part in the attack graph:

{ "type": "focus", "id": "cache-is-safe", "argument_status": "out" }

It reads as a node colour/badge in the playground’s Argument lens.

Conflict reports

Flag: --audit (or --compute).

The conflict report is the mirror’s flagship: it surfaces where what you asserted disagrees with what your own structure implies. It ships the conflict; it never auto-corrects.

A separate channel

Conflicts are not diagnostics. Diagnostics judge a document’s form (is it well-formed?). Conflicts judge its coherence (do your beliefs hang together?). A document can be perfectly strict-clean and still carry a conflict — that’s the interesting case. So conflicts ride their own channel, in an audit section, and never affect strict parsing.

"audit": {
  "conflicts": [
    { "kind": "confidence-vs-status", "severity": "error",
      "subjects": ["ops-agent-holds-cache-is-safe", "cache-is-safe"],
      "message": "`ops-agent` asserts confidence 0.90 in `cache-is-safe`, but your own structure defeats it (argument status: out)" }
  ]
}

Each conflict has a kind, a severity (error / warning / info), the subjects it concerns, and a human-readable message.

confidence-vs-status

It compares each authored stance’s confidence against the grounded argument status of its target. Two cases fire:

ConditionSeverityReading
target is out and confidence ≥ 0.66errorhigh credence in a claim the structure defeats
target is in and confidence ≤ 0.34warninglow credence in a claim that survives every attack

(A confidence range is taken at its midpoint. Stances on targets that don’t take part in the attack graph are not compared.)

The first case is the flagship — you wrote down the objection and believed the claim anyway. The second is the inverse tell — you’re underweighting something your own evidence upholds.

It reports; it does not decide

This is worth stating plainly, because it’s the whole design. When the mirror finds a confidence-vs-status conflict, it does not:

  • lower your confidence,
  • flip the argument status,
  • or tell you which one is right.

Maybe the structure is incomplete (a missing rebuttal would change the status). Maybe the confidence is the honest number and the structure overstates the attack. The mirror can’t know — you do. It just makes the disagreement impossible to miss.

The bundled ship-the-hotfix.thml exists precisely to demonstrate this: clean document, real conflict, no verdict.

definition-divergence

The second conflict type catches a different kind of disagreement: the same focus defined more than once with differing content.

{ "kind": "definition-divergence", "severity": "warning",
  "subjects": ["launch-date"],
  "message": "`launch-date` is defined more than once with differing content; all 2 definitions are kept" }

Ordinarily a repeated focus id merges (first-wins on body / quantity / formula). But when a later mention states a genuinely different value, ThoughtML does not drop it — every alternative is retained on the focus’s divergent list, and this conflict points at the disagreement. It’s the lossless-authoring tell: two agents (or two of your own passes) wrote down incompatible versions of the same thing, and the mirror asks you to reconcile them rather than picking one silently.

More conflict types are coming

confidence-vs-status and definition-divergence are the first two. The conflict report is built as an extensible channel; future readings (calibration drift, numeric inconsistency, stale beliefs) will land here as additional kinds — each one a disagreement surfaced, never a decision made.

The compute layer

Flags: --formulas, --decisions, --sensitivity (or --compute).

ThoughtML can compute over the numbers in a document — formulas, expected value, sensitivity. This is the most powerful part of the mirror, and the one to be most careful about framing:

The compute layer is a second reading of the author’s numbers, not a program the document runs. Every result is opt-in, lands in its own field, and never overwrites what you wrote.

Quantities recap

A focus can carry an authored quantity — a number with a unit, classified into a dimension and normalized to a base unit where convertible. Quantities are the inputs the rest of this layer reads.

Formulas

--formulas. A focus whose value is computed from other foci, written as a = <expr> line:

focus hosting
  quantity 1200 USD
focus bandwidth
  quantity 300 USD
focus monthly-cost
  = hosting + bandwidth
  • The expression supports references to other foci, numbers, quantities, the arithmetic operators + - * / ( ), and functions like min/max/sum.
  • Evaluation runs in dependency order, so a formula sees its inputs’ computed values. A dependency cycle is detected and reported (never computed).
  • Full dimensional analysis: you can multiply USD/instance by instance, but not add dollars to milliseconds — a dimension clash is a warning.
  • The result lands in computed_quantity, presented in a human-friendly unit (8 GB, not 8e9 B) and strictly separate from any authored quantity. A computed value has no provenance basis — it wasn’t authored.

The bundled cloud-bill.thml is a full worked example.

Decision expected value

--decisions. The capstone, composing quantities, formulas, and derived confidence. The model:

  • An option focus has leads-to edges to outcome foci.
  • Each leads-to edge carries a probability; if it doesn’t, the outcome’s derived confidence is used as a fallback.
  • Each outcome carries a payoff — its computed_quantity if a formula produced one, else its authored quantity.

Then:

expected_value(option) = Σ  probability · payoff

with full dimensional checking (you can’t average dollars with milliseconds). A decision focus, named by option-of edges, gets its options ranked by expected value, highest first. Each option also reports its downside (the worst-case payoff) and probability_mass (Σ probability).

link harvard option-of where-to-go
link harvard leads-to harvard-thrive
  probability 0.7
link harvard leads-to harvard-coast
  probability 0.3

It ranks; it does not crown. There is deliberately no best option and no margin. The mirror reports the expected values, ordered, with each option’s downside — and leaves the choice to you. Decisions are about risk, not just the mean, and the call is yours. See ship-or-hold.thml and ship-or-hold.thml.

Diagnostics (never errors) flag the gaps: an outcome with no payoff, a leads-to with no probability and no derived confidence, mixed dimensions, or an authored probability mass over 1.

Sensitivity (leverage)

--sensitivity. How load-bearing is each piece of evidence? For each evidence edge e into target T:

leverage(e) = derived(T) − derived_without_e(T)

It recomputes the target’s derived confidence with that one edge removed (a target left with no evidence falls to the neutral 0.5), and records the difference. Positive leverage means e props the target up (a support); negative means it drags it down (an attack); the magnitude is how much the conclusion rests on that single edge.

leverage is set on each evidence link. The bundled ship-or-hold.thml ranks evidence by it. It is computed by re-deriving the graph with each edge ablated in turn — single-edge sensitivity, precomputed for every edge at once — so the CLI never perturbs the document; it reports it as authored.

Use cases

ThoughtML earns its keep wherever the reasoning matters as much as the conclusion — where someone later needs to check why, not just what. Here are the concrete situations it’s built for.

1. Decision records you can lint

An architecture decision record (ADR) is usually prose: “we chose Postgres because…”. Written as ThoughtML, the ADR is a graph — options considered, the evidence for and against each, the question that blocks sign-off, the choice and its justification. Now it’s checkable: did you actually reject the alternatives for stated reasons? Is the decision still blocked on an open question?

See choose-datastore.thml.

2. AI agent reasoning a human (or CI) can audit

This is the headline use case. An AI agent makes a call — what to ship, which fix to apply, how to triage. Instead of a paragraph of justification, it emits a ThoughtML document: the claim, the evidence it weighed, its confidence, and the basis of each number. A human, another agent, or a CI step then runs the mirror over it and catches the tells: high confidence in a defeated claim, numbers marked assumed where they should be measured, dangling assumptions.

The agent does the reasoning; ThoughtML makes it legible enough to check. See ThoughtML for AI agents.

3. Design and code review of an argument

Reviewing a proposal often means reviewing an argument, and arguments hide their flaws in prose. A ThoughtML version surfaces them: the confidence-vs-status conflict catches “you hold this at 0.9, but your own listed risk defeats it” — the exact thing a reviewer is trying to notice and often misses.

The canonical demo is ship-the-hotfix.thml.

4. Incident postmortems / root-cause analysis

A postmortem is a causal story under uncertainty: a metric shifted, a deploy is suspected to have caused it, evidence accumulates, a fix is chosen but blocked on a benchmark. ThoughtML keeps the causal links, the suspicion (with a confidence range, honestly), and the blockers explicit — and flags impossible causal cycles.

See triage-742.thml.

5. Research and claim mapping with provenance

Mapping a contested question — does X cause Y? — means tracking claims, the evidence weight behind each, who holds what, and where the numbers came from. ThoughtML’s provenance basis and graded weights make a literature map you can interrogate, not just read.

See hiring-panel.thml.

6. High-stakes personal decisions

Not everything is engineering. A big personal choice — which job, which school — has goals, evidence, options with uncertain payoffs, and a downside you’d rather not face. Writing it out as ThoughtML forces the structure into the open and lets you compare options by expected value without pretending the number decides for you.

See ship-or-hold.thml.

When not to reach for it

  • For a quick note with no argument structure, prose is fine.
  • For hard numeric modelling, use a spreadsheet or real code — ThoughtML’s compute layer is a reading of your numbers, not a computation engine.
  • For a decision nobody will ever need to re-examine, the overhead isn’t worth it.

The common thread in every good fit: the reasoning will be revisited, by someone who needs to trust it.

ThoughtML for AI agents

ThoughtML is designed for an age where an AI agent can emit reasoning structure at no cost, and a human (or another agent, or CI) audits it. This guide is about that workflow.

Why a language, and why this one

When an agent explains a decision in prose, the explanation is unstructured: you can read it, but you can’t check it mechanically. ThoughtML gives the agent a target format that is:

  • Cheap to emit. It’s plain text with a small, regular grammar. An LLM can produce it reliably.
  • Typed and explicit. Every claim has a kind, every link a direction and meaning, every belief a confidence and (optionally) a basis. Nothing important is implied.
  • Auditable. Once it’s structure, the mirror can read it a second way and flag where the agent’s own structure betrays its stated confidence.

The point is not to have the agent compute the answer in ThoughtML. It’s to make the agent’s reasoning legible enough that its flaws can’t hide.

The author/auditor loop

agent reasons → emits .thml → mirror reads it back → conflicts surface → human/agent resolves

A concrete version:

  1. An agent decides to ship a change and writes a ThoughtML document: the claim (cache-is-safe), the evidence it weighed, its confidence, and the basis of each number.
  2. CI runs thoughtml --audit (and maybe --strict-provenance).
  3. The conflict report catches that the agent held a claim at 0.9 that its own recorded counter-evidence defeats.
  4. A human looks at exactly that one disagreement — not the whole paragraph.

Self-correcting against machine-readable diagnostics

An agent authoring ThoughtML doesn’t have to get it right first try — it can loop against the validator. thoughtml check --json emits each diagnostic with a stable code, the offending line, and a suggested help fix (e.g. unknown relationdid you mean supports?). The loop is: emit → check --json → apply the suggested fixes → repeat until clean. Add --lint to catch the supports-used-as-a-list smell that silently inflates confidence.

For authoring from scratch, the whole language travels inside the tool: run thoughtml guide --full for a single self-contained, source-derived brief — closed vocabularies, the distinctions that matter, and how to read the mirror back — meant to be pasted into a system prompt. (thoughtml guide alone prints a one-screen tour, and thoughtml guide <topic> looks up a single section.) It’s the same llms.txt the site serves and the packages embed — one source, so the CLI, the site, and the dump can never disagree.

Practical tips for generating ThoughtML

  • Declare foci with explicit kinds. It makes the graph readable and lets the kind-mismatch lint catch category errors.
  • Use confidence ranges for genuine uncertainty (0.45..0.70) rather than a false-precision point estimate.
  • Always set a provenance basis. This is the single most valuable habit for an agent: a 0.9 assumed is honest in a way a bare 0.9 is not. Run CI with --strict-provenance to enforce it.
  • Record the counter-evidence. The mirror can only catch a confidence-vs-status conflict if the opposing observation is in the document. An agent that writes down what argues against its own conclusion gets the most value.
  • Keep computed and authored numbers separate — which the language does for you: never put a derived value in an authored field.

In CI

A minimal gate for agent-authored documents:

# fail on malformed structure (warnings included) and missing provenance
thoughtml --strict --strict-provenance reasoning.thml > model.json

# inspect the conflict report
thoughtml --audit reasoning.thml | jq '.audit.conflicts'

A non-empty confidence-vs-status error is a signal worth a human’s attention: the agent believed something its own structure defeats.

See assistant-memory.thml for an agent’s evolving memory of a user, and ship-the-hotfix.thml for the audit in action.

CLI reference

The reference implementation’s command-line tool is thoughtml. With no subcommand it parses a .thml file and emits the canonical object model as JSON; subcommands add the rest of the toolchain — validation, formatting, tracing, and a belief-level diff.

thoughtml [OPTIONS] <FILE>      # parse + emit the canonical JSON model
thoughtml check <FILE>          # validate and report diagnostics
thoughtml fmt <FILE>            # rewrite in the canonical style
thoughtml explain <FILE> <ID>   # trace a node's derived confidence / status
thoughtml diff <A> <B>          # semantic (belief-level) diff of two documents

Install

With a Rust toolchain, install the binary onto your PATH (~/.cargo/bin):

cargo install --path crates/thoughtml     # from the repository root
thoughtml --help

Re-run the same command after changing the parser to update the installed binary. The result is a single self-contained executable with no runtime dependencies.

Default invocation — parse and emit

thoughtml [OPTIONS] <FILE>

<FILE> is the input path, or - to read from stdin.

  • stdout — the canonical JSON.
  • stderr — diagnostics, sorted by source line.
  • exit code — non-zero if there are errors (or, with --strict, warnings).

Output options

FlagEffect
--astEmit the surface AST instead of the canonical model.
--compactSingle-line JSON instead of pretty-printed.
--htmlEmit a self-contained interactive HTML viewer instead of JSON (implies --compute). See The standalone viewer.
-o, --out <PATH>Write output to a file instead of stdout.
--strictTreat warnings as failures for the exit code.

Time options (as-of replay)

Project the model to a point in time before emitting it (see Time and revision). Dangling links and stances are cascaded away so the projection stays coherent.

FlagEffect
--as-of <INSTANT>Keep only what was valid as of this date/time (valid-time axis).
--as-of-seq <N>Keep only the first N recorded events (transaction order). The two are mutually exclusive.

Mirror options (opt-in readings)

All off by default; each adds a derived field to the output. See The Mirror.

FlagReading
--derivedderived_confidence — propagate evidence (§10.3)
--statusargument_status — grounded in/out/undecided
--auditthe conflict report (confidence-vs-status)
--sensitivityper-edge leverage
--formulasevaluate = expr foci into computed_quantity
--decisionsdecision expected value over leads-to / option-of
--actsemit Act provenance objects for readable actions
--strict-provenancewarn on numbers with no measured/estimated/assumed basis
--computeturn on all the mirror readings above (except --acts / --strict-provenance)

thoughtml check — validate

Parse and report diagnostics without emitting the model — the tight authoring gate.

thoughtml check <FILE> [--json] [--lint] [--strict]
FlagEffect
--jsonEmit diagnostics as JSON — a stable code, severity, line, message, and a suggested help fix. Built for editors, CI, and AI agents that self-correct in a loop.
--lintAlso run opinionated modeling lints. Today: the supports-used-as-a-list detector (TML501) — a claim with many supports edges and no counter-evidence is probably an enumeration that should be part-of, which would otherwise inflate its confidence.
--strictExit non-zero on any warning, not just errors.

Diagnostics carry stable codes (TML1xx vocabulary, TML2xx references, TML3xx graph coherence, TML4xx numbers, TML5xx lints) and, for the “unknown <thing>” family, a nearest-spelling suggestion from the closed vocabulary. See Diagnostics.

thoughtml check --json reasoning.thml     # machine-readable, for an agent loop
thoughtml check --lint --strict doc.thml  # opinionated + fail on any warning (CI)

thoughtml fmt — format

Rewrite a document in the one canonical style: two-space indentation, a blank line between records, and a normalized field/body order. fmt re-parses its own output and refuses to write if the model would change, so formatting is always safe. It declines a document with parse errors. (Comments are not yet preserved.)

thoughtml fmt <FILE>          # print the formatted document to stdout
thoughtml fmt -w <FILE>       # rewrite the file in place
thoughtml fmt --check <FILE>  # exit non-zero if not already formatted (CI)

thoughtml explain — trace a reading

Explain why a node reads the way it does: its derived confidence and grounded argument status, the evidence for and against it (each edge’s weight and leverage), the stances agents hold on it, and any mirror conflict it is caught in.

thoughtml explain <FILE> <ID>
$ thoughtml explain hiring.thml strong-hire
strong-hire  (claim)
  Alex is a strong hire.

  derived confidence : 0.500
  argument status    : out  (defeated)

  evidence in:
    opposes    take-home-failed   weight -   leverage -0.231  (source: in)
    supports   aced-interview     weight -   leverage +0.231  (source: -)

  stances:
    panel holds  confidence 0.9

  conflicts:
    [confidence-vs-status] `panel` asserts confidence 0.90 in `strong-hire`, but ... (out)

  why: defeated by attacker(s) that stand: take-home-failed.

thoughtml diff — belief-level diff

Compare two documents semantically, not textually: nodes added and removed, and for nodes in both, the changes that matter — derived confidence, grounded status (in/out), lifecycle status, supersession, a stance’s confidence, a link’s weight — plus the mirror conflicts that appeared or resolved between them. This is version control for reasoning.

thoughtml diff <BEFORE> <AFTER>
$ thoughtml diff before.thml after.thml
belief diff: A -> B

added (2):
  + con  (observation)
  + con-opposes-c  (link:opposes)

changed (1):
  ~ c
      confidence 0.731 -> 0.500
      status — -> out

conflicts:
  + [confidence-vs-status] `analyst` asserts confidence 0.90 in `c`, but ... (out)

Examples

# Canonical JSON + diagnostics
thoughtml examples/triage-742.thml

# The full second reading, compact, to a file
thoughtml --compute --compact -o out.json examples/ship-or-hold.thml

# Just the conflict report
thoughtml --audit examples/ship-the-hotfix.thml

# Replay: what did the document believe as of a date?
thoughtml --as-of 2026-01-13 examples/launch-readiness.thml

# A standalone interactive viewer — one self-contained HTML file, opens anywhere
thoughtml --html -o decision-record.html examples/choose-datastore.thml

# Enforce provenance and fail on any warning (good for CI)
thoughtml --strict --strict-provenance reasoning.thml

# Read from stdin
cat doc.thml | thoughtml -

Multi-document projects

If the input file contains import <name> as <ns> lines, thoughtml resolves it as a project: it reads each imported document as <name>.thml from the entry file’s directory, recursively, and merges everything into one model before validating and deriving. A missing import is reported as unknown import; an import cycle is reported and broken. See Profiles, imports, namespaces.

Running from source

Before installing the binary, you can run via cargo from the repository root (-p thoughtml selects the parser crate):

cargo run -p thoughtml -- --compute examples/ship-or-hold.thml

Using the playground

The playground is a live editor and graph view — the fastest way to see a ThoughtML document. It runs the exact same parser as the CLI, compiled to WebAssembly, so the two never disagree.

Open the playground → fatin-ishraq.github.io/ThoughtML/playground

No install — it runs entirely in your browser. To run it locally instead, see Installation: npm run wasm && npm run dev.

The playground is for authoring — live editing and examples. To share a finished document as a single self-contained interactive file (no server, opens anywhere), export it with the standalone viewer: thoughtml doc.thml --html -o doc.html.

The layout

  • Editor (left) — a code editor with ThoughtML syntax highlighting and a lint gutter. Diagnostics appear inline as you type.
  • Graph (centre) — the document rendered interactively, in one of two surfaces:
    • Viewer (default) — a time-driven view: reasoning laid out along time (earlier beliefs left, later right), vertical position emerging from a force layout, with an as-of bar and replay. This is the same renderer the standalone --html viewer uses.
    • Structural — the classic node-link graph: foci as nodes (shaped by kind), links as labelled arrows (styled by relation), stances attached to their targets.
  • Detail panel — click any node to see its facts: body, fields, authored numbers, and the mirror’s derived values beside them (never merged).
  • Example tray — load any bundled example to explore it.

The mirror is on by default

Unlike the CLI (where readings are opt-in), the playground turns the display-relevant mirror readings on, so you always see:

This is why a document can look clean in the editor (no diagnostics) yet show a conflict — exactly the ship-the-hotfix.thml case.

Lenses

On the Structural surface, a lens recolours the whole graph to foreground one reading:

  • Type — colour by record/kind. The default, for reading structure.
  • Argument — colour by in / out / undecided, to see what survives.

Replay (the as-of bar)

For documents with timestamps, the Viewer carries an as-of bar built into the timeline. Press play (or drag it back) and the reasoning replays moment by moment: beliefs fade in as of when they were asserted, and revised-away or abandoned branches dim — so you can watch a conclusion form (or fall apart) as evidence arrived. Try it on launch-readiness.thml or launch-readiness.thml. The same projection is on the CLI as --as-of.

Note. The playground curates a spine of ten examples and the two lenses above for v0.1.0. The compute and multi-document demos and additional lenses are parked, not deleted — the CLI exposes the full set of readings via flags.

The standalone viewer

The playground is for authoring; the standalone viewer is for sharing. thoughtml --html bakes a document into a single, self-contained HTML file that opens in any browser — no server, no install, no network.

thoughtml choose-datastore.thml --html -o decision-record.html

Open the result and you get the same time-driven view the playground shows under “Viewer”: reasoning laid out along time (earlier left, later right), pan / zoom, click a node for its detail, the legend, an as-of bar with replay, and light / dark — all running on a model baked into the file. Press play (or drag the bar) and beliefs fade in as of when they were asserted. --html turns on the full mirror compute stack automatically, so the derived readings have data to show.

What’s in the file

The exported artifact is the canonical JSON plus a small renderer, inlined into one self-contained HTML file (~600 KB). There is no WebAssembly and no parser inside it — parsing already happened when you ran the command. That is why it is small, offline, and deterministic: it carries the result, not the compiler. The fonts are the reader’s system fonts, so nothing is fetched.

A snapshot, by design

The viewer renders a snapshot of the model at export time. There is no live re-parsing inside the file — re-run thoughtml --html after editing the source to refresh it, the same way you would recompile.

Which surface when

You want to…Use
Author live, edit, experimentthe playground
Check a document in CI or a scriptthoughtml doc.thml → JSON + exit code
Hand someone an interactive, time-driven viewthoughtml doc.thml --html -o doc.html

All three render from the same parser and the same time-driven renderer — one canonical model, many faithful projections.

Glossary

Agent — the actor a stance attributes a belief to (ops-agent, analyst, team, me). Just an identifier; not a defined entity.

Argument status — the grounded Dung label of a node: in (survives every attack), out (defeated), or undecided. Opt-in (--status).

Attack — an opposes or undercuts link. The only relations that affect argument status.

Basis — the provenance of an authored number: measured, estimated, or assumed.

Body — the free-text prose under a record’s header.

Canonical model — the normalized, ordered array of typed objects the parser emits as JSON. The interchange form of the language.

Conflict — a mirror finding: a place the computed reading disagrees with what the author asserted. Distinct from a diagnostic.

Derived confidence — a per-claim strength the mirror computes by propagating evidence, separate from authored confidence. Opt-in (--derived).

Desugar — the step that turns the readable action surface (agent posture target) into canonical core objects.

Diagnostic — an error or warning about a document’s form (vs. a conflict, about its coherence).

Focus — a node you reason about; the basic unit. Has a kind.

Kind — a focus’s semantic category (observation, claim, decision, …).

Leverage — how load-bearing one evidence edge is: the change in its target’s derived confidence when the edge is removed. Opt-in (--sensitivity).

Link — a typed, directed edge with a relation.

Mirror — ThoughtML’s opt-in second reading of a document. “A mirror, not an oracle”: it reports disagreements, it never decides.

Orphan — a focus nothing connects to. Flagged as a warning.

Posture — the verb in a stance (holds, doubts, chooses, …).

Profile — a declaration of custom vocabulary (kinds/relations/fields/postures) a document’s dialect adds.

Quantity — an authored numeric measure with a unit and a dimension. A computed result is a separate computed_quantity.

Relation — the type of a link (supports, causes, leads-to, …). Twelve in v0.1.0.

Scope — a grouping of records that can cascade context onto its members.

Stance — an agent’s relationship to a target, optionally with confidence.

Strict-clean — parsing with zero errors and zero warnings under default options. Every bundled example must be strict-clean.

Supersession — marking a belief replaced by a later one (via revises), without deleting it. The basis of the as-of view.

Weight — a link’s evidential strength, 0..1. (Distinct from probability, the outcome likelihood on a leads-to edge.)

Example gallery

The reference implementation ships a corpus of twenty example documents in examples/. Every one parses strict-clean (zero errors, zero warnings) under default options — a test enforces it. They double as the playground’s example tray.

Open any of them in the playground to see the graph, or run thoughtml <file> (add --compute for the second reading, --audit for the mirror). The set is designed to span the language: many kinds and relations, both mirror conflicts, the temporal layer, the compute layer, profiles, and imports — across engineering, ops, medicine, science, business, product, security, and AI-agent scenarios.

Start here

ExampleWhat it teaches
ship-the-hotfix.thmlA clean document the mirror still flags — the confidence-vs-status conflict. The flagship demo.
triage-742.thmlThe canonical minimal document: noticed → question → suspects → hold-until. The smallest complete piece of reasoning.
weekend-plan.thmlThe plainest shape: a goal, two options, and the pick — proof it reads naturally for low-stakes reasoning.

Arguments and decisions

ExampleWhat it teaches
pr-feedback.thmlundercuts (attack an inference) versus opposes (attack a claim) — the two distinct ways to push back.
hiring-panel.thmlSeveral interviewers on one call: shared evidence, different confidence, because, per-stance notes.
choose-datastore.thmlAn ADR as a graph: options weighed, one rejected and kept abandoned, a blocking benchmark (until), the question settled.
differential-dx.thmlA clinician’s differential: competing hypotheses, candidate-for proposals versus the answers that resolves.
moderation-decision.thmlAn AI moderation call emitted for a human to audit — the confidence and evidence made inspectable.

Time and memory

ExampleWhat it teaches
launch-readiness.thmlA belief revised twice as evidence lands; earlier versions superseded, not erased. Replay with --as-of.
assistant-memory.thmlAn assistant’s evolving memory: noticed, infers-from sources, remembers, revises, an unknown (?).
merge-conflict-beliefs.thmlThe second mirror conflict: two agents define one focus two ways — definition-divergence, kept losslessly.

Causes, collections, and everyday reasoning

ExampleWhat it teaches
prod-outage.thmlA postmortem as an acyclic causes/enables/prevents graph, with nested scopes and -by attribution.
roadmap-priorities.thmlpart-of for grouping (not evidence), and a question that contains its candidate options as a thought-tree.
bad-oyster.thmlEveryday causal reasoning: suspects a cause and asks the question that would settle it.
replication-study.thmlWeighing a scientific claim: a critique that undercuts a failed replication, with measured/estimated bases.

The compute layer

ExampleWhat it teaches
cloud-bill.thmlA cost model that computes itself: = formulas over line items with full unit-checking (USD/hour × hour = USD).
ship-or-hold.thmlThe whole compute layer in one decision: formula payoffs, a probability borrowed from derived confidence, EV ranking, and a what-if that flips it.

Dialects and modularity (advanced)

ExampleWhat it teaches
threat-model.thmlA profile declaring a security dialect — custom threat/control kinds, mitigates/aggravates relations, likelihood/severity fields, a flags posture.
control-library.thmlA minimal importable library — the building block the rollout imports.
compliance-rollout.thmlimport … as and namespaced cross-document references. Run as a project with its library.

A walkthrough: ship-the-hotfix.thml

The most instructive example is the smallest interesting one:

focus hotfix-is-safe
  kind claim
  The payments hotfix is safe to ship to production now.

focus suite-is-green
  kind observation
  The full unit and integration suite passed on the release branch.

focus canary-errored
  kind observation
  The 5% canary threw a spike of HTTP 500s on checkout within ten minutes.

link suite-is-green supports hotfix-is-safe
link canary-errored opposes hotfix-is-safe

oncall holds hotfix-is-safe
  confidence 0.9 assumed
  note Shipping — the suite is green and the release window closes at 17:00.

Read it through the mirror:

  • Argument status. canary-errored has no attackers → in. It opposes hotfix-is-safe, so hotfix-is-safeout (defeated by its own recorded counter-evidence, even though a passing suite supports it).
  • Conflict. The on-call holds the now-out claim at 0.9 (≥ 0.66) → a confidence-vs-status error.
  • Provenance. That 0.9 is assumed — the mirror shows not just how sure the engineer is, but on what footing.

The document is structurally clean. The mirror surfaces the contradiction the form can’t — and leaves the call to you. That’s ThoughtML in one screen.

FAQ

Is ThoughtML a programming language?

It’s a language for representing reasoning, not for computing. You don’t run a ThoughtML document to produce a result; you write down a structured argument and the tooling reads it back — typed, dated, checkable. The opt-in compute layer evaluates the numbers you authored, but that’s a reading of your reasoning, not a program it executes.

How is it different from a mind map or a bullet list?

A bullet list flattens structure; a mind map captures connection but not meaning. ThoughtML keeps both: every link has a typed relation and a direction, every belief a holder and a confidence, evidence can be defeated by other evidence, and beliefs are dated. Because the structure is explicit and typed, a machine can read it a second way — which a mind map can’t offer.

What does “a mirror, not an oracle” mean?

The engine produces a second, mechanical reading of your structure and tells you where it disagrees with what you wrote — but it never overrules you or decides for you. It surfaces the conflict; you resolve it. See The Mirror.

Why is everything opt-in and off by default?

Two reasons. A document with no derivations serializes identically every time, which keeps output stable and diffable in CI. And it enforces the discipline that computed values never overwrite authored ones — each derived field lives beside what you wrote, never on top of it.

No — that’s the canonical core, and you can write it directly. Most of the time you’ll use the readable surface (analyst noticed metric-shift, team chooses postgres-option), which desugars into the core for you. They produce the same model; merge-conflict-beliefs.thml shows the equivalence.

Why did rejects, mitigates, and strongly/weakly disappear?

v0.1.0 was a deliberate subtraction. rejects and mitigates as relations only duplicated opposes; the strength adverbs each smuggled in a magic number the author never chose. They were removed to keep one honest way to say each thing. (rejects still exists as a posture.) See the project CHANGELOG.

Is the compute layer gone, then?

No — quantities, formulas, expected value, and sensitivity all still ship, behind opt-in flags (--formulas, --decisions, --sensitivity, or --compute for all). v0.1.0 reframed them as an opt-in second reading, not as the language’s headline. See The compute layer.

Can an AI agent write ThoughtML?

That’s a primary design goal. The grammar is small and regular, so an LLM can emit it reliably, and the mirror lets a human or CI audit the result. See ThoughtML for AI agents.

Is the syntax stable?

It’s v0.1.0 — real and usable, but the surface may still move (hence 0.x). Breaking changes will be recorded in the CHANGELOG.

Where’s the formal specification?

The single source of truth is the reference parser in crates/thoughtml. This documentation is derived from it: if the two ever disagree, the parser wins (and that’s a documentation bug worth reporting). There is no separate formal grammar document — this book is the specification, kept honest against the parser.

How do I report a bug or a documentation error?

Open an issue at github.com/Fatin-Ishraq/ThoughtML/issues. If this book and the parser disagree, that’s a documentation bug worth filing.