# Clause-graph compiler: build template engine cho hợp đồng đa dạng không materialize variants

> Quản lý 54 biến thể hợp đồng (3 loại × 6 phân khúc × 3 tier khách) bằng cách compile clause overlay thay vì copy-paste 54 file. Pattern transfer được cho BOQ, dashboard, form builder.

**Author**: Tien Dang (Đặng Hồng Tiên), Founder of OKG and AIC, Vietnam
**Published**: 2026-05-13
**Pillar**: architecture
**Tags**: template-engine, compiler, dsl, contract-automation, claude-skills
**Canonical URL**: https://danghongtien.com/posts/2026-05-13-clause-graph-compiler-pattern/
**AI assistance disclosed**: yes (structure draft)

---

## TL;DR
Pattern challenge: quản lý 54 biến thể hợp đồng (3 loại × 6 phân khúc × 3 tier khách) cho team SMB. Materialize = 54 file maintain. Compile = 1 base + overlay rules + render multi-format. Bonus: parameterize mọi giá trị tài chính + trigger UX qua Spreadsheet checkbox cho team non-tech. Transfer được sang BOQ, dashboard, form builder.

## Key claims
- Materialize variants không scale khi N tổ hợp > 10 — một typo phải fix ở N file, drift là không thể tránh
- Clause overlay với operations enum rõ ràng (replace / insert_after / insert_before / append / skip) tốt hơn monkey-patch text
- Tier defaults cascade trước per-request override là pattern 'convention over configuration' áp dụng cho contract
- Multi-format render (PDF, DOCX, Google Doc) share một intermediate representation (compiled clause graph) — single source of truth
- Trigger UX qua Spreadsheet checkbox là 'lowest common denominator' cho team non-tech, tốt hơn ép họ học CLI

import { Image } from 'astro:assets';
import apartmentTKCover from '../../assets/posts/2026-05-13-clause-graph-compiler-pattern/apartment-tk-cover.png';
import apartmentTKBody from '../../assets/posts/2026-05-13-clause-graph-compiler-pattern/apartment-tk-body.png';
import villaCover from '../../assets/posts/2026-05-13-clause-graph-compiler-pattern/villa-trongoi-cover.png';
import villaBilingualBody from '../../assets/posts/2026-05-13-clause-graph-compiler-pattern/villa-trongoi-bilingual-body.png';

## TL;DR

Tôi cần một template engine cho 54 biến thể hợp đồng (3 loại HĐ × 6 phân khúc công trình × 3 client tier). Nếu materialize = 54 file `.docx` template. Mỗi lần sửa wording = 54 nơi fix. Sai một typo = drift inevitable.

Giải pháp: clause-graph compiler 5-layer (base → type overlay → segment overlay → tier+params → locale). Mỗi clause node có thuộc tính `vi`, `en`, `op` (replace / insert / skip), `when` (conditional). Compiler nhận `legal_request.json` → output flat clause graph → multi-format renderer (PDF + DOCX + Google Doc) đọc cùng intermediate representation.

Pattern này transfer được sang BOQ chain, dashboard variants, form builder — bất kỳ nơi nào có "core common + per-customer overlay".

## 1. Vấn đề

Một công ty B2B SMB ký nhiều loại hợp đồng tùy khách:

- 3 loại hợp đồng: Thiết kế (TK) — Thi công (TC) — Trọn gói (TRON_GOI)
- 6 phân khúc công trình: căn hộ — văn phòng — biệt thự — F&B — retail — kiosk
- 3 tier khách: small (SME/cá nhân) — medium (default) — large (enterprise lớn)
- 2 ngôn ngữ: tiếng Việt — bilingual VI+EN cho khách FDI

3 × 6 × 3 = **54 tổ hợp**. Mỗi tổ hợp có wording hơi khác. Vd:

- Trong văn phòng hạng A: thêm clause về làm đêm 22h-6h theo BQL tòa nhà, phụ phí điện ngoài giờ
- Trong F&B: thêm clause an toàn vệ sinh thực phẩm, exhaust + gas
- Tier large: payment 30/30/30/10 (giam vốn), tier medium 40/40/20

