TANGEUL — Rules Written in Markdown, Audited by Humans Image: AI generated

toulmin computes contracts. Rules are Go functions, exceptions are declared as a defeat graph, and an h-Categoriser turns the graph into a verdict. The problem is that the person who actually needs to audit the contract usually can’t read Go.

A tax accountant auditing a lending policy. A small-business owner checking an access-control rule. A bank clerk reviewing a transfer’s compensation logic. None of them can look at func(ctx Context, specs Specs) (bool, any) and say “yes, this is correct.” No matter how sound the engine’s math is, if the reader can’t read the code, the audit is theater.

AI writes it, a human reads it

TANGL (Toulmin Amgoud Nute Graph Language) closes that gap. You write markdown sentences, and those sentences are the executable code.

## tangl:Cases
- in case of `can access`
  - `user` is required
  - `authenticate` is a general rule
  - `block ip` is a counter rule using `policy`.`IsIPBlocked` with `blocklist`
  - don't `authenticate` when `block ip`
  - do `policy`.`Allow` when `authenticate`

These five lines are the whole toulmin graph — Rule, Counter, Attacks, and the execution edge. There is no separate compile step the author has to think about: the tangl toolchain parses this markdown straight into *ast.Document, and validate/effects/gen run on it unchanged. The author is an AI, the verifier is a person — that asymmetry is the whole point. The AI only has to follow one strict canonical grammar; the human only has to read a sentence in their own language.

One semantics, many surfaces

TANGL’s semantics are defined once, in the English grammar: Evaluate/Run duality, once (tick idempotency), undo (compensation), cascade timing, deterministic execution order. Any locale edition — Korean, or whichever comes next — does not redefine any of this. It only swaps the keyword and word-order table sitting on top of the same AST. One Korean sentence maps 1:1 to one English sentence, and both parse to the identical AST node.

That’s not a translation layer bolted on for convenience — it’s load-bearing. TANGL’s actual readers in Korea are non-developer auditors: 소상공인, 세무사. English keywords like don't X when Y or is a general rule don’t let a non-developer confirm “yes, this reads correctly.” If the reader is Korean, the surface has to be Korean for the audit to actually work — and the reverse holds symmetrically for English readers.

Building the Korean edition surfaced something not obvious from the English grammar alone: Korean’s SOV order isn’t a translation burden, it’s a better fit for conditional rules. `Y`면 `X` 실행한다 (roughly, “when Y, do X”) reads more naturally than do X when Y in Korean — condition-first, predicate-last is simply how the language is structured to begin with. The Korean particle policy pushes this further: particles (은/는/이/가/을/를 …) are optional tokens the lexer discards, but the AI-written canonical form always includes them anyway — for the human reader, not the parser. Since the AI is the only author, the parser never has to accept free word order or spoken variation; it only has to parse one canonical form. That asymmetry is what makes a non-trivial natural-language surface tractable at all.

Seven sections

Subject → See → Definitions → Rules → Cases → Provides → Internal
SectionRoleRequired
Subjectdeclares what the document is aboutrequired
Seereferences external package symbolsoptional
Definitionsterms and structsoptional
Rulesinline condition predicatesoptional
Casesjudgment + execution (the graph itself)required
Providesendpointsoptional
Internaltick/event-driven internal workoptional

Each construct is only valid in its own section — the same scoping discipline any locale edition inherits unchanged. Statements read as complete sentences that name their own kind: is a general rule, don't ... when, do ... once when, undo ... when, run ... when.

Example: a coffee robot

Here’s how TANGL’s execution semantics — the Evaluate/Run duality, once, cascading — show up as ordinary markdown sentences.

## tangl:Cases
- in case of `can place cup`
  - `order received` is a general rule using `sensor`.`isOrdered`
  - `no cup` is a counter rule using `sensor`.`noCupAvailable`
  - don't `order received` when `no cup`
  - do `arm`.`placeCup` once when `order received`
  - run `can pour water` when `order received`

- in case of `can pour water`
  - `cup placed` is a general rule using `sensor`.`cupIsPlaced`
  - do `arm`.`pourWater` once when `cup placed`
  - run `can brew espresso` when `cup placed`

## tangl:Provides
- provides `make coffee`
  - run `can place cup`

## tangl:Internal
- every 1s until `can serve coffee`
  - run `can place cup`

