TANGEUL — 마크다운으로 짜고 사람이 감사하는 규칙 Image: AI generated

toulmin은 계약을 계산한다. 규칙을 Go 함수로 쓰고, 예외를 그래프로 선언하고, h-Categoriser가 판정을 수식으로 뽑는다. 문제는 — 그 계약을 실제로 감사해야 하는 사람이 Go를 읽지 못한다는 것이다.

신용 대출 규칙을 감사하는 세무사, 접근 제어 정책을 확인하는 소상공인, 이체 보상 로직을 검토하는 은행 실무자. 이들은 func(ctx Context, specs Specs) (bool, any)를 보고 “이대로 맞네"라고 판단할 수 없다. 규칙 엔진이 아무리 정교해도, 읽는 사람이 코드를 못 읽으면 감사는 형식뿐이다.

AI가 쓰고, 사람이 읽는다

TANGL(Toulmin Amgoud Nute Graph Language)은 이 간극을 없앤다. 마크다운 문장을 쓰면 그게 곧 실행 코드다.

## tangl:사안
- `접근 가능` 의 경우에
  - `사용자` 는 필수다
  - `인증` 은 일반 규칙이다
  - `IP 차단``정책`.`IsIPBlocked` 쓰는 반박 규칙이다, 기준 `차단 목록`
  - `IP 차단` 이면 `인증` 무효다
  - `인증` 이면 `정책`.`Allow` 실행한다

이 다섯 줄이 toulmin 그래프 전체다 — Rule, Counter, Attacks, 실행 간선까지. 별도의 컴파일 단계 없이 tangl 툴체인이 이 마크다운을 파싱해 *ast.Document로 만들고, 거기서 validate·effects·gen이 그대로 돈다. 작성자는 AI, 검증자는 사람 — 이 비대칭이 핵심이다. AI는 정규형 문법 하나만 정확히 지키면 되고, 사람은 자연어 문장을 읽으면 된다.

의미론 하나, 표면 둘

TANGL의 의미론 정본은 영어판이다. Evaluate/Run 이중성, once(틱 멱등성), undo(보상), 캐스케이드 시간 의미론 — 전부 영어판이 정의한다. 한국어판은 이를 재서술하지 않는다. 같은 AST 위에 얹히는 키워드·어순 테이블 교체일 뿐이다. 한국어 문장 하나는 영어 문장 하나와 1:1 대응하고, 동일한 AST 노드로 파싱된다.

그런데 이건 번역이 아니다. 한국어판은 자체 표면 문법 문서(TANGEUL.ko.md)를 가진 1급 로케일이다 — 한국어 키워드·어순·조사 정책·섹션 헤더를 스스로 정의하고, 의미론만 영어판에서 가져온다. TANGL의 실제 독자는 한국 소상공인·세무사 감사자다. don't X when Y, is a general rule 같은 영어 키워드로는 비개발자가 감사할 수 없다. 읽는 사람이 한국인이면 읽는 층도 한국어여야 감사가 작동한다.

한국어판을 얹으며 알게 된 건, SOV 어순이 번역 부담이 아니라 오히려 유리하다는 점이다. 한국어는 “조건 → 결과” 어순이 자연스럽다. `Y` 면 `X` 실행한다는 영어 do X when Y보다 잘 읽힌다 — 조건이 먼저 오고 서술어로 맺는 것이 애초에 한국어의 기본 구조이기 때문이다.

조사는 읽는 사람을 위한 것이지 파서를 위한 것이 아니다

파서 입장에서 조사(은/는/이/가/을/를 등)는 잡음이다. 렉서가 무시 목록의 조사를 버리면 남는 건 백틱 이름·키워드·리터럴뿐이고, 이 열의 위치가 구문을 유일하게 결정한다.

`Y` 면 `X` 무효다  →  [이름, 이름, 무효다]  →  Y.Attacks(X)

그런데 AI 정규형은 조사를 항상 포함한다. 왜 파서가 무시하는 걸 굳이 쓰는가 — 읽는 사람이 사람이기 때문이다. 받침 유무로 갈리는 이형태(은/는, 이/가, 을/를, 와/과, 로/으로, 면/이면)는 어느 쪽을 써도 파싱 결과가 같다. 두 이형태 모두 무시 목록에 있어서다.

마지막 문자판별
한글 음절종성 있으면 받침형(은/이/을/과/으로/이면), 없으면 무받침형(는/가/를/와/로/면)
숫자낭독 받침 기준 (예: 650 을)
ASCII·기호받침 없음 취급 (예: `refund` 로 되돌린다)