Khi sales/legal team mở Word, copy file template gần nhất, edit thủ công cho từng dự án. Hậu quả:

- Một typo lan ra 10+ file
- Một sửa đổi pháp lý cần fix 54 file
- Drift sau 3-6 tháng → mỗi file một version
- Bilingual table dễ lệch giữa VI và EN

Nhu cầu: **single source of truth cho clause text, render multi-format multi-language tự động**.

Đây là output cuối — cover page một bản Hợp đồng Thiết Kế dự án căn hộ (fixture demo, tier M, tiếng Việt đơn ngôn):

<Image src={apartmentTKCover} alt="Cover page Hợp đồng Thiết Kế — gold border, logo, doc meta bar, project info, parties grid 2 cột" loading="lazy" />

## 2. Architecture: 5-layer compiler

Sau khi loại bỏ option "materialize 54 file", thiết kế compiler:

```
PHASE 1 (compile clause graph):

   ┌─────────────────────────┐
   │  legal_request.json     │  ← input contract (validated JSON Schema)
   │  contract_type=TC       │
   │  loaiCongTrinh=office   │
   │  client_tier=L          │
   │  languages=[vi,en]      │
   │  params={...overrides}  │
   └────────────┬────────────┘
                │
       ┌────────▼─────────┐
       │  1. BASE          │  common.json (8 articles core, AIC-friendly)
       └────────┬─────────┘
                │
       ┌────────▼─────────┐
       │  2. TYPE          │  type_TC.json / type_TK.json / type_TRON_GOI.json
       │     overlay       │  (operations: replace, replace_with_schedule, append)
       └────────┬─────────┘
                │
       ┌────────▼─────────┐
       │  3. LOAI          │  overlay_office.json / overlay_apartment.json / ...
       │     overlay       │  (BQL hours, food safety, anchor lease, ...)
       └────────┬─────────┘
                │
       ┌────────▼─────────┐
       │  4. TIER+PARAMS   │  tier_S.json / tier_M.json / tier_L.json
       │     cascade       │  + request.params override (cao nhất)
       └────────┬─────────┘
                │
       ┌────────▼─────────┐
       │  5. LOCALE        │  locale.vi.json / locale.en.json
       │     labels        │  + {{key}} interpolation
       └────────┬─────────┘
                │
                ▼
       Compiled clause graph  ← intermediate representation
       { articles: [{num, title_vi, title_en, clauses: [...]}], params: {...}, ctx: {...} }

PHASE 2 (render multi-format):

       Compiled clause graph
              │
       ┌──────┼──────┐
       ▼      ▼      ▼
      PDF   DOCX  Google Doc
   WeasyPrint Pandoc Docs API
```

Mỗi clause node trong base có shape:

```json
{
  "num": "3.1",
  "anchor": "TC.payment.installment_1",
  "vi": "Lần 1 — 40% giá trị Hợp đồng...",
  "en": "1st Installment — 40% of Contract value...",
  "when": "client_tier in [S,M]"
}
```

`anchor` là khóa logic ổn định (không đổi khi số `num` xê dịch theo overlay). Overlay rule dùng `anchor` để target clause cần modify.

## 3. Operations enum — semantic không phải monkey-patch

Quan trọng nhất là operations enum trong overlay file. Không dùng regex replace (fragile), không dùng diff text (khó reason). Mỗi operation có ý nghĩa rõ ràng:

```js
// Trong type_TC.json
{
  "overlays": [
    {
      "op": "replace",
      "anchor": "BASE.scope.main",
      "vi": "Bên B thực hiện công việc thi công và trang trí nội thất...",
      "en": "Party B implements the construction and interior..."
    },
    {
      "op": "replace_with_schedule",
      "anchor": "BASE.payment.placeholder"
      // Compiler tự sinh N clauses từ params.payment_schedule
    },
    {
      "op": "append",
      "num": "X.Y",
      "anchor": "TC.pccc.support",
      "vi": "Bên B HỖ TRỢ Bên A trong thủ tục PCCC...",
      "en": "Party B SUPPORTS Party A in firefighting procedures...",
      "when": "loaiCongTrinh in [office,fnb,retail]"
    }
  ]
}
```

