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 conditions-are-fine
kind claim
Conditions on site are fine to pour the foundation slab this afternoon.
focus truck-is-booked
kind observation
The ready-mix truck is booked for 14:00 and the full crew is on site.
focus overnight-freeze
kind observation
The site thermometer logged minus four degrees from 02:00 to 06:00, and
tonight's forecast repeats it.
link truck-is-booked supports conditions-are-fine
link overnight-freeze opposes conditions-are-fine
site-engineer holds conditions-are-fine
confidence 0.88 assumed
note Pouring today. The truck is booked and the crew moves on Thursday.
This document is clean — no errors, no warnings. But the mirror flags a
conflict: the engineer holds conditions-are-fine at 0.88, while their own
recorded evidence (overnight-freeze opposes conditions-are-fine) defeats that
claim. They logged the reading that says concrete will freeze before it sets,
and poured anyway. ThoughtML surfaces that disagreement; it doesn’t decide for
you. (And the 0.88 declares its basis — assumed, not measured — provenance
you can see.)
This is examples/pour-the-slab.thml,
shipped with the toolchain. Every snippet in this book is a real document you
can run.
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, author multi-file projects, stream long-running work, and share a standalone viewer.
- Appendix — glossary, example gallery, FAQ.
A note on stability
This documentation describes v0.5.0, the current 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/grant-panel.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/pour-the-slab.thml
Export a standalone view
Turn any document into a single self-contained interactive HTML file — no server, opens in any browser:
thoughtml examples/grant-panel.thml --html -o datastore-decision.html
It carries the interactive graph with the model baked in (no wasm). See The standalone viewer.
Stream a changing project
To observe work while the source continues changing, host a private read-only view from the editing computer:
thoughtml stream .thoughtml/project.thml
The default link is loopback-only and stops when the command or computer stops.
It watches transitive imports and uploads nothing. See
Live streaming before using --lan.
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:
observation flat-loaf
The Sunday sourdough came out of the oven flat.
hypothesis dead-starter
The starter had lost its lift after three weeks unfed in the fridge.
link dead-starter causes flat-loaf
baker holds dead-starter
confidence 0.8 estimated
Three things are happening:
- Two foci. A focus is a thing you’re reasoning about. The header word
states what sort it is — an
observationyou made, ahypothesisabout why — and the indented line under each is its prose body. - A link.
dead-starter causes flat-loafrecords a typed, directed relationship between them. - A stance.
baker holds dead-startersays who believes what, theconfidence 0.8says how strongly, andestimatedsays on what footing.
The bundled why-the-loaf-failed.thml is where this
goes next: three explanations proposed, one of them settled by evidence.
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": "flat-loaf", "kind": "observation",
"body": "The Sunday sourdough came out of the oven flat." },
{ "type": "focus", "id": "dead-starter", "kind": "hypothesis",
"body": "The starter had lost its lift after three weeks unfed in the fridge." },
{ "type": "link", "id": "dead-starter-causes-flat-loaf",
"from": "dead-starter", "relation": "causes", "to": "flat-loaf" },
{ "type": "stance", "id": "baker-holds-dead-starter",
"agent": "baker", "posture": "holds", "target": "dead-starter",
"confidence": { "kind": "number", "value": 0.8 }, "basis": "estimated" }
]
}
Notice the parser gave every record an id (flat-loaf,
dead-starter-causes-flat-loaf, …). 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 dead-starter causes flat-laof
Run it again and the parser warns on stderr:
warning: link.to of `dead-starter-causes-flat-laof` is an unresolved reference `flat-laof`
warning: focus `flat-loaf` is not connected to anything
Two warnings, not one: the typo also left the real flat-loaf with nothing
pointing at it. That second line is the parser noticing an orphan — a node that
earns no place in the argument.
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: a site engineer is deciding whether to pour a concrete foundation
slab this afternoon, with a frost forecast for tonight. By the end you’ll have
written the bundled example
pour-the-slab.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 (
conditions-are-fine). 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 conditions-are-fine
Conditions on site are fine to pour the slab this afternoon.
The id (conditions-are-fine) 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 conditions-are-fine
kind claim
Conditions on site are fine to pour the slab this afternoon.
focus truck-is-booked
kind observation
The ready-mix truck is booked for 14:00 and the crew is on site.
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 truck-is-booked
The ready-mix truck is booked for 14:00 and the crew is on site.
noticed creates the focus truck-is-booked 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 conditions-are-fine
kind claim
Conditions on site are fine to pour the slab this afternoon.
focus truck-is-booked
kind observation
The ready-mix truck is booked for 14:00 and the crew is on site.
focus overnight-freeze
kind observation
The thermometer logged minus four degrees overnight.
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 truck-is-booked supports conditions-are-fine
link overnight-freeze opposes conditions-are-fine
Read left to right: truck-is-booked supports conditions-are-fine; overnight-freeze
opposes conditions-are-fine. 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 delivery-hypothesis: late-delivery causes set-delayed
The proposed mechanism: evicted hot keys force slow cold reads.
link thermometer-misread undercuts delivery-hypothesis
The indented sentence under a link is its body — prose explaining why the
relation holds. Here thermometer-misread undercuts delivery-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 conditions-are-fine
kind claim
Conditions on site are fine to pour the slab this afternoon.
focus truck-is-booked
kind observation
The ready-mix truck is booked for 14:00 and the crew is on site.
focus overnight-freeze
kind observation
The thermometer logged minus four degrees overnight.
link truck-is-booked supports conditions-are-fine
link overnight-freeze opposes conditions-are-fine
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>:
site-engineer holds conditions-are-fine
confidence 0.9
note Shipping — the load test passed.
This says the agent site-engineer holds the focus conditions-are-fine, 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 conditions-are-fine
kind claim
Conditions on site are fine to pour the slab this afternoon.
focus truck-is-booked
kind observation
The ready-mix truck is booked for 14:00 and the crew is on site.
focus overnight-freeze
kind observation
The thermometer logged minus four degrees overnight.
link truck-is-booked supports conditions-are-fine
link overnight-freeze opposes conditions-are-fine
site-engineer holds conditions-are-fine
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 pour document doesn’t need a question — the engineer has already decided. 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)
site-engineer holds conditions-are-fine
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:
site-engineer holds conditions-are-fine
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:
site-engineer holds conditions-are-fine
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 dating-the-codex.thml is built entirely
around this — a manuscript dating superseded once the radiocarbon result lands, where
--as-of makes the conflict disappear because at that date it did not exist yet.
Our pour 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. This is
pour-the-slab.thml exactly as it ships — the bodies
are fuller than the ones we typed along the way, and nothing else has changed:
focus conditions-are-fine
kind claim
Conditions on site are fine to pour the foundation slab this afternoon.
focus truck-is-booked
kind observation
The ready-mix truck is booked for 14:00 and the full crew is on site.
source site-diary
observed-at 2026-03-11
focus overnight-freeze
kind observation
The site thermometer logged minus four degrees from 02:00 to 06:00, and tonight's
forecast repeats it. Fresh concrete that freezes before it sets never recovers.
source site-diary
observed-at 2026-03-11
link truck-is-booked supports conditions-are-fine
link overnight-freeze opposes conditions-are-fine
site-engineer holds conditions-are-fine
confidence 0.88 assumed
note Pouring today. The truck is booked and the crew moves to another job Thursday.
It is clean
Run it normally:
thoughtml pour-the-slab.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 pour-the-slab.thml
The canonical JSON now carries an audit section:
"audit": {
"conflicts": [
{
"kind": "confidence-vs-status",
"severity": "error",
"subjects": ["site-engineer-holds-conditions-are-fine", "conditions-are-fine"],
"message": "`site-engineer` asserts confidence 0.88 in `conditions-are-fine`, but your own structure defeats it (argument status: out)"
}
]
}
Read what happened. The engineer holds conditions-are-fine at 0.88. But the document
also records overnight-freeze opposes conditions-are-fine. When the mirror computes the
argument status, conditions-are-fine 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.88 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.88) 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 conditions-are-fine.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.5.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
grant-panel.thml writes both in one document — a stance
longhand with an alias next to readable <agent> <posture> lines — 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
kind claim
Some prose.
focus b
kind observation
More prose.
focus a is a header at column 0. The two lines indented under it are block
lines belonging to a — one field, one line of prose. focus b returns to
column 0, which closes a and opens b.
(Those annotations are prose rather than trailing # comments for a reason —
see just below.)
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.
conditions-are-fine ✓
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 | conditions-are-fine | 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": "conditions-are-fine", "kind": "claim",
"body": "Conditions on site are fine to pour the slab this afternoon." }
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": "overnight-freeze-opposes-conditions-are-fine",
"from": "overnight-freeze", "relation": "opposes", "to": "conditions-are-fine" }
Optional: weight, probability, basis, body, fields, plus derived
superseded_by, derived_confidence, leverage, argument_status.
Stance
{ "type": "stance", "id": "site-engineer-holds-conditions-are-fine",
"agent": "site-engineer", "posture": "holds", "target": "conditions-are-fine",
"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": "kabir-hat-survey", "includes": ["cases-clustered", "…"] }
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.dead-starter-causes-flat-loaf) - 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 conditions-are-fine
kind claim
Conditions on site are fine to pour the foundation slab this afternoon.
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 conditions-are-fine
Conditions on site are fine to pour the foundation slab this afternoon.
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
Pour the foundation slab this week.
focus truck-is-booked
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 truck-is-booked supports conditions-are-fine
link delivery-hypothesis: late-delivery causes set-delayed
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:
focus disk-headroom
kind observation
Free space on the primary volume.
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 kabir-hat-survey
source district-health-survey
observed-at 2026-05-03
observation cases-clustered
Nineteen of the twenty-two cases live within three hundred metres of the well.
field-nurse noticed cases-clustered
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 grant-panel.thml nests a goal, a question, a
decision and three options inside one scope, so the panel reads as a single unit.
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 burndown-review
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
likelihood high
focus weak-monitoring
kind risk
No alerting on berth occupancy.
link weak-monitoring aggravates port-strike
stance ops flags port-strike
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
bridge-inspection.thml is a complete example. The
profile itself is recorded as a Profile object (document metadata, not a
referenceable node).
A profile posture needs the
stancelonghand. Above it isstance ops flags port-strike, notops flags port-strike. The readable<agent> <posture> …form is resolved against the twelve core postures before any profile is consulted, so a dialect’s own posture is only reachable throughstance.
Imports — multiple documents
A document can pull in another and reference its records under a namespace:
import inspection-standards as standard
link deck-is-serviceable depends-on standard.load-rating-on-record
import <name> as <ns>makes the records of document<name>available under the prefix<ns>..- A reference like
standard.load-rating-on-recordresolves to theload-rating-on-recordrecord in the importedinspection-standardsdocument. That pair ships with the toolchain: seebridge-inspection.thmland the library it imports. - 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: the project workspace resolves imports against the sibling
.thmlfiles currently open. It can connect to a real directory (where the browser supports directory access), open a portable selection of files, or load the bundled six-file Snake project.
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 bridge-inspection.thml (which imports
inspection-standards.thml) must be run as a project to resolve — open it in the
playground, or run it through the CLI from the examples directory.
Provenance in the unified graph
Project compilation produces a source map beside the canonical model. Every authored or desugared object records the module and source line that produced it. The playground uses that map to label imported nodes, jump from a Reasoning Card to the exact editor location, and qualify diagnostics; live and standalone viewers keep the location as read-only provenance.
An imported conclusion used by another module is a compact gateway in the unified graph. If it has supporting ancestry inside its own module, a subtle stacked-node marker appears. Select it and choose Expand reasoning to reveal that same-module ancestry in place; Collapse reasoning folds it back into the project overview. Expansion does not reparse or alter the document—it is a view over the already merged canonical graph.
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 |
site-engineer holds conditions-are-fine
confidence 0.88 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, TML502 circular justification |
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.circular justification: a → b → a(TML502) — asupportsloop. The language refuses circular causation by default (TML303); this is circular justification, and it matters for the same reason plus one more: claims inside the loop derive confidence above 0.5 from no evidence outside it. Advisory rather than an error, because mutual support is not always a mistake — two readings of one body of evidence can genuinely reinforce each other. Either give one of them outside evidence, or merge them if they are one belief said twice.
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 Reasoning Card’s expanded details beside your authored
confidence — two bars, never merged. On the bundled
evacuate-or-shelter.thml, town-overrun lands ≈0.802 on
one weighted support, while fire-turns-away comes out ≈0.401 — a 0.5-weight support
outweighed by a 0.7-weight attack.
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 bundled pour-the-slab.thml:
link truck-is-booked supports conditions-are-fine
link overnight-freeze opposes conditions-are-fine
overnight-freeze has no attackers → in. It opposes conditions-are-fine,
and that attacker is in, so conditions-are-fine → 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 engineer’s authored 0.88.
Where it appears
argument_status is set on every focus and link that takes part in the attack
graph:
{ "type": "focus", "id": "conditions-are-fine", "argument_status": "out" }
{ "type": "focus", "id": "overnight-freeze", "argument_status": "in" }
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": ["site-engineer-holds-conditions-are-fine", "conditions-are-fine"],
"message": "`site-engineer` asserts confidence 0.88 in `conditions-are-fine`, but your own structure defeats it (argument status: out)" }
]
}
Each conflict has a kind, a severity (error or warning), the subjects
it concerns, and a human-readable message. That output is
examples/pour-the-slab.thml
run with --audit, verbatim.
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.
What it cannot see
The mirror checks a document against itself, never against the world. A
falsification pass (crates/thoughtml/tests/falsification.rs) pins the boundary,
and it is worth knowing precisely, because everything inside it is trustworthy
only if you know where it ends.
These produce an empty conflict report and confident-looking numbers:
| Error | What happens |
|---|---|
| A false premise | earth-is-flat supports maps-are-wrong is valid reasoning from nonsense. The conclusion derives ≈0.73. |
| One fact entered twice | Two ids for one observation both support a claim. Independence is assumed, so it compounds to ≈0.96. |
| Evidence never written down | Omit the two failed trials and the graph is coherent at ≈0.86. |
There is a fourth that is visible in the graph, and so is caught — but only if
you ask. a supports b and b supports a derive above 0.5 for both, from no
outside evidence at all. thoughtml check <file> --lint reports that as
TML502, off by default like every modelling lint.
The first three are not bugs waiting to be fixed. They are the limit of what any self-consistency check can reach, and they are exactly why the mirror reports rather than decides. The part it cannot do is yours: write down the evidence that cuts against you.
The bundled pour-the-slab.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 orchard-water.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 evacuate-now option-of valley-response
link evacuate-now leads-to everyone-out
probability 0.6 estimated
link evacuate-now leads-to road-cut-off
probability 0.4 estimated
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. Seeevacuate-or-shelter.thml, which those edges come from, andorchard-water.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
evacuate-or-shelter.thml ranks evidence by it. It is
computed by re-deriving the graph with each edge ablated in turn — single-edge
sensitivity, precomputed for every edge at once — so the CLI never perturbs the
document; it reports it as authored.
Use cases
ThoughtML earns its keep wherever the reasoning matters as much as the conclusion — where someone later needs to check why, not just what. Here are the concrete situations it’s built for.
1. Decision records you can lint
An architecture decision record (ADR) is usually prose: “we chose Postgres because…”. Written as ThoughtML, the ADR is a graph — options considered, the evidence for and against each, the question that blocks sign-off, the choice and its justification. Now it’s checkable: did you actually reject the alternatives for stated reasons? Is the decision still blocked on an open question?
See grant-panel.thml.
2. AI agent reasoning a human (or CI) can audit
This is the headline use case. An AI agent makes a call — what to ship, which fix
to apply, how to triage. Instead of a paragraph of justification, it emits a
ThoughtML document: the claim, the evidence it weighed, its confidence, and the
basis of each number. A human, another agent, or a CI step then runs the
mirror over it and catches the tells: high confidence in a
defeated claim, numbers marked assumed where they should be measured,
dangling assumptions.
The agent does the reasoning; ThoughtML makes it legible enough to check. See ThoughtML for AI agents.
3. Design and code review of an argument
Reviewing a proposal often means reviewing an argument, and arguments hide
their flaws in prose. A ThoughtML version surfaces them: the
confidence-vs-status conflict catches “you hold this
at 0.9, but your own listed risk defeats it” — the exact thing a reviewer is
trying to notice and often misses.
The canonical demo is pour-the-slab.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 pour-the-slab.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 peer-review.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.
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
(
conditions-are-fine), 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 dating-the-codex.thml for a belief revised as new
evidence landed — with the earlier one kept and replayable — and
pour-the-slab.thml for the audit in action.
Long-running repository work
Keep substantial agent reasoning beside the repository as a project rather than forcing it into one growing file:
.thoughtml/
├── project.thml # imports the modules and connects their conclusions
├── architecture.thml
├── implementation.thml
├── verification.thml
└── release.thml
The entry document compiles the recursive import closure into one graph while the source map preserves every object’s file and line. A human can remain at the project level, expand an imported conclusion in place to inspect why the module reached it, then jump to source in the playground when an edit is needed.
Start a local observer before a long task:
thoughtml stream .thoughtml/project.thml --json --events
Parse the one startup object from stdout, give its viewer_url to the local
administrator, and continue editing ordinary .thml files. Runtime lifecycle
events stay on stderr. The stream debounces write bursts, recompiles every
transitive import, keeps the last valid graph during incomplete edits, and needs
no publishing API or account. See Live streaming for the network
boundary and session controls.
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, a
belief-level diff, the embedded language guide, and local live streaming.
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
thoughtml guide [TOPIC] # learn the bundled language, offline
thoughtml stream <FILE> # host a live view from this computer
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)
What formatting normalizes. Comments are preserved: a comment belongs to whatever it sits above, so an unindented block is re-emitted directly above its record and a trailing one stays at the end of the file. A blank line left between a file-header comment and the first record is closed up, since the comment introduces that record. Comments written inside a block are kept but move to the top of it — the formatter reorders a block’s contents (body first, then fields), so there is no stable position to return them to.
fmtre-parses its own output and refuses to write if the canonical model changed, so formatting can never alter meaning.
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)
thoughtml guide — the language inside the binary
The complete AI-ready language guide is embedded from the same llms.txt that
ships with the packages and site:
thoughtml guide # one-screen model and topic index
thoughtml guide relations # one section by name, alias, or number
thoughtml guide --full # the complete guide, suitable for an agent prompt
It requires no network and stays versioned with the installed binary.
thoughtml stream — computer-hosted live view
Watch an entry document and its transitive sibling imports, compile the complete project locally after each settled edit, and host a read-only live viewer:
thoughtml stream investigation.thml
The safe default binds to 127.0.0.1 and is viewable only on the editing
computer. Share it with another device on the same network explicitly:
thoughtml stream investigation.thml --lan
--lan does not create an internet tunnel or upload anything. The link works
only while the command and host computer are running, and the operating system’s
firewall may ask whether to allow the listener. Anyone who can reach the computer
and obtains the link can read the compiled model, so use trusted networks.
The watcher coalesces rapid writes, recompiles the full project locally, and pushes versioned canonical snapshots over Server-Sent Events. When the newest edit is invalid, the browser receives its diagnostics while retaining the last valid graph. Each diagnostic identifies its source file. Existing offline commands never open a network connection.
The viewer uses the shared Reasoning Card, retains file/line provenance for imported objects, supports inline expansion of a module conclusion’s hidden ancestry, and can download the current valid revision as standalone HTML.
Manage detached sessions locally:
thoughtml stream status # list live and stale records
thoughtml stream status --json # machine-readable session inventory
thoughtml stream stop <SESSION> # id or unambiguous prefix
thoughtml stream stop # stop every recorded live session
Useful options:
| Flag | Effect |
|---|---|
--lan | Listen on all interfaces and advertise a detected LAN address. |
--host <IP> | Bind an explicit interface instead of the loopback default. |
--advertise-host <HOST> | Override the hostname printed in the viewer URL. |
--port <N> | Pick a port; 0 (default) selects a free one. |
--json | Print one startup JSON object for an agent or script; ongoing logs use stderr. |
--events | With --json, emit runtime lifecycle events as JSON Lines on stderr. |
--debounce-ms <N> | Set the quiet period after edits (default: 400 ms). |
--strict-provenance | Include strict number-provenance diagnostics. |
See Live streaming for the protocol and agent workflow.
Examples
# Canonical JSON + diagnostics
thoughtml examples/pour-the-slab.thml
# The full second reading, compact, to a file
thoughtml --compute --compact -o out.json examples/evacuate-or-shelter.thml
# Just the conflict report
thoughtml --audit examples/pour-the-slab.thml
# Replay: what did the document believe as of a date?
thoughtml --as-of 2026-01-13 examples/dating-the-codex.thml
# A standalone interactive viewer — one self-contained HTML file, opens anywhere
thoughtml --html -o decision-record.html examples/grant-panel.thml
# A live view hosted by this computer until Ctrl+C
thoughtml stream examples/evacuate-or-shelter.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/evacuate-or-shelter.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
- Project editor (left) — a file list, tabs, and code editor with ThoughtML syntax highlighting. The entry file and all of its sibling imports compile into the same graph; diagnostics appear on the correct file 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
- Reasoning Card — click any node for a compact floating explanation: prose, high-signal status/confidence, connection count, and exact source. Explore details reveals the complete technical record without permanently consuming canvas space.
- Example tray — load any bundled example to explore it.
Multi-file projects
The playground treats one directory of sibling .thml files as a project. A
repository can keep that directory at .thoughtml/ so reasoning stays separate
from product source:
snake-game/
├── src/
├── tests/
└── .thoughtml/
├── project.thml
├── product.thml
├── architecture.thml
├── gameplay.thml
├── quality.thml
└── release.thml
The file marked entry owns the import declarations; imported nodes are referenced by their namespace, exactly as in the native CLI:
import quality as quality
link quality.core-rules-are-stable supports ship-v1
- Open folder connects the editor to a real directory when the browser supports directory access. Open files remains the portable fallback.
- New creates a sibling file and inserts its import into the entry file.
- Rename updates import module names across the project while preserving aliases, so qualified references do not need to change.
- Delete reports dependent files before staging a removal. A project entry must be changed before it can be deleted.
- Entry makes the active tab the project root.
- Save writes the active file; Save all writes every dirty file and finishes staged renames/deletions. Without writable handles, these actions download a file or complete project ZIP instead.
- Dirty marks, open tabs, browser recovery state, cursor positions, per-file undo history, and scroll positions are kept separately for each file.
Ctrl+Ssaves one file,Ctrl+Shift+Ssaves the project,Ctrl+Popens a file switcher, andCtrl+Shift+Fsearches every project file.- The sidebar reports missing imports, cycles, and files outside the entry’s transitive import closure.
- Clicking a file-qualified diagnostic switches tabs and moves to its line.
- Clicking an authored graph node exposes its defining file and line in the Reasoning Card; that source chip navigates back to the editor. These locations come from the Rust compiler, including links generated from readable syntax.
- Imported conclusions with hidden supporting ancestry carry a subtle stacked marker and branch glyph. Select one and use Expand reasoning in the card to reveal only its same-module reasons inside the unified graph. The action then becomes Collapse reasoning, returning to the compact project overview.
Compilation runs in a Web Worker after a short debounce. The editor therefore stays responsive and keeps showing the last valid graph while a newer project version is compiling. The compiler still validates the complete transitive import closure; the worker and stale-result cancellation avoid blocking or displaying an older compilation after a newer edit.
When a connected directory changes outside the browser, Refresh compares it with the last saved baseline. Clean files reload directly. If both the browser and an external tool or AI agent changed the same file, the playground asks whether to use disk, keep the editor version, or download both. It never silently overwrites both sides of a conflict.
Press Snake demo for a real six-file repository example covering product
goals, architecture, gameplay, quality, and release reasoning. The same project
ships under examples/snake-project/ and is native-CLI valid:
thoughtml --strict --strict-provenance examples/snake-project/project.thml
The browser workspace does not upload selected files. Directory permission is granted by the user and scoped by the browser; a remembered handle is reused only while that permission remains granted. The CLI remains the direct filesystem workflow for agents working inside a repository:
thoughtml --strict --strict-provenance .thoughtml/project.thml
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 pour-the-slab.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
dating-the-codex.thml or
well-water.thml. The same projection is on the
CLI as --as-of.
The playground keeps the visual lens set deliberately small—Type and Argument—
while the Reasoning Card surfaces derived confidence and other computed facts on
demand. The CLI exposes the full readings through --derived, --status,
--sensitivity, --decisions, and --compute.
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 grant-panel.thml --html -o decision-record.html
Open the result and you get the same time-driven view the playground shows
under “Viewer”: semantic node silhouettes, clean relation routing, reasoning laid
out along time (earlier left, later right), pan / zoom, an as-of bar with
replay, and light / dark—all running on a model baked into the file. Click a
node for the same floating Reasoning Card used by the playground and live stream:
readable prose first, source provenance and high-signal facts next, complete
technical detail on demand. 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.
For a compiled multi-file project, imported conclusions can be expanded directly
inside the unified graph. The stacked-node/branch marker means supporting module
reasoning is folded behind the conclusion; Expand reasoning reveals it and
Collapse reasoning restores the overview. The source map stays in the HTML,
so the card can identify quality.thml:18 even though the source text and parser
are not embedded.
What’s in the file
The exported artifact is the canonical JSON, its project source map, and 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 |
| Observe a changing local project | thoughtml stream |
Every surface renders from the same parser and the same time-driven renderer—one canonical model, many faithful projections.
Live streaming
thoughtml stream lets a person observe an agent’s evolving reasoning without
giving the viewer access to the working directory or editor:
thoughtml stream investigation.thml
The command prints a temporary link and keeps running. It watches the entry file
and every transitively imported sibling .thml file. After an edit settles, the
same Rust reference compiler that powers the CLI recompiles the complete project
with the full mirror stack and sends the browser a new canonical snapshot.
Local by design
This first streaming transport is hosted by the editing computer. There is no ThoughtML account, cloud relay, upload, domain, tunnel, or third-party runtime.
- By default the listener is
127.0.0.1, so only that computer can open it. --lanmakes the viewer available to reachable devices on the same network.- The viewer and control paths use independent operating-system-random tokens.
- The link stops working when the command exits or the computer goes offline.
- The source files remain local. The browser receives the compiled canonical model, diagnostics, watched file names, and semantic revision history.
The viewer token protects against casual discovery, but possession of the link is the authorization model. On a LAN, use a trusted network and assume anyone who obtains the link can read the model. The viewer remains strictly read-only; the separate stop endpoint is accepted only from the host computer.
What a viewer sees
The ordinary reasoning timeline remains the main surface. A live status badge opens a session panel containing:
- current revision, connection state, and connected viewer count;
- the files in the active import closure;
- file-qualified diagnostics from the newest edit;
- semantic activity entries with added, changed, and removed object counts; and
- whether the graph is showing the newest model or the last valid one.
Selecting a graph node shows the exact project source that produced it, such as
quality.thml:18, in the same floating Reasoning Card used everywhere else.
Imported conclusions with folded module ancestry carry a quiet stacked-node
marker; Expand reasoning reveals those supporting nodes inside the current
graph and Collapse reasoning folds them away again. The Snapshot button
downloads the graph currently being shown as a self-contained HTML file. If the
newest edit is invalid, the download uses—and is named for—the last valid
revision, matching the graph on screen.
If an agent saves a half-written or invalid document, the current graph does not disappear. The stream publishes the errors and keeps the most recent successful canonical model until the source becomes valid again.
Agent workflow
An autonomous agent can start the stream without prompts and parse one stable JSON line from stdout:
thoughtml stream investigation.thml --json
{"status":"streaming","viewer_url":"http://127.0.0.1:49172/s/...","bind_address":"127.0.0.1:49172","entry_file":".../investigation.thml","transport":"local-http+sse","session_id":"a1b2c3d4e5f6"}
Ongoing operational messages go to stderr, leaving stdout machine-readable. Add
--events with --json to receive JSON Lines events for started, compiled,
invalid, unchanged, and stopped. The agent continues editing normal
ThoughtML files; it does not call a publishing API.
Sessions can be discovered and stopped without remembering a process id:
thoughtml stream status
thoughtml stream status --json
thoughtml stream stop a1b2c3
thoughtml stream stop # stop every recorded live session
stop uses an independent local control token and asks the server to shut down
cleanly. Ctrl+C remains available for an attached terminal.
Update and performance model
The watcher polls file contents and uses a project signature to ignore unchanged states. A change starts a 400 ms debounce window. More writes reset that window, so a burst becomes one compilation rather than a queue of obsolete work.
Version 1 intentionally sends complete canonical snapshots. The server caps project input at 16 MiB, request headers at 64 KiB, and concurrent connections and viewers to bounded totals. This keeps browser and compiler state impossible to desynchronise and gives future hosted relays a small, versioned protocol boundary. The browser preserves its camera and selected node when it applies a newer snapshot, and briefly highlights added or modified nodes. Semantic diffs are computed between valid revisions for activity history.
The HTTP surface is deliberately small:
| Route | Purpose |
|---|---|
/s/<viewer-token> | self-contained live viewer |
/api/<viewer-token>/snapshot | current schema-versioned state |
/api/<viewer-token>/events | Server-Sent Events carrying newer snapshots |
/health | process health check — loopback only |
The root route deliberately does not redirect to the private viewer link.
What the transport does and does not protect
The viewer link is an unguessable capability: 192 bits of OS randomness, and the token is compared without an early exit. Two things it is not:
- It is not encrypted. The stream is plain HTTP and the token sits in the
URL path, so it also lands in browser history, proxy logs, and
Referer. Anyone who can see the traffic can read the document and reuse the link. Share a--lansession only on a network you would trust with the document itself. - It is not an identity. Whoever has the link has full read access, and there is no way to revoke one recipient short of restarting the session.
The server refuses requests whose Host is a name it does not serve — only
localhost, IP literals, and whatever --advertise-host declared. That blocks
DNS rebinding, where a web page points its own hostname at 127.0.0.1 so the
browser treats your stream as same-origin and lets the page read it.
Exposing the stream beyond your own network needs --expose-public. The check is
on where the bind reaches, not on what you typed — --lan and --host 0.0.0.0
both mean “every interface I have”, which is your LAN on a laptop and the open
internet on a cloud host, so the machine’s own outbound address decides. On a home
or office network nothing changes; run the same command on a VPS and it stops and
tells you, naming the address you would have been reachable at.
/health answers over loopback only; it reports revision count and connected
viewers, which is not something a whole network needs to know.
This transport can later sit behind a hosted relay without moving parsing into the service: the CLI remains the compiler and the viewer remains a consumer of canonical snapshots.
Upgrading
To v0.4.1 — the security release
For most documents, nothing changes. The language is the same, the CLI is the
same, the output is the same. v0.4.1 is a drop-in upgrade and you should take it:
it fixes issues in the parser, the viewer, the thoughtml stream server, and the
release pipeline, and for most of them there is no configuration workaround.
Three things can bite. Only the first is likely.
1. A document that reported “clean” may now report an error
Two places used to accept any text and now require a well-formed value: a
quantity’s unit, and a question’s expects / status. That is deliberate —
those strings are rendered straight back to whoever opens the document, and
unvalidated they were the route to executing script in a reader’s browser.
| Now an error | Write instead |
|---|---|
quantity 5 $, quantity 4 € | 5 USD, 4 EUR — the ISO code, not the symbol |
quantity 3 °C | 3 degC |
quantity 2 items(net) | 2 items-net |
expects Cause | expects cause |
status In Progress | status in-progress |
The rule is a character class, not a list of blessed units: letters, digits, and
% / - _ ., starting with a letter. Invented units keep working — users,
widgets, story-points, req/s, USD/hour, m², µs all parse exactly as
before. It is symbols, spaces, brackets and capitals that no longer do.
expects and status follow the same rule ids always have: lowercase
kebab-case.
To find out before you upgrade anything important:
thoughtml check path/to/doc.thml
Anything that now errors is in the table above, and each fix is a one-word edit.
2. Sharing a stream by machine name stops working
The server now refuses requests whose Host is a name it does not serve. That is
the defence against DNS rebinding, where a web page points its own hostname at
your machine so the browser treats your stream as same-origin and lets the page
read it. A hostname is what makes that attack possible; an IP address does not.
So this still works:
http://192.168.1.20:53318/s/<token> ← the link the CLI prints
http://localhost:53318/s/<token>
and this now returns 421 Misdirected Request:
http://my-laptop:53318/s/<token>
If you share the link by machine name, declare it:
thoughtml stream doc.thml --lan --advertise-host my-laptop
Two smaller changes in the same area:
/healthanswers over loopback only. A remote health probe will need to move to the machine itself.- Exposing the stream beyond your own network needs
--expose-public. The check is on where the bind actually reaches, not on what you typed:--lanand--host 0.0.0.0both mean “every interface I have”, so on a laptop nothing changes, while on a cloud host the same command now stops and tells you.
3. Very large documents lose the leverage numbers
Past about 2 000 objects the sensitivity pass is skipped, with a warning saying so. It re-derives the whole confidence computation once per evidence edge, which made a large document usable as a denial of service against anything that compiles input it did not write. Every other analysis still runs and the document is still valid. Hand-written documents are nowhere near this; generated ones may be.
Also worth knowing
thoughtml fmt cannot preserve comments — it rebuilds the document from the
parsed model, and the model does not carry # lines. It used to delete them
silently on -w. It now refuses to rewrite a file that has comments and explains
why, rather than destroying them. A document with no comments formats as before.
Full detail
The CHANGELOG lists everything, and the repository’s security advisories carry the technical write-up.
Glossary
Agent — the actor a stance attributes a belief to
(site-engineer, 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 ten example documents in
examples/.
Every one parses strict-clean (zero errors, zero warnings) under default
options and is fmt-clean — tests enforce both. A curated spine appears in
the playground’s example tray; every file can also be opened directly in the
project workspace.
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). Ten files is deliberate: few enough to read all of them in a sitting, and
between them they carry a worked instance of every kind, relation, posture, field,
basis, lifecycle state, confidence form, and both mirror conflicts — pinned by a
test, so the claim cannot quietly go stale. The domains are spread on purpose, from
a home kitchen to a wildfire: nothing about the language is specific to software.
The repository also includes a six-file project under examples/snake-project/.
Its project.thml entry imports product, architecture, gameplay, quality, and
release modules into one graph. Use it to try the multi-file playground,
file/line provenance, live streaming, and inline expansion of imported
conclusions:
thoughtml stream examples/snake-project/project.thml
Start here
| Example | What it teaches |
|---|---|
pour-the-slab.thml | A clean document the mirror still flags — the confidence-vs-status conflict. The flagship demo, and the smallest complete piece of reasoning. |
why-the-loaf-failed.thml | An everyday question done properly: candidate-for proposes, answers resolves, and a ruled-out guess is parked abandoned rather than deleted. |
Arguments and evidence
| Example | What it teaches |
|---|---|
peer-review.thml | undercuts (attack an inference — target the link) versus opposes (attack a claim), plus the second mirror conflict: two referees write one id two ways and both are kept. |
well-water.thml | A field investigation as an acyclic causes/enables/prevents graph; a scope whose provenance its members inherit; a weighted evidence bundle. |
Time and change
| Example | What it teaches |
|---|---|
dating-the-codex.thml | A belief revised as evidence lands; the earlier one superseded, not erased. --as-of replay makes a conflict disappear, because at that date it did not exist yet. |
Decisions
| Example | What it teaches |
|---|---|
grant-panel.thml | One award, four people: criteria collected with part-of (not evidence), options weighed, one withdrawn and kept abandoned, the award blocked until a review answers. |
The compute layer
| Example | What it teaches |
|---|---|
orchard-water.thml | A budget that computes itself: = formula lines with real dimensional analysis (L/min × min = L, ÷ trees = L/tree, and a subtraction that must match dimensions). |
evacuate-or-shelter.thml | The whole compute layer in one decision: a probability borrowed from derived confidence, expected-value ranking, and a what-if that flips it. |
Dialects and modularity (advanced)
| Example | What it teaches |
|---|---|
inspection-standards.thml | A standalone importable library — a part-of collection of requirements, and the building block the inspection imports. |
bridge-inspection.thml | import … as with namespaced cross-document references, run as a project — plus a profile declaring a structural-engineering dialect (defect/remedy kinds, aggravates/mitigates relations, a severity field, a certifies posture). |
A walkthrough: pour-the-slab.thml
The most instructive example is the smallest interesting one:
focus conditions-are-fine
kind claim
Conditions on site are fine to pour the foundation slab this afternoon.
focus truck-is-booked
kind observation
The ready-mix truck is booked for 14:00 and the full crew is on site.
focus overnight-freeze
kind observation
The site thermometer logged minus four degrees from 02:00 to 06:00, and tonight's
forecast repeats it. Fresh concrete that freezes before it sets never recovers.
link truck-is-booked supports conditions-are-fine
link overnight-freeze opposes conditions-are-fine
site-engineer holds conditions-are-fine
confidence 0.88 assumed
note Pouring today. The truck is booked and the crew moves to another job Thursday.
Read it through the mirror:
- Argument status.
overnight-freezehas no attackers →in. Itopposesconditions-are-fine, soconditions-are-fine→out(defeated by its own recorded counter-evidence, even though a booked truck supports it). - Conflict. The site engineer holds the now-
outclaim at0.88(≥ 0.66) → aconfidence-vs-statuserror. - Provenance. That
0.88isassumed— 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 (field-nurse noticed cases-clustered,
team chooses postgres-option), which desugars into the core for you. They
produce the same model; grant-panel.thml uses both side by side to
show 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.5.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.