AI가 유일한 작성자이므로 파서는 어순·조사·구어 변형을 전부 받아줄 필요가 없다 — 정규형 하나만 정확히 파싱하면 된다. 이 비대칭이 한국어 파싱의 난제(형태소 분석, 어순 자유도)를 통째로 없앤다. 감사자용 렌더링(모드 2)에서 그래프를 한국어로 재생성할 때는 반대로 디코더가 이 표를 따라 자연스러운 이형태를 자동으로 골라준다.

일곱 섹션

주제 → 참조 → 정의 → 규칙 → 사안 → 업무 → 내부
섹션역할필수
주제문서가 다루는 대상 선언필수
참조외부 패키지 심볼 참조선택
정의용어·구조체 정의선택
규칙인라인 조건 규칙선택
사안판정 + 실행 (그래프 본체)필수
업무엔드포인트선택
내부틱/이벤트 기반 내부 업무선택

각 섹션에서 쓸 수 있는 구문이 정해져 있다 — 영어판과 동일한 스코프 규칙이다. 문미 키워드(무효다/실행한다/진행한다/되돌린다/판정한다/규칙이다 등)가 statement 종류를 결정한다. 한국어는 SOV이므로 문미 판별이 오히려 균일하다 — 영어판의 문두 키워드(do/don't/run/undo)와 거울 대칭인 셈이다.

예시: 커피 로봇

TANGL의 실행 의미론(Evaluate/Run 이중성, once, 캐스케이드)이 어떻게 마크다운 문장으로 드러나는지 전체 예제로 보자.

## tangl:사안
- `컵 놓기 가능` 의 경우에
  - `주문 접수``센서`.`isOrdered` 쓰는 일반 규칙이다
  - `컵 없음``센서`.`noCupAvailable` 쓰는 반박 규칙이다
  - `컵 없음` 이면 `주문 접수` 무효다
  - `주문 접수``팔`.`placeCup` 한 번만 실행한다
  - `주문 접수``물 붓기 가능` 진행한다

- `물 붓기 가능` 의 경우에
  - `컵 놓임``센서`.`cupIsPlaced` 쓰는 일반 규칙이다
  - `컵 놓임` 이면 `팔`.`pourWater` 한 번만 실행한다
  - `컵 놓임` 이면 `추출 가능` 진행한다

## tangl:업무
- `커피 만들기` 를 한다면
  - `컵 놓기 가능` 진행한다

## tangl:내부
- 매 1초마다, `서빙 가능` 까지
  - `컵 놓기 가능` 진행한다

감사자가 읽으면 이렇게 읽힌다 — “컵 놓기가 가능한 경우에, 주문이 접수되면 컵을 한 번만 놓고 물 붓기로 이어간다. 컵이 없으면 주문 접수는 무효다. 커피 만들기를 한다면 컵 놓기부터 진행하고, 서빙 가능해질 때까지 매 1초마다 다시 진행한다.”

한 번만(once)이 없으면 매 틱마다 팔이 다시 컵을 놓는다 — 센서 갱신이 늦어 다음 틱이 이어받아도 물리적으로 안전한 이유가 이 한 단어에 있다. 감사자는 Go 코드의 가드 조건을 읽지 않고도 이 문장 하나로 재실행 안전성을 확인한다.

예시: 보상 — 실패해도 세상은 원래대로

undo는 판정 거부(반박)와 다르다. 반박은 정상 경로의 한 갈래고, undo는 실행이 도중에 죽었을 때의 보상이다.

- `잔액 충분` 이면 `은행`.`withdraw` 한 번만 실행한다
- `잔액 충분` 이면 `은행`.`refund` 로 되돌린다
- `잔액 충분` 이면 `입금 가능` 진행한다

“잔액이 충분하면 출금을 한 번만 실행하고, 도중에 실패하면 환불로 되돌리며, 입금으로 이어간다.” 실패 시 순서는 결정론적이다.

1. withdraw 실행 성공     → refund 보상 무장
2. deposit 실행 에러      → Run 즉시 중단
3. 보상 스택 역순 실행    → refund 발화, 돈이 돌아온다
4. 원 에러 보고           → 이체는 실패했고, 세상은 이체 전과 같다

refund마저 실패하면 나머지 보상을 중단하고 전체가 사람 게이트(REVIEW)로 올라간다 — 절반 실행된 이체는 자동으로 덮을 수 있는 상태가 아니라는 판단을 시스템이 스스로 내린다.

텍스트가 정본이 아니게 되는 순간 — TANGEUL

여기까지는 TANGL이다. 텍스트 마크다운을 직접 파싱해서 실행한다. 그런데 텍스트가 실행 입력이라는 건 편의이지 필연이 아니다. 언어가 영어·한국어 두 표면을 가지는 순간, “어느 쪽이 정본인가"라는 질문이 생긴다. 지금은 영어판이 정본이라고 정하고 있을 뿐이다 — 구조적으로 강제되는 게 아니다.

TANGEUL은 이 질문 자체를 지운다. 그리고 이건 계획이 아니라 이미 pkg/tangeul로 출시된 tangl 툴체인의 바이너리 정본(SSOT)이다. 실행의 정본은 텍스트가 아니라 바이너리 그래프 스트림(.tangeul)이고 check/ast/effects/gen이 전부 그 위에서 돈다. 영어·한국어 마크다운은 그 그래프로 흡수되고 다시 재생되는 표면 코덱 두 개다.

 EN .md ──[EN 코덱]──┐                        ┌──[모드1]── 저작 원문 verbatim (바이트 항등)
                     ├──> .tangeul (SSOT) ────┤
 KO .md ──[KO 코덱]──┘                        └──[모드2]── EN/KO 정규형 렌더링
  • 모드 1(verbatim): 저작 원문 바이트를 그대로 재생한다. 디코드 출력이 원본 .md와 바이트 항등임을 CLI 왕복 테스트가 검증한다.
  • 모드 2(재생성): decode --locale en|ko가 각 statement를 대상 로케일 코덱으로 다시 렌더링하고 산문 스팬은 verbatim 재생한다. 한국어로 쓴 문서를 영어 감사자용으로, 혹은 그 반대로 뽑아낼 수 있다.

저작 표면은 문서 하나당 하나로 고정되지만(영어·한국어 혼용 문서는 거부), .tangeul 스트림 자체는 로케일을 모른다. 어느 쪽으로든 재렌더링할 수 있는 건 정본이 텍스트가 아니라 그래프이기 때문이다.

비트 수준 지식은 word(16비트 워드·프레임 헤더)와 codebook(append-only 코드값) 두 패키지에만 격리된다 — 그 위 계층(패킷·스트림·Doc 컨테이너)은 인코딩이 어떻게 바뀌든 흔들리지 않는다:

pkg/tangeul/
├── word/       // 16비트 워드·프레임 헤더 — 비트 지식은 여기까지만
├── codebook/   // NodeType/PropCode/Control 코드값, append-only
├── packet/     // Node/Edge/Control/Text 패킷
├── stream/     // Doc ↔ .tangeul 직렬화
└── doc/        // Doc 컨테이너 + VerifyTiling + 표면 Codec 인터페이스

VerifyTiling이 무결성 게이트다 — 기록된 블록이 원문을 빈틈·겹침 없이 타일링해야 하고, 모든 load와 인코더 출력마다 돈다. 패리티 테스트는 .tangeul 스트림에서 로드한 AST가 파서가 원본 .md에서 만든 AST와 (줄 번호를 빼면) 같음을 단언한다. 바이너리로의 재타기팅은 코드가 하는 일을 바꾸는 게 아니라 — 그래프도, 따라서 생성 출력도 동일하다 — 텍스트 여러 벌 사이의 드리프트 가능성을 구조적으로 없애는 작업이다.

감사 가능한 계약

toulmin의 결론은 “판정은 사람이 아니라 수식이 계산한다"였다. TANGL은 여기에 한 줄을 더한다 — 계산한 결과를, 계산을 짠 적 없는 사람도 읽고 확인할 수 있다.

  • Warrant(규칙) = `n` 이다 문장 한 줄
  • Rebuttal(반박) = `X` 이면 무효다 문장 한 줄
  • Ground(증거) = 참조 패키지의 함수 호출 하나
  • Verdict(판정) = h-Categoriser 수식

그리고 TANGEUL은 이 문장들이 어느 언어로 쓰이든 같은 그래프를 가리킨다는 것을 바이트 단위로 보증하는 작업이다. 규칙을 짜는 사람과 감사하는 사람이 같은 언어를 쓸 필요가 없어진다.

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

출처

변경이력

  • 2026-07-03: 초판