An auditor reads this as: “Once the cup can be placed — if the order was received, place the cup once and move on to pouring water. If there’s no cup, the order is void. To make coffee, start from placing the cup, and retry every second until it can be served.”

Without once, the arm would place a new cup on every tick for as long as the order stays active — the word is the entire reason a slow sensor update doesn’t turn into a physically unsafe re-run. An auditor confirms re-run safety from this one sentence, without ever reading a Go guard clause.

Example: compensation — the world stays whole even on failure

undo is not the same as a rejection. A rejection is a normal branch of the graph; undo is compensation for an execution that died partway through.

- do `bank`.`withdraw` once when `balance sufficient`
- undo `bank`.`refund` when `balance sufficient`
- run `can deposit` when `balance sufficient`

“If the balance is sufficient, withdraw once; if anything downstream fails, roll back via refund; and continue to deposit.” On failure, the ordering is deterministic:

1. withdraw succeeds        → refund is armed on the compensation stack
2. deposit errors           → the Run aborts immediately
3. stack unwinds LIFO       → refund fires, the money comes back
4. the original error is reported — the transfer failed, and the world looks like it never happened

If refund itself fails, the remaining compensations stop and the whole transaction escalates to a human review gate (REVIEW) — the system decides for itself that a half-executed transfer is not something to paper over automatically.

Where text stops being the source of truth — TANGEUL

Everything above is TANGL: markdown text parsed and executed directly. But text being the execution input is a convenience, not a necessity. The moment a language has two surfaces — English and Korean — the question “which one is canonical?” appears. Right now the answer is “English, by convention.” That’s a decision, not a structural guarantee.

TANGEUL removes the question, and it already ships as the pkg/tangeul binary single source of truth for the tangl toolchain. The canonical execution artifact is a binary graph stream (.tangeul) that check/ast/effects/gen all run on directly; the English and Korean markdown files are surface codecs that absorb into, and render back out of, that same graph.

 EN .md ──[EN codec]──┐                        ┌──[mode 1]── verbatim byte-identical round trip
                      ├──> .tangeul (SSOT) ────┤
 KO .md ──[KO codec]──┘                        └──[mode 2]── normalized EN/KO rendering
  • Mode 1 (verbatim): replays the authored bytes exactly, so decode output is byte-identical to the original .mdchecked by the CLI round-trip test.
  • Mode 2 (regenerated): decode --locale en|ko re-renders each statement through the target locale’s codec, replaying prose spans verbatim. A document authored in Korean can be rendered for an English auditor, and vice versa.

A document’s authoring surface is fixed to a single locale (mixing English and Korean sections in one file is rejected), but the .tangeul stream itself doesn’t know which locale wrote it. It can be rendered into either language on demand precisely because the canonical artifact is a graph, not a paragraph.

All bit-level knowledge is quarantined in two packages — word (16-bit words, frame headers) and codebook (append-only code values) — so the layers above them (packets, stream, the document container) stay stable no matter how the encoding evolves:

pkg/tangeul/
├── word/       // 16-bit words, frame headers — bit-level knowledge stops here
├── codebook/   // NodeType/PropCode/Control codes, append-only
├── packet/     // Node/Edge/Control/Text packets
├── stream/     // Doc ↔ .tangeul serialization
└── doc/        // Doc container + VerifyTiling + the surface Codec interface

VerifyTiling is the integrity gate: the recorded blocks must tile the source text exactly, and it runs on every load and every encoder output. Parity tests assert that the AST loaded from a .tangeul stream equals the AST the parser produces from the original .md (up to line numbers). Retargeting to binary isn’t about changing what the code does — the graph, and therefore the generated output, is identical; it’s about structurally removing the possibility of drift between multiple copies of the same text.

Auditable contracts

toulmin’s conclusion was that a verdict is computed by a formula, not decided by a person. TANGL adds one more line to that: the computation is readable by someone who never wrote the formula.

  • Warrant (a rule) = one sentence, `n` is a ... rule
  • Rebuttal = one sentence, don't `X` when `Y`
  • Ground (evidence) = one call into a referenced package function
  • Verdict = the h-Categoriser formula

And TANGEUL is the work of guaranteeing, at the byte level, that these sentences point to the same graph regardless of which language they’re written in. The person who writes the rule and the person who audits it no longer have to share a language.

MIT License. github.com/park-jun-woo/toulminpkg/tangl, pkg/tangeul, cmd/tangl.

Sources

Changelog

  • 2026-07-03: First edition