5 op core:

| `op` | Semantic | Khi nào dùng |
|---|---|---|
| `replace` | Swap text, giữ num + anchor | Override wording riêng cho type/segment |
| `replace_with_schedule` | Special — clone N clauses từ params.payment_schedule | Đợt thanh toán phụ thuộc tier |
| `insert_after` / `insert_before` | Splice clause mới tại vị trí anchor | Thêm clause phụ |
| `append` | Add cuối article hoặc cuối doc | Clause mới hoàn toàn |
| `skip` | Xóa clause | Tier S không cần penalty |

Mỗi op có thể có thuộc tính `when` để conditional apply. Compiler eval `when` trên context — expression nhỏ, restricted scope (chỉ support `==`, `in`, `&&`, `||`, dot access). Cố ý không cho user định nghĩa function — security + simplicity.

## 4. Parameterize hết — không hard-code financial values

Lesson từ skill cũ (đã DELETE): hard-code `0.5%/ngày phạt × max 8%` vào clause. Một số khách yêu cầu KHÔNG penalty, một số yêu cầu 0.3%. Phải fork clause. Chu kỳ fork → drift → review hell.

Refactor: mọi giá trị tài chính + timeline là **parameter trong `params` object**:

```json
{
  "params": {
    "penalty": null,                          // null = no penalty (tier S/M default)
    "warranty_months": 12,
    "payment_schedule": [
      { "pct": 0.40, "milestone": "after_signing", "vi": "Sau ký HĐ" },
      { "pct": 0.40, "milestone": "70%_completion", "vi": "Hoàn thành 70%" },
      { "pct": 0.20, "milestone": "handover", "vi": "Bàn giao" }
    ],
    "construction_days": 30,
    "force_majeure_scope": "broad",   // broad | standard
    "pccc_role": "support",           // support | represent
    "night_work_surcharge": "passthrough"
  }
}
```

3 file `tier_S.json` / `tier_M.json` / `tier_L.json` chứa defaults cho từng tier. Resolution order:

```
tier_<X>.json defaults  →  _project_meta.json  →  legal_request.params (cao nhất)
```

Pattern "convention over configuration". Sales mặc định lấy tier M, chỉ override khi khách lớn yêu cầu thay đổi cụ thể (vd "khách yêu cầu penalty 0.3%, override `params.penalty.rate = 0.003`").

## 5. Multi-format render — share intermediate representation

Output cần 3 format vì mục đích khác nhau:

- PDF: print legal, ký dấu mộc. Render với WeasyPrint HTML+CSS — kiểm soát layout chi tiết (cover page, bilingual table 2 cột, signature block).
- DOCX: client edit qua Word, comment. Render qua Pandoc (HTML → DOCX). Loss một số CSS branding nhưng đủ cho draft circulation.
- Google Doc: collaborative review online, comment + suggesting mode. Render qua Google Docs API native (batchUpdate requests).

Vì 3 format đều đọc cùng compiled clause graph, không có 3 bộ template song song. Sửa wording trong `common.json` → 3 format đồng bộ tự động.

Bilingual table render thực tế (fixture demo Trọn Gói, tier L, song ngữ VI+EN) — VI bên trái, EN italic bên phải:

<Image src={villaBilingualBody} alt="Hợp đồng song ngữ Trọn Gói VI+EN, bilingual table 2 cột với clause numbering gold, layout chuẩn legal FDI" loading="lazy" />

Pattern này quan trọng cho khách FDI Singapore-style — họ thường muốn xem song song để verify wording chuẩn legal trước khi ký.

Trade-off: Google Doc API có rate limit 429 → cần retry wrapper với exponential backoff. WeasyPrint không có Python module trên macOS brew (chỉ CLI binary) → render qua subprocess. Pandoc cần install riêng. Mỗi format có quirks runtime — wrapper script abstract hết.

## 6. Trigger UX qua Spreadsheet checkbox

Vấn đề: team sales/PM không biết JSON, không biết CLI. Cách hỏi họ "muốn gen HĐ loại gì cho dự án nào" càng simple càng tốt.

