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
.thmlfile, 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 wasmmust use the rustup toolchain. Ifwasm-packpicks up a standalone MSVC Rust instead, the build fails. Make sure~/.cargo/bin(where rustup installs) is early on yourPATH.
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:
- 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.
- A link.
deploy-change causes metric-shiftrecords a typed, directed relationship between them. - A stance.
team holds metric-shiftsays who believes what, and theconfidence 0.8says 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:
- Foci — name the things you’re reasoning about.
- Links — connect them with typed, directed relations.
- Stances — record who believes what, and how.
- Questions — mark what’s still open and what it blocks.
- Numbers — confidence, weight, and where numbers come from.
- Time and revision — date beliefs and let them change.
- 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:
| Kind | What it is |
|---|---|
observation | Something seen or measured |
claim | An assertion put forward as true |
hypothesis | A proposed explanation, not yet settled |
option | A choice on the table |
decision | A choice to be made (or made) |
outcome | A result an option can lead to |
goal | Something you want |
assumption | Something taken as given |
memory | A recollection carried forward |
action | Something 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:
| Relation | Meaning |
|---|---|
supports | The source is evidence for the target |
opposes | The source is evidence against the target (a rebuttal) |
undercuts | The source attacks an inference, not the claim itself |
Structural / causal — how things relate in the world or the plan:
| Relation | Meaning |
|---|---|
causes | The source brings about the target |
enables | The source makes the target possible |
prevents | The source stops the target |
depends-on | The target is needed for the source |
blocks | The source holds the target up (see until in chapter 4) |
answers | The source resolves a question |
revises | The source replaces the target (see chapter 6) |
Decision — for expected-value analysis (see the compute layer):
| Relation | Meaning |
|---|---|
leads-to | An option leads to an outcome (carries a probability) |
option-of | An option belongs to a decision |
opposesvs.undercuts.opposesrebuts a node (“that claim is wrong”).undercutsattacks 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 separaterejectsrelation — a hard rejection is justopposes, 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.
What can a link connect?
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:
| Posture | Meaning |
|---|---|
noticed | Registered an observation |
considers | Put an option on the table |
suspects | Proposed a tentative link (a hypothesis) |
infers | Drew a conclusion from sources |
asks | Raised a question |
holds | Commits to / believes |
chooses | Selected an option |
rejects | Ruled something out |
revises | Replaced a previous stance |
remembers | Carried a fact forward |
doubts | Holds with low credence |
accepts | Agrees 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:
| Posture | Creates a focus of kind |
|---|---|
noticed | observation |
considers | option |
holds / chooses | decision |
remembers | memory |
infers | claim |
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
-
suspectsproposes a link and takes a stance on it:analyst suspects ai-automation causes job-displacement as displacement-hypothesis confidence 0.45..0.70This creates the two foci, a
causeslink aliaseddisplacement-hypothesis, and a stance in which the analyst suspects that link. (Note the confidence is a range — see chapter 5.) -
infersdraws a conclusion from one or more sources, wiring asupportslink 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.
expectssays what kind of answer would settle it (number,option,forecast, …) — free-form, for the reader.statusis typicallyopenor 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 — on a link
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.
Probability — on a leads-to link
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 core —
focus,link,stance,question,scoperecords 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 — lines, indentation, comments, value types.
- Records and the canonical model — the seven object types and their JSON shape.
- Foci and kinds, Links and relations, Stances and postures, Questions — the primitives in detail.
- Fields — every known field and where it attaches.
- Scopes and Profiles, imports, namespaces — structure and modularity.
- Numbers, units, provenance — the value model.
- Diagnostics — every error and warning, and what triggers it.
Lexical structure
A ThoughtML document is a sequence of lines. Each line is classified before anything else happens.
Line kinds
| Line | Rule |
|---|---|
| Blank | Empty or whitespace-only. Ignored. |
| Comment | First non-space character is #. Ignored. |
| Header | Zero indentation, non-blank. Starts a new record. |
| Block | Indented (≥ 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:
| Form | Example | Notes |
|---|---|---|
| Number | 0.72, 1, -3 | Plain base-10. No exponents, inf, or nan. |
| Range | 0.25..0.70 | low..high, inclusive. |
| Unknown | ? | The explicit “not stated” marker. |
| Time | 2026-06-09, 2026-06-14T14:05+00:00 | Loose ISO-8601. |
| Ref | cache-is-safe | A bare identifier — a reference to another record. |
| Symbol | open, high | Same lexical form as a ref; intent depends on the field. |
| URI | uri:https://example.org/x | A uri:-prefixed value. |
| List | a, b, c | Comma-separated identifiers/symbols. |
| Text | "quoted string", or any multi-word value | Free 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 assumedis two tokens, so it would classify as Text. ThoughtML peels a trailing basis keyword (measured/estimated/assumed) off first, then re-classifies the remaining0.9as a Number. That’s howconfidence 0.9 assumedparses 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
| Header | Form | Becomes |
|---|---|---|
focus | focus <id> | a Focus |
link | link [alias:] <from> <relation> <to> | a Link |
stance | stance [alias:] <agent> <posture> <target> | a Stance |
question | question <id> | a Question |
scope | scope <id> | a Scope |
profile | profile <name> | a Profile |
import | import <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).
Link
{ "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
linkandstance— 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
-2suffix is appended.
- a link →
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
| Part | Source | Notes |
|---|---|---|
id | the header (focus <id>) | lowercase kebab-case |
kind | a kind field, or inferred | see below |
body | the indented prose | first mention wins on merge |
quantity | a quantity field | a typed measure |
formula | a = <expr> line | opt-in compute |
status | a status field | belief lifecycle, see below |
includes | nested child records | thought-tree members, see below |
fields | any other field lines | free-form, order-preserved |
Kinds
The ten kinds:
| Kind | Meaning |
|---|---|
observation | Something seen or measured |
claim | An assertion put forward as true |
hypothesis | A proposed explanation, not yet settled |
option | A choice on the table |
decision | A choice to be made or recorded |
outcome | A result an option can lead to |
goal | A desired end |
assumption | Something taken as given |
memory | A recollection carried forward |
action | A 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:
- Explicit
kindfield — authoritative and sticky. Once set explicitly, nothing overrides it silently. - Posture inference — a focus-creating posture implies a kind:
noticed→observation,considers→option,holds/chooses→decision,remembers→memory,infers→claim. This is soft: a later posture can refine it. - Decision-graph inference — the endpoints of
leads-to/option-ofedges get provisional kinds: the source of either is anoption; the target ofleads-tois anoutcome; the target ofoption-ofis adecision. 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:
| Status | Meaning |
|---|---|
open | live — still in play (the default if unstated) |
settled | resolved |
superseded | replaced by a later belief (cf. the revises relation) |
abandoned | a 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, andformulaare first-wins (a later mention doesn’t overwrite a value already stated).fieldsaccumulate.kindfollows 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
| Part | Source |
|---|---|
id | the alias:, or generated <from>-<relation>-<to> |
from, relation, to | the header |
weight | a weight field (0..1 evidential strength) |
probability | a probability field (0..1, leads-to only) |
basis | a provenance keyword on the number |
body | the 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)
| Relation | Polarity | Meaning |
|---|---|---|
supports | + | source is evidence for target |
opposes | − | source rebuts target (a node attack) |
undercuts | − | source 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
| Relation | Meaning |
|---|---|
causes | source brings about target |
enables | source makes target possible |
prevents | source stops target |
depends-on | target is required for source |
blocks | source holds target up (the desugaring of until) |
answers | source resolves a question |
revises | source 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
| Relation | Meaning |
|---|---|
leads-to | an option leads to an outcome; carries probability |
option-of | an option belongs to a decision |
These power decision expected value.
Membership and candidacy (non-evidential)
| Relation | Meaning |
|---|---|
part-of | source is one item in the target (a collection member) |
candidate-for | source 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
opposesrebuts a node: “that claim is false.”undercutsattacks 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
weightis evidential strength (any evidence/structural link).probabilityis outcome likelihood (leads-toonly).
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
| Posture | Creates a focus? | Inferred kind | Notes |
|---|---|---|---|
noticed | yes | observation | registered an observation |
considers | yes | option | put an option forward |
holds | yes | decision | commits to / believes |
chooses | yes | decision | selects an option |
remembers | yes | memory | carries a fact forward |
infers | yes | claim | concludes from sources (special form) |
suspects | — | — | proposes a link (special form) |
asks | no | — | raises a question |
doubts | no | — | low credence |
accepts | no | — | agrees |
rejects | no | — | rules out |
revises | no | — | supersedes 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
noteon 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
| Part | Source | Meaning |
|---|---|---|
id | the header (question <id>) | |
body | indented prose | the question itself |
expects | expects <symbol> | what kind of answer settles it (option, number, forecast, …) |
status | status <symbol> | typically open, or settled |
asks_about | about <id-list> | the foci the question concerns |
fields | other field lines | free-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
| Field | Attaches to | Value | Purpose |
|---|---|---|---|
kind | focus | symbol | the focus’s kind |
quantity | focus | <number> <unit> | a typed measure |
confidence | stance | number / range / ? | credence in the target |
weight | link | 0..1 | evidential strength |
probability | link (leads-to) | 0..1 | outcome likelihood |
note | stance | text | rationale (always on the stance) |
because | stance | ref | the supporting reason |
answers | stance | ref | the question this settles |
until | stance | <ref> [status] | desugars to a blocks link |
about | question | id-list | what the question concerns |
expects | question | symbol | the kind of answer wanted |
status | question / link | symbol | open, answered, … |
source | any | text / uri: | provenance of the record |
observed-at | any | time | when observed |
asserted-at | any | time | when asserted |
valid-during | any | start..end | the 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
Scopes are not link endpoints
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-budgetresolves to thecapacity-budgetrecord in the importedshared-defsdocument. - 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
importlines, the parser reads the sibling files<name>.thmlfrom 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
formulastring 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
| Number | Where | Range / form |
|---|---|---|
confidence | stance | a scalar, a range lo..hi, or ? |
weight | link | 0..1 (clamped, with a warning, if outside) |
probability | link (leads-to) | 0..1 (clamped, with a warning, if outside) |
quantity | focus | <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 GBand500 MBcan be compared, and formulas can compute over them). The canonical JSON keeps both the authoredvalue/unitand thenormalized/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:
| Basis | Meaning |
|---|---|
measured | observed / counted directly |
estimated | reasoned approximation |
assumed | taken 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/weaklyadverbs 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:
| Range | Family | Examples |
|---|---|---|
TML1xx | vocabulary | TML101 unknown kind, TML102 unknown relation, TML103 unknown posture, TML104 unknown field |
TML2xx | references | TML201 unresolved reference, TML202 illegal link endpoint |
TML3xx | graph coherence | TML301 orphan, TML302 contradictory stances, TML303 cycle, TML304 revision before its target, TML305 decision-graph |
TML4xx | numbers | TML401 missing basis, TML402 out-of-range clamp |
TML5xx | lints (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 supprts → supports.
Codes are part of the tool’s contract and are not renumbered once assigned.
Errors
| Message (abbreviated) | Cause |
|---|---|
tab indentation is invalid; v0 requires spaces | a tab in leading whitespace |
indented block line before any record header | a block line with no open record |
<kw> header expects exactly one identifier | malformed focus/scope/question/profile header |
link header expects [alias:] from relation to | wrong link arity |
stance header expects [alias:] agent posture target | wrong 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 identifier | wrong arity for a simple action |
suspects expects from relation to [as alias] | malformed suspects |
infers expects target from id-list / requires at least one source | malformed 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..high | reversed range |
weight/probability must be a number in 0..1 | non-numeric weight/probability |
link.from/to … targets a <kind>; links may only connect foci, questions, or links | a link pointing at a stance or scope |
Warnings
Indentation & structure
block lines should be indented by two or more spacesonly a scope may contain nested objects; desugaring them at the top level
Ids & kinds
duplicate id/id is reused across recordsfocus … was declared as kind X but redeclared as Y; keeping Xduplicate <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; clampingprobability on a <rel> link is ignored/a weight on a leads-to link is ignoredkind requires a value/until requires a referencequestion 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 amongcauses/depends-onedges.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 inflatederived_confidence; if these are list items, usepart-ofinstead. Off by default so strict-clean documents are unaffected.
Decision graph
leads-to edge … points outcome … at itselfoption … has no leads-to outcomes, but its sibling options do— it’d be silently missing from the EV ranking.
Temporal
… revises … but is asserted earliervalid-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:
| Flag | Reading |
|---|---|
--derived | Derived confidence — propagate evidence into a per-claim strength |
--status | Argument status — grounded in/out/undecided |
--audit | Conflict report — where confidence disagrees with status |
--sensitivity | per-edge leverage |
--formulas | evaluate = expr foci into computed_quantity |
--decisions | decision expected value |
--acts | emit Act provenance objects for readable actions |
--compute | all 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:
- 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.
- Computed ≠ authored. Every derived value lives in its own field, beside
(never replacing) what you wrote.
derived_confidencesits next to your authoredconfidence;computed_quantitynext to yourquantity. The mirror adds a reading; it never edits yours.
The four readings, in one line each
- Derived confidence — how strong is this claim, given its evidence?
- Argument status — does 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
+1forsupports,−1foropposes/undercuts. - weight is the link’s
weight, or0.5if 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.
| Label | Meaning |
|---|---|
in | accepted — every attacker is defeated |
out | defeated — at least one attacker is accepted |
undecided | neither — 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:
- Start every contested node
undecided. - Repeatedly:
- label a node
inif all of its attackers are alreadyout(a node with no attackers isinimmediately); - label a node
outif any attacker isin.
- label a node
- 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-safe → out. (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:
| Condition | Severity | Reading |
|---|---|---|
target is out and confidence ≥ 0.66 | error | high credence in a claim the structure defeats |
target is in and confidence ≤ 0.34 | warning | low 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 likemin/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/instancebyinstance, 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, not8e9 B) and strictly separate from any authoredquantity. 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-toedges to outcome foci. - Each
leads-toedge carries aprobability; if it doesn’t, the outcome’s derived confidence is used as a fallback. - Each outcome carries a payoff — its
computed_quantityif a formula produced one, else its authoredquantity.
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
bestoption and nomargin. 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. Seeship-or-hold.thmlandship-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?
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:
- 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. - CI runs
thoughtml --audit(and maybe--strict-provenance). - The conflict report catches that the agent held a claim at 0.9 that its own recorded counter-evidence defeats.
- 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 relation → did 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 assumedis honest in a way a bare0.9is not. Run CI with--strict-provenanceto enforce it. - Record the counter-evidence. The mirror can only catch a
confidence-vs-statusconflict 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
| Flag | Effect |
|---|---|
--ast | Emit the surface AST instead of the canonical model. |
--compact | Single-line JSON instead of pretty-printed. |
--html | Emit 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. |
--strict | Treat 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.
| Flag | Effect |
|---|---|
--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.
| Flag | Reading |
|---|---|
--derived | derived_confidence — propagate evidence (§10.3) |
--status | argument_status — grounded in/out/undecided |
--audit | the conflict report (confidence-vs-status) |
--sensitivity | per-edge leverage |
--formulas | evaluate = expr foci into computed_quantity |
--decisions | decision expected value over leads-to / option-of |
--acts | emit Act provenance objects for readable actions |
--strict-provenance | warn on numbers with no measured/estimated/assumed basis |
--compute | turn 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]
| Flag | Effect |
|---|---|
--json | Emit 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. |
--lint | Also 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. |
--strict | Exit 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
--htmlviewer 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.
- 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
- 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:
- derived confidence next to authored confidence,
- argument status on contested nodes,
- the conflict report when your structure disagrees with what you said.
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, experiment | the playground |
| Check a document in CI or a script | thoughtml doc.thml → JSON + exit code |
| Hand someone an interactive, time-driven view | thoughtml 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
| Example | What it teaches |
|---|---|
ship-the-hotfix.thml | A clean document the mirror still flags — the confidence-vs-status conflict. The flagship demo. |
triage-742.thml | The canonical minimal document: noticed → question → suspects → hold-until. The smallest complete piece of reasoning. |
weekend-plan.thml | The plainest shape: a goal, two options, and the pick — proof it reads naturally for low-stakes reasoning. |
Arguments and decisions
| Example | What it teaches |
|---|---|
pr-feedback.thml | undercuts (attack an inference) versus opposes (attack a claim) — the two distinct ways to push back. |
hiring-panel.thml | Several interviewers on one call: shared evidence, different confidence, because, per-stance notes. |
choose-datastore.thml | An ADR as a graph: options weighed, one rejected and kept abandoned, a blocking benchmark (until), the question settled. |
differential-dx.thml | A clinician’s differential: competing hypotheses, candidate-for proposals versus the answers that resolves. |
moderation-decision.thml | An AI moderation call emitted for a human to audit — the confidence and evidence made inspectable. |
Time and memory
| Example | What it teaches |
|---|---|
launch-readiness.thml | A belief revised twice as evidence lands; earlier versions superseded, not erased. Replay with --as-of. |
assistant-memory.thml | An assistant’s evolving memory: noticed, infers-from sources, remembers, revises, an unknown (?). |
merge-conflict-beliefs.thml | The second mirror conflict: two agents define one focus two ways — definition-divergence, kept losslessly. |
Causes, collections, and everyday reasoning
| Example | What it teaches |
|---|---|
prod-outage.thml | A postmortem as an acyclic causes/enables/prevents graph, with nested scopes and -by attribution. |
roadmap-priorities.thml | part-of for grouping (not evidence), and a question that contains its candidate options as a thought-tree. |
bad-oyster.thml | Everyday causal reasoning: suspects a cause and asks the question that would settle it. |
replication-study.thml | Weighing a scientific claim: a critique that undercuts a failed replication, with measured/estimated bases. |
The compute layer
| Example | What it teaches |
|---|---|
cloud-bill.thml | A cost model that computes itself: = formulas over line items with full unit-checking (USD/hour × hour = USD). |
ship-or-hold.thml | The 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)
| Example | What it teaches |
|---|---|
threat-model.thml | A profile declaring a security dialect — custom threat/control kinds, mitigates/aggravates relations, likelihood/severity fields, a flags posture. |
control-library.thml | A minimal importable library — the building block the rollout imports. |
compliance-rollout.thml | import … 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-erroredhas no attackers →in. Itopposeshotfix-is-safe, sohotfix-is-safe→out(defeated by its own recorded counter-evidence, even though a passing suite supports it). - Conflict. The on-call holds the now-
outclaim at0.9(≥ 0.66) → aconfidence-vs-statuserror. - Provenance. That
0.9isassumed— 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.
Do I have to write focus / link / stance?
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.