Master Spreadsheet đã có sẵn 1 row mỗi dự án (track BOQ). Thêm 7 cột:

| Cột | Kiểu | Default |
|---|---|---|
| `legal_TK_request` | Checkbox | FALSE |
| `legal_TC_request` | Checkbox | FALSE |
| `legal_TRON_GOI_request` | Checkbox | FALSE |
| `legal_languages` | Dropdown vi / vi_en / vi_cn | vi |
| `legal_client_tier` | Dropdown S / M / L | M |
| `legal_status` | Auto text | (empty) |
| `legal_drive_links` | Auto text | (empty) |

Cron poll Spreadsheet mỗi 15 phút. Khi thấy row có `legal_TC_request = TRUE` và `legal_status NOT IN [generating, done]`:

1. Lock per-project (`flock` per row)
2. Set status = `generating`
3. Build `legal_request.json` từ row + project meta
4. Spawn compiler → render PDF + DOCX + Google Doc → upload Drive folder dự án
5. Update status = `done` + paste link Drive vào cột `legal_drive_links`
6. Telegram bot notify

Team chỉ cần tick 1 checkbox, không phải học JSON.

## 7. Reservations

- Compiler ≠ luật sư. Output là first draft, luật sư vẫn phải review. Skill có pre-flight check (BOQ confidence ≥ 65%, schema valid) nhưng không thay được human legal review cho mỗi khách lớn.
- Customization fatigue. Quá nhiều `param` mở ra = sales paralysis ("nên đặt warranty 12 hay 18 tháng?"). Tier defaults giúp 80% case, nhưng tỉ lệ "tôi muốn override field này" vẫn cần training.
- Wording proofread. Bilingual table tự gen được, nhưng wording EN cần native review cho khách lớn. Pattern "AI gen + human polish" — không skip step polish.
- Drift trong base. Khi sửa `common.json` clause 5.1, mọi tổ hợp dùng clause này thay đổi cùng lúc. Tốt cho consistency, nhưng cần workflow review cẩn thận trước commit base.

## 8. Pattern transfer được

Compiler approach không chỉ áp dụng cho hợp đồng. Cùng pattern dùng cho:

- BOQ chain: base item list + overlay tier (premium / standard / lean) + overlay segment (apartment / office / villa) → compile thành Sheet 3-tab.
- Dashboard variants: base widgets + overlay role (executive / ops / dev) + theme dark/light → compile JSON config → render React.
- Form builder: base questions + overlay flow (onboarding / renewal / survey) + tier-specific fields → compile schema → render UI.
- Email template: base content + overlay campaign type + locale → compile MJML → render multi-language.

Anywhere bạn thấy **"có common base + variants với delta nhỏ"** và **N grows > 10**, compile pattern wins over materialize.

## 9. Kết quả: deliverable đẹp = thương hiệu nâng tầm

Đây là phần tôi không nghĩ tới lúc thiết kế architecture, nhưng lúc mở file PDF đầu tiên trên Preview thì... thực sự rất vui.

Một bộ 3 deliverable cho khách (Hợp đồng + Phụ lục Hợp đồng + Đề nghị thanh toán) — tất cả có:

- Cùng brand identity: gold #B8932E + Playfair Display serif + Inter sans, logo AIC pin top-left, tagline `Architecture · Interior · Construction` ở top-right header
- Cover page chuẩn: doc meta bar (Số HĐ / Mã dự án / Loại), republic block VN+EN, title block, project info highlight, parties grid 2 cột
- Bilingual table layout nhất quán cho khách FDI
- Signature block professional với stamp area + role labels song ngữ

Đối với SMB Việt như công ty tôi (AIC), đây là **bước tiến lớn về độ chỉn chu và chuyên nghiệp**. Trước đây mỗi PM/sales tự copy template Word, font xộc xệch, dấu cách lung tung, đôi khi sót logo, đôi khi quên signature block — mỗi khách thấy một version. Khách FDI lớn nhìn thoáng qua HĐ là biết "team này có process hay chưa". Một HĐ scan kém + dấu cách lệch = mất 5-10 điểm trust ngay từ first impression.

Giờ tick checkbox trong Spreadsheet → 90 giây sau có 3 file PDF/DOCX/Google Doc level deliverable FDI:

<Image src={apartmentTKBody} alt="Body một bản HĐ Thiết Kế render từ skill — clause numbering gold, justify alignment, font Inter, layout chuẩn legal" loading="lazy" />

Nhìn lại 3 ngày build, có lẽ phần "wow" nhất không phải code architecture mà là một cảm giác rất cụ thể: **khi bạn build tool cho company của mình, mỗi deliverable đẹp ra là một viên gạch nhỏ nâng tầm thương hiệu** — không phải marketing fluff, không phải tagline. Mỗi khách nhận file đó sẽ cảm thấy team mình có hệ thống, có brand, có investment vào tooling. Và họ cũng quyết định trust mình nhiều hơn.

Sustainable hơn cả là: pattern này tự amplify. Khi tooling chuyên nghiệp, team cũng nâng chuẩn output theo. Còn ngược lại, khi tool kém, anh em cũng dần buông chuẩn để... thoát kịp deadline.

## 10. Kết technical

Tôi xây skill này trong 3 ngày — 1 ngày architecture (chọn compile vs materialize), 1 ngày implement core (compile_clause_graph.js + 5-layer data), 1 ngày multi-format render + Spreadsheet integration. Verified E2E với 3 golden fixture (TC bilingual + TK VN-only + TRON_GOI bilingual). Render đẹp, byte-stable diff giữa các run.

Bonus discovery: trong quá trình build, phát hiện memory cá nhân của mình ghi sai info công ty (địa chỉ, mã số thuế). Verify từ 10+ digest project nội bộ → ground truth → update memory. Lesson: "anchored facts" như DKKD, MST cần periodic audit vs multiple sources — đơn nguồn dễ drift mà không phát hiện.

Skill này v0.2.0 status POC. Lên BETA tier khi production-run 30 ngày không incident.

---

**Stack**: Node.js (compiler + Google Docs API + Pandoc wrapper), Python (WeasyPrint + DOCX edit qua python-docx), JSON Schema (ajv validate), Google Sheets API (trigger UX).

**Tags pattern**: template-engine, compiler, dsl, contract-automation, claude-skills, vietnam-smb.

## FAQ
### Sao không dùng templating engine có sẵn như Jinja hay Handlebars?
Jinja/Handlebars handle string interpolation tốt nhưng không quản lý semantic operations giữa các biến thể — bạn vẫn cần một layer ở trên để mô tả 'với contract_type=X và loại công trình=Y, thay thế clause Z bằng nội dung khác'. Clause-graph compiler là layer đó.

### Khi nào materialize 54 variants tốt hơn compile?
Khi N nhỏ (≤ 5-10), khi biến thể hiếm khi thay đổi cùng lúc, và khi mỗi biến thể có nội dung rất riêng (không share common base). Dưới ngưỡng đó, copy-paste rẻ hơn compiler.

### Làm sao handle clause dependency — clause B chỉ valid nếu clause A có?
Mỗi clause có thuộc tính `when` chứa expression có thể evaluate trên context (vd `client_tier in [M,L]`). Compiler filter clauses theo `when` sau khi apply overlay. Dependency phức tạp hơn nên đẩy về validation runtime (JSON Schema + business rule).

### Multi-language workflow như thế nào (VI + EN + bilingual table)?
Mỗi clause node có 3 field {vi, en, cn}. Renderer biết `languages` array của request — nếu length=2 render bilingual table 2 cột, nếu length=1 render single column. Locale labels (như 'Điều 3', 'Article 3') tách ra `locale.<lang>.json` để dễ proofread cho native speaker.

### Test strategy cho 54 combinations?
Không test 54. Test 3 'golden fixture' đại diện: một tier S + ngôn ngữ đơn, một tier M + bilingual, một tier L + bilingual với mọi overlay. Mỗi commit chạy 3 fixture, byte-stable diff catch regression nhanh hơn ma trận 54.

---

Source: https://danghongtien.com/posts/2026-05-13-clause-graph-compiler-pattern/
Markdown export of canonical HTML article. License: CC BY 4.0 with attribution.
