# 06. Rule engine design

> **Đối tượng đọc**: Tech lead, backend dev (rule engine), mobile dev (rule application), P3 (cán bộ rule pack), QA.
> **Mục đích**: Khóa thiết kế chi tiết của rule engine — DSL, schema, lifecycle, signing, distribution, application — và bộ rule v1 ban đầu trích trực tiếp từ NĐ 37/2026 và TT 29/2023.

---

## 6.1. Mục tiêu thiết kế

Rule engine là **trái tim "phán quyết tuân thủ"** của OmiScan. Nó phải đáp ứng đồng thời 6 yêu cầu:

| # | Yêu cầu | Lý do |
|---|---|---|
| RE1 | **Remote-updatable** không cần release app mới | Văn bản pháp lý sửa 1–2 lần/năm, không thể chờ chu kỳ release Apple Store / Google Play (5–14 ngày) |
| RE2 | **Cùng một bộ rule chạy cả trên mobile và server** | Mobile cho preview sơ bộ, server là authoritative — cùng rule pack đảm bảo nhất quán |
| RE3 | **Non-coder có thể biên soạn** qua CMS form-based | P3 (cán bộ rule pack Viện DD) là chuyên gia dinh dưỡng, không phải lập trình viên |
| RE4 | **Verifiable + tamper-proof** | Rule pack ảnh hưởng đến phán quyết pháp lý → phải ký và verify để chống tampering |
| RE5 | **Versioned + rollback-able** | Có thể rollback nếu rule pack mới gây cảnh báo sai hàng loạt |
| RE6 | **Performant** trên cả mobile lẫn server | Chạy < 100ms cho 1 submission với ~50 rule |

---

## 6.2. Lựa chọn DSL — JsonLogic + extension

ADR-007 (Section 8.14) đã chốt: dùng **JsonLogic** với một bộ extension custom. Lý do tóm tắt:

| Tiêu chí | JsonLogic | CEL | Rego/OPA | Embedded JS/Lua |
|---|---|---|---|---|
| Cross-platform (JS, Dart, Kotlin, Swift) | ✅ Có lib | ✅ Google maintain | ⚠ Runtime ~5MB | ⚠ Sandbox khó |
| Form-based UI khả thi | ✅ JSON thuần | ⚠ Type-safe khó render | ❌ Quá phức tạp | ❌ Free-form code |
| Audit-friendly | ✅ Diff JSON dễ | ✅ | ✅ | ❌ Code diff khó audit |
| Tốc độ | ✅ Nhanh | ✅ Nhanh | ⚠ Trung bình | ✅ Nhanh |
| Bộ ops mạnh | ⚠ Cần extend | ✅ Mạnh sẵn | ✅✅ Rất mạnh | ✅ Mạnh |
| Học dễ cho dev | ✅ | ⚠ | ❌ | ✅ |

→ JsonLogic được chọn vì cân bằng tốt nhất 6 tiêu chí.

### 6.2.1. Operators chuẩn JsonLogic dùng trong OmiScan

| Op | Ý nghĩa | Ví dụ |
|---|---|---|
| `==`, `!=`, `<`, `<=`, `>`, `>=` | So sánh | `{"<": [{"var": "fields.weight_g"}, 100]}` |
| `and`, `or`, `!` | Logic | `{"and": [{">": [...]}, {"<": [...]}]}` |
| `if` | Điều kiện | `{"if": [cond, then, else]}` |
| `var` | Truy cập field | `{"var": "fields.nutrition_per_100g.kcal"}` |
| `missing` | Field thiếu | `{"missing": ["fields.name_vi"]}` |
| `in` | Membership | `{"in": ["thực_phẩm", {"var": "category"}]}` |
| `all`, `some`, `none` | Quantifier trên array | `{"all": [arr, predicate]}` |
| `cat` | Concat string | `{"cat": ["Sản phẩm: ", {"var": "name"}]}` |
| `+`, `-`, `*`, `/` | Số học | `{"*": [{"var": "fat_g"}, 9]}` |

### 6.2.2. Extension custom OmiScan

Bộ JsonLogic chuẩn không đủ cho domain ghi nhãn thực phẩm. Em bổ sung 6 op custom:

| Op | Ý nghĩa | Ví dụ |
|---|---|---|
| **`exists`** | Field tồn tại và không null/empty | `{"exists": ["fields.name_vi"]}` |
| **`regex_match`** | String khớp regex | `{"regex_match": [{"var": "fields.nsx"}, "^\\d{4}-\\d{2}-\\d{2}$"]}` |
| **`unit_convert`** | Chuyển đơn vị (g↔kg, ml↔l, mg↔g) | `{"unit_convert": [{"var": "sodium"}, "mg", "g"]}` |
| **`nutrition_ratio`** | Tỷ lệ giữa 2 nutrient | `{"nutrition_ratio": ["fat_g", "energy_kcal", "kcal_from_fat"]}` |
| **`category_match`** | Submission category nằm trong list nhóm hàng (theo Phụ lục I NĐ 37) | `{"category_match": [{"var": "category"}, ["thực_phẩm", "đồ_uống"]]}` |
| **`date_diff_days`** | Số ngày giữa 2 mốc thời gian | `{"date_diff_days": [{"var": "hsd"}, "today"]}` |

Mỗi extension được implement như predicate plugin trong cả 3 stack: TypeScript (server Lambda), Dart (mobile), TypeScript (CMS preview). Test bằng property-based testing để đảm bảo behavior y hệt nhau cross-platform.

### 6.2.3. Code skeleton extension

```typescript
// rule-engine/src/extensions.ts (TypeScript shared cho server + CMS)
import jsonLogic from 'json-logic-js';

jsonLogic.add_operation('exists', (val: any) => {
  if (val === null || val === undefined) return false;
  if (typeof val === 'string') return val.trim().length > 0;
  if (Array.isArray(val)) return val.length > 0;
  return true;
});

jsonLogic.add_operation('regex_match', (val: string, pattern: string) => {
  if (typeof val !== 'string') return false;
  return new RegExp(pattern).test(val);
});

jsonLogic.add_operation('unit_convert', (val: number, fromUnit: string, toUnit: string) => {
  // Bảng tra cứu
  const factors: Record<string, Record<string, number>> = {
    g: { kg: 0.001, mg: 1000, g: 1 },
    kg: { g: 1000, mg: 1_000_000, kg: 1 },
    mg: { g: 0.001, kg: 0.000001, mg: 1 },
    ml: { l: 0.001, ml: 1 },
    l: { ml: 1000, l: 1 },
  };
  const factor = factors[fromUnit]?.[toUnit];
  if (factor === undefined) throw new Error(`unit_convert unsupported: ${fromUnit}→${toUnit}`);
  return val * factor;
});

jsonLogic.add_operation('category_match', (cat: string, allowed: string[]) => {
  return allowed.includes(cat);
});

jsonLogic.add_operation('nutrition_ratio', (numerator: string, denominator: string, type: string) => {
  // Implementation: lookup theo type, ví dụ "kcal_from_fat" = fat_g * 9 / energy_kcal
  // ...
});

jsonLogic.add_operation('date_diff_days', (date1: string, date2: string | 'today') => {
  const d1 = new Date(date1);
  const d2 = date2 === 'today' ? new Date() : new Date(date2);
  return Math.floor((d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24));
});
```

```dart
// rule-engine/lib/extensions.dart (Dart cho mobile)
class JsonLogicExtensions {
  static dynamic exists(dynamic val) {
    if (val == null) return false;
    if (val is String) return val.trim().isNotEmpty;
    if (val is List) return val.isNotEmpty;
    return true;
  }

  static dynamic regexMatch(String val, String pattern) {
    return RegExp(pattern).hasMatch(val);
  }

  // ... unitConvert, categoryMatch, nutritionRatio, dateDiffDays
}
```

Cross-language behavior verified bằng test fixture chia sẻ: `test/fixtures/rule_evaluations.json` với 200 case input → expected output.

---

## 6.3. Rule pack schema

```typescript
interface RulePack {
  // Metadata
  schema_version: "1.0";              // versioning của bản thân schema
  pack_version: string;               // semver của rule pack, ví dụ "2026.05.10"
  applies_from: string;               // ISO 8601 date, ví dụ "2026-06-01"
  effective_until?: string;           // optional, khi rule pack expire
  legal_basis: string[];              // ["NĐ 37/2026/NĐ-CP", "TT 29/2023/TT-BYT"]
  authoring: {
    author_user_id: string;
    approver_user_id: string;
    created_at: string;
    approved_at: string;
  };
  changelog: string;                  // user-facing description of changes

  // Rules
  rules: Rule[];

  // Crypto
  signature: {
    algorithm: "ECDSA-P256";
    public_key_id: string;            // ID khóa công khai để client verify
    signature_b64: string;            // signature của canonical JSON (không bao gồm `signature` field)
    signed_at: string;
  };

  // App compat
  min_app_version: string;            // semver, app cũ hơn không apply pack này
  min_schema_version: string;         // schema bắt buộc, để bridge tương lai
}

interface Rule {
  id: string;                         // unique trong pack, ví dụ "ND37-MANDATORY-NAME"
  enabled: boolean;                   // có thể tắt rule mà không xóa
  severity: "violation" | "warning" | "advisory";
  source: "compliance" | "advisory";  // compliance = bắt buộc theo luật; advisory = khuyến nghị Viện DD
  scope: RuleScope;
  legal_ref: {                        // hoặc advisory_ref cho rule advisory
    document: string;                 // "NĐ 37/2026/NĐ-CP"
    article: string;                  // "Điều 42"
    clause?: string;                  // "khoản 1 điểm a"
  };
  title: string;                      // ngắn, hiển thị trên CMS
  predicate: JsonLogicExpr;           // expression. Returns true = pass, false = violated
  message_template: string;           // i18n key hoặc template literal với placeholder {value}
  remediation_hint?: string;          // gợi ý sửa cho user (P1)
  tags?: string[];                    // ví dụ: ["nutrition", "TPCN"]
  created_at: string;
  updated_at: string;
}

interface RuleScope {
  // Áp dụng nếu submission match TẤT CẢ điều kiện scope
  product_categories?: string[];      // theo enum 11 nhóm thực phẩm — xem 6.3.1
  product_subtypes?: string[];        // ví dụ ["sữa_thêm_đường", "đồ_uống_chiên_rán"]
  is_imported?: boolean;              // chỉ áp với hàng nhập khẩu
  has_specific_ingredient?: string[]; // ví dụ ["chất_tạo_ngọt", "phụ_gia_INS_xxx"]
  // Nếu không có condition nào → áp với tất cả submission (scope = "all")
}

type JsonLogicExpr = any;             // Recursive nested object theo grammar JsonLogic
```

### 6.3.1. Enum nhóm thực phẩm OmiScan v1

Trích từ Phụ lục I NĐ 37/2026 (xem [04_legal_framework.md](04_legal_framework.md)), thu hẹp về 11 nhóm relevant:

| Code | Tên | Đến từ Phụ lục I NĐ 37 |
|---|---|---|
| `lương_thực` | Lương thực | Nhóm 1 |
| `thực_phẩm` | Thực phẩm chung | Nhóm 2 |
| `thực_phẩm_BVSK` | Thực phẩm bảo vệ sức khỏe | Nhóm 3 |
| `thực_phẩm_chiếu_xạ` | Thực phẩm đã qua chiếu xạ | Nhóm 4 |
| `thực_phẩm_GMO` | Thực phẩm biến đổi gen | Nhóm 5 |
| `đồ_uống_không_cồn` | Đồ uống (trừ rượu) | Nhóm 6 |
| `rượu` | Rượu | Nhóm 7 |
| `phụ_gia` | Phụ gia thực phẩm, chất hỗ trợ | Nhóm 9 |
| `vi_chất_dinh_dưỡng` | Vi chất dinh dưỡng | Nhóm 10 |
| `nguyên_liệu_thực_phẩm` | Nguyên liệu thực phẩm | Nhóm 11 |
| `thực_phẩm_đặc_biệt` | TPBS, TPDDYH, TP chế độ ăn đặc biệt | Nhóm 67 |

Subtype:
- `sữa_thêm_đường` (cho TT 29 Điều 5.2 — bắt buộc đường tổng số)
- `đồ_uống_chiên_rán` (cho TT 29 Điều 5.3 — bắt buộc chất béo bão hòa)
- Các subtype khác theo nhu cầu rule

---

## 6.4. Bộ rule v1 ban đầu — trích trực tiếp từ NĐ 37/2026 + TT 29/2023

Đây là **input đề xuất ban đầu** cho rule pack v1. Cần Anh + Viện DD review trước khi publish.

### 6.4.1. Nhóm A — Compliance rules (severity = violation)

#### Từ Điều 42 NĐ 37/2026 — Nội dung bắt buộc nhãn

```json
{
  "id": "ND37-E42-1-A-NAME-EXISTS",
  "enabled": true,
  "severity": "violation",
  "source": "compliance",
  "scope": {},
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Điều 42", "clause": "khoản 1 điểm a" },
  "title": "Tên hàng hóa phải có trên nhãn",
  "predicate": { "exists": [{ "var": "fields.name_vi" }] },
  "message_template": "Thiếu tên hàng hóa — bắt buộc theo NĐ 37/2026 Điều 42.1.a",
  "remediation_hint": "Kiểm tra mặt trước bao bì, chụp lại nếu chữ bị mờ"
}
```

```json
{
  "id": "ND37-E42-1-B-MANUFACTURER-EXISTS",
  "severity": "violation",
  "source": "compliance",
  "scope": {},
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Điều 42", "clause": "khoản 1 điểm b" },
  "title": "Tên + địa chỉ tổ chức/cá nhân chịu trách nhiệm phải có",
  "predicate": {
    "and": [
      { "exists": [{ "var": "fields.manufacturer.name" }] },
      { "exists": [{ "var": "fields.manufacturer.address" }] }
    ]
  },
  "message_template": "Thiếu tên hoặc địa chỉ tổ chức/cá nhân chịu trách nhiệm về hàng hóa — bắt buộc theo NĐ 37/2026 Điều 42.1.b"
}
```

```json
{
  "id": "ND37-E42-1-C-ORIGIN-EXISTS",
  "severity": "violation",
  "source": "compliance",
  "scope": {},
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Điều 42", "clause": "khoản 1 điểm c" },
  "title": "Xuất xứ hàng hóa phải có",
  "predicate": { "exists": [{ "var": "fields.origin" }] },
  "message_template": "Thiếu thông tin xuất xứ hàng hóa — bắt buộc theo NĐ 37/2026 Điều 42.1.c"
}
```

#### Từ Phụ lục I NĐ 37/2026 — Nhóm 2 (Thực phẩm)

```json
{
  "id": "ND37-PL1-FOOD-WEIGHT",
  "severity": "violation",
  "source": "compliance",
  "scope": { "product_categories": ["thực_phẩm", "lương_thực", "đồ_uống_không_cồn"] },
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Phụ lục I, mục 2.a" },
  "title": "Định lượng phải có trên nhãn thực phẩm",
  "predicate": {
    "and": [
      { "exists": [{ "var": "fields.weight_g" }] },
      { "exists": [{ "var": "fields.weight_unit" }] }
    ]
  },
  "message_template": "Thiếu định lượng (khối lượng tịnh) — bắt buộc theo NĐ 37/2026 Phụ lục I mục 2.a"
}
```

```json
{
  "id": "ND37-PL1-FOOD-NSX-HSD",
  "severity": "violation",
  "source": "compliance",
  "scope": { "product_categories": ["thực_phẩm", "lương_thực", "đồ_uống_không_cồn"] },
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Phụ lục I, mục 2.b-c" },
  "title": "Ngày sản xuất và hạn sử dụng (hoặc Best before) phải có",
  "predicate": {
    "and": [
      { "exists": [{ "var": "fields.nsx" }] },
      {
        "or": [
          { "exists": [{ "var": "fields.hsd" }] },
          { "==": [{ "var": "fields.best_before" }, true] }
        ]
      }
    ]
  },
  "message_template": "Thiếu ngày sản xuất hoặc hạn sử dụng — bắt buộc theo NĐ 37/2026 Phụ lục I mục 2.b-c"
}
```

```json
{
  "id": "ND37-E46-DATE-FORMAT",
  "severity": "warning",
  "source": "compliance",
  "scope": { "product_categories": ["thực_phẩm", "lương_thực", "đồ_uống_không_cồn", "thực_phẩm_BVSK"] },
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Điều 46", "clause": "khoản 1" },
  "title": "Format ngày sản xuất phải đúng quy định",
  "predicate": {
    "or": [
      { "regex_match": [{ "var": "fields.nsx" }, "^\\d{2}/\\d{2}/\\d{2,4}$"] },
      { "regex_match": [{ "var": "fields.nsx" }, "^\\d{2}-\\d{2}-\\d{2,4}$"] },
      { "regex_match": [{ "var": "fields.nsx" }, "^NSX:?\\s*\\d{2}/\\d{2}/\\d{2,4}$"] }
    ]
  },
  "message_template": "Format NSX có thể không đúng — NĐ 37/2026 Điều 46.1 yêu cầu thứ tự ngày-tháng-năm dương lịch"
}
```

```json
{
  "id": "ND37-PL1-FOOD-INGREDIENTS",
  "severity": "violation",
  "source": "compliance",
  "scope": { "product_categories": ["thực_phẩm", "lương_thực", "đồ_uống_không_cồn"] },
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Phụ lục I mục 2.d + Điều 48" },
  "title": "Thành phần phải có trên nhãn",
  "predicate": { "exists": [{ "var": "fields.ingredients_text" }] },
  "message_template": "Thiếu danh sách thành phần — bắt buộc theo NĐ 37/2026 Điều 48"
}
```

```json
{
  "id": "ND37-PL1-FOOD-USAGE-STORAGE",
  "severity": "warning",
  "source": "compliance",
  "scope": { "product_categories": ["thực_phẩm", "lương_thực", "đồ_uống_không_cồn"] },
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Phụ lục I mục 2.e" },
  "title": "Hướng dẫn sử dụng + bảo quản nên có",
  "predicate": {
    "and": [
      { "exists": [{ "var": "fields.usage_instruction" }] },
      { "exists": [{ "var": "fields.storage_instruction" }] }
    ]
  },
  "message_template": "Thiếu hướng dẫn sử dụng hoặc bảo quản — khuyến nghị bổ sung theo NĐ 37/2026 Phụ lục I mục 2.e"
}
```

#### Từ TT 29/2023/TT-BYT — Nutrition

> **Thuật ngữ thống nhất**: TT 29/2023 yêu cầu **5 chỉ tiêu CORE bắt buộc** (năng lượng, đạm, carbohydrat, chất béo, natri — Điều 5.1) + **2 chỉ tiêu CONDITIONAL** áp theo nhóm sản phẩm:
> - Đường tổng số — bắt buộc với sản phẩm thêm đường (Điều 5.2)
> - Chất béo bão hòa — bắt buộc với thực phẩm chiên rán (Điều 5.3)
>
> Schema `nutrition_per_100g` ở Section 05 + Section 07 có 7 fields tổng (5 core + 2 conditional) — không phải đòi hỏi tất cả 7. Rule engine có **exemption layer chạy TRƯỚC violation rules** (xem rule TT29-E5-4 dưới đây): nếu giá trị nutrient ≤ ngưỡng Phụ lục I (kcal ≤ 4/100ml, đạm/carb/chất béo ≤ 0.5g/100g, natri ≤ 5mg/100g, …), submission được miễn ghi field đó và rule TT29-E5-1 KHÔNG flag violation. Logic exemption: `if exempt_check_passes(field) then field_required = false`.

```json
{
  "id": "TT29-E5-1-NUTRITION-5-CORE",
  "severity": "violation",
  "source": "compliance",
  "scope": {
    "product_categories": ["thực_phẩm", "lương_thực", "đồ_uống_không_cồn"]
  },
  "legal_ref": { "document": "TT 29/2023/TT-BYT", "article": "Điều 5", "clause": "khoản 1" },
  "title": "Bảng dinh dưỡng phải có 5 chỉ tiêu cơ bản",
  "predicate": {
    "and": [
      { "exists": [{ "var": "fields.nutrition_per_100g.kcal" }] },
      { "exists": [{ "var": "fields.nutrition_per_100g.protein_g" }] },
      { "exists": [{ "var": "fields.nutrition_per_100g.carb_g" }] },
      { "exists": [{ "var": "fields.nutrition_per_100g.fat_g" }] },
      { "exists": [{ "var": "fields.nutrition_per_100g.sodium_mg" }] }
    ]
  },
  "message_template": "Thiếu chỉ tiêu dinh dưỡng cơ bản — TT 29/2023 Điều 5.1 yêu cầu năng lượng, đạm, carbohydrat, chất béo, natri"
}
```

```json
{
  "id": "TT29-E5-2-SUGAR-FOR-SWEETENED",
  "severity": "violation",
  "source": "compliance",
  "scope": { "product_subtypes": ["sữa_thêm_đường", "nước_giải_khát_thêm_đường", "thực_phẩm_thêm_đường"] },
  "legal_ref": { "document": "TT 29/2023/TT-BYT", "article": "Điều 5", "clause": "khoản 2" },
  "title": "Sản phẩm thêm đường phải ghi đường tổng số",
  "predicate": { "exists": [{ "var": "fields.nutrition_per_100g.sugar_g" }] },
  "message_template": "Thiếu chỉ tiêu Đường tổng số — TT 29/2023 Điều 5.2 bắt buộc cho nước giải khát/sữa/thực phẩm thêm đường"
}
```

```json
{
  "id": "TT29-E5-3-SAT-FAT-FOR-FRIED",
  "severity": "violation",
  "source": "compliance",
  "scope": { "product_subtypes": ["thực_phẩm_chiên_rán"] },
  "legal_ref": { "document": "TT 29/2023/TT-BYT", "article": "Điều 5", "clause": "khoản 3" },
  "title": "Thực phẩm chiên rán phải ghi chất béo bão hòa",
  "predicate": { "exists": [{ "var": "fields.nutrition_per_100g.fat_sat_g" }] },
  "message_template": "Thiếu Chất béo bão hòa — TT 29/2023 Điều 5.3 bắt buộc với thực phẩm chiên rán"
}
```

```json
{
  "id": "TT29-E5-4-EXEMPT-LOW-VALUES",
  "severity": "advisory",
  "source": "compliance",
  "scope": { "product_categories": ["thực_phẩm", "đồ_uống_không_cồn"] },
  "legal_ref": { "document": "TT 29/2023/TT-BYT", "article": "Điều 5 + Phụ lục I" },
  "title": "Chỉ tiêu giá trị thấp được miễn ghi (informational)",
  "predicate": {
    "and": [
      { "<=": [{ "var": "fields.nutrition_per_100g.kcal" }, 4] }
    ]
  },
  "message_template": "Năng lượng ≤ 4 kcal/100ml — TT 29/2023 Phụ lục I cho phép miễn ghi"
}
```

```json
{
  "id": "TT29-E6-1-NUTRITION-UNITS",
  "severity": "violation",
  "source": "compliance",
  "scope": { "product_categories": ["thực_phẩm", "lương_thực", "đồ_uống_không_cồn"] },
  "legal_ref": { "document": "TT 29/2023/TT-BYT", "article": "Điều 6", "clause": "khoản 1" },
  "title": "Đơn vị dinh dưỡng phải đúng quy định",
  "predicate": {
    "and": [
      { "==": [{ "var": "fields.nutrition_per_100g.kcal_unit" }, "kcal"] },
      { "in": [{ "var": "fields.nutrition_per_100g.basis" }, ["100g", "100ml", "khẩu_phần"]] },
      { "==": [{ "var": "fields.nutrition_per_100g.sodium_unit" }, "mg"] }
    ]
  },
  "message_template": "Đơn vị bảng dinh dưỡng không đúng quy định — TT 29/2023 Điều 6.1: kcal cho năng lượng, g cho đạm/carb/chất béo, mg cho natri, biểu thị trên 100g/100ml/khẩu phần"
}
```

#### Hàng nhập khẩu — Điều 39 + 40 NĐ 37/2026

```json
{
  "id": "ND37-E40-VI-SUB-LABEL-IMPORTED",
  "severity": "violation",
  "source": "compliance",
  "scope": { "is_imported": true },
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Điều 40", "clause": "khoản 1" },
  "title": "Hàng nhập khẩu phải có nhãn phụ tiếng Việt",
  "predicate": { "==": [{ "var": "fields.has_vi_sub_label" }, true] },
  "message_template": "Hàng nhập khẩu thiếu nhãn phụ tiếng Việt — bắt buộc theo NĐ 37/2026 Điều 40"
}
```

#### TPCN/TPBVSK — Phụ lục I NĐ 37 nhóm 3

```json
{
  "id": "ND37-PL1-3-TPBVSK-DISCLAIMER",
  "severity": "violation",
  "source": "compliance",
  "scope": { "product_categories": ["thực_phẩm_BVSK"] },
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Phụ lục I mục 3.h" },
  "title": "TPBVSK phải có cụm từ tuyên bố không thay thế thuốc",
  "predicate": {
    "regex_match": [
      { "var": "fields.warning_labels_concat" },
      "Thực phẩm này không phải là thuốc"
    ]
  },
  "message_template": "Thiếu cụm từ \"Thực phẩm này không phải là thuốc, không có tác dụng thay thế thuốc chữa bệnh\" — bắt buộc theo NĐ 37/2026 Phụ lục I mục 3.h"
}
```

```json
{
  "id": "ND37-PL1-3-TPBVSK-LABEL",
  "severity": "violation",
  "source": "compliance",
  "scope": { "product_categories": ["thực_phẩm_BVSK"] },
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Phụ lục I mục 3.g" },
  "title": "Phải ghi cụm từ \"Thực phẩm bảo vệ sức khỏe\"",
  "predicate": {
    "regex_match": [
      { "var": "fields.product_descriptors" },
      "Thực phẩm bảo vệ sức khỏe"
    ]
  },
  "message_template": "Thiếu cụm từ \"Thực phẩm bảo vệ sức khỏe\" — bắt buộc theo NĐ 37/2026 Phụ lục I mục 3.g"
}
```

#### Thực phẩm chiếu xạ + GMO — Phụ lục I nhóm 4, 5

```json
{
  "id": "ND37-PL1-4-IRRADIATED-LABEL",
  "severity": "violation",
  "source": "compliance",
  "scope": { "product_categories": ["thực_phẩm_chiếu_xạ"] },
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Phụ lục I mục 4.e" },
  "title": "Thực phẩm chiếu xạ phải ghi cụm từ",
  "predicate": {
    "regex_match": [
      { "var": "fields.product_descriptors" },
      "Thực phẩm đã qua chiếu xạ"
    ]
  },
  "message_template": "Thiếu cụm từ \"Thực phẩm đã qua chiếu xạ\" — bắt buộc theo NĐ 37/2026 Phụ lục I mục 4.e"
}
```

```json
{
  "id": "ND37-PL1-5-GMO-LABEL",
  "severity": "violation",
  "source": "compliance",
  "scope": { "product_categories": ["thực_phẩm_GMO"] },
  "legal_ref": { "document": "NĐ 37/2026/NĐ-CP", "article": "Phụ lục I mục 5.e" },
  "title": "Thực phẩm biến đổi gen phải ghi cụm từ",
  "predicate": {
    "or": [
      { "regex_match": [{ "var": "fields.product_descriptors" }, "Thực phẩm biến đổi gen"] },
      { "regex_match": [{ "var": "fields.product_descriptors" }, "biến đổi gen"] }
    ]
  },
  "message_template": "Thiếu cụm từ \"Thực phẩm biến đổi gen\" hoặc \"biến đổi gen\" — bắt buộc theo NĐ 37/2026 Phụ lục I mục 5.e"
}
```

### 6.4.2. Nhóm B — Advisory rules từ Viện Dinh dưỡng (severity = warning / advisory)

Các rule này KHÔNG phải compliance pháp lý mà là khuyến nghị dinh dưỡng do chuyên gia Viện DD soạn. Mục đích: cung cấp insight cho cán bộ + lãnh đạo để hoạch định chính sách dinh dưỡng quốc gia.

```json
{
  "id": "VDD-WHO-SODIUM-HIGH",
  "severity": "advisory",
  "source": "advisory",
  "scope": { "product_categories": ["thực_phẩm", "đồ_uống_không_cồn"] },
  "legal_ref": { "document": "Khuyến nghị WHO về giảm muối", "article": "—" },
  "title": "Cảnh báo natri cao",
  "predicate": { ">": [{ "var": "fields.nutrition_per_100g.sodium_mg" }, 600] },
  "message_template": "Hàm lượng natri {value}mg/100g — cao hơn ngưỡng WHO khuyến nghị (600mg/100g). Nên giảm muối để bảo vệ sức khỏe tim mạch"
}
```

```json
{
  "id": "VDD-SUGAR-HIGH",
  "severity": "advisory",
  "source": "advisory",
  "scope": { "product_categories": ["đồ_uống_không_cồn"] },
  "legal_ref": { "document": "Khuyến nghị Viện DD về giảm đường", "article": "—" },
  "title": "Cảnh báo đường cao",
  "predicate": { ">": [{ "var": "fields.nutrition_per_100g.sugar_g" }, 10] },
  "message_template": "Đường tổng số {value}g/100ml — cao hơn ngưỡng khuyến nghị Viện Dinh dưỡng (10g/100ml)"
}
```

```json
{
  "id": "VDD-FAT-RATIO-HIGH",
  "severity": "advisory",
  "source": "advisory",
  "scope": { "product_categories": ["thực_phẩm"] },
  "legal_ref": { "document": "Khuyến nghị Viện DD", "article": "—" },
  "title": "Tỷ lệ năng lượng từ chất béo cao",
  "predicate": {
    ">": [
      {
        "/": [
          { "*": [{ "var": "fields.nutrition_per_100g.fat_g" }, 9] },
          { "var": "fields.nutrition_per_100g.kcal" }
        ]
      },
      0.35
    ]
  },
  "message_template": "Tỷ lệ năng lượng từ chất béo > 35% tổng năng lượng — cao hơn khuyến nghị (Viện DD: tối đa 25-35% tùy đối tượng)"
}
```

```json
{
  "id": "VDD-FAT-SAT-RATIO-HIGH",
  "severity": "advisory",
  "source": "advisory",
  "scope": { "product_categories": ["thực_phẩm"] },
  "legal_ref": { "document": "Khuyến nghị Codex CAC/GL 2-1985 + WHO", "article": "—" },
  "title": "Chất béo bão hòa cao so với tổng chất béo",
  "predicate": {
    ">": [
      {
        "/": [
          { "var": "fields.nutrition_per_100g.fat_sat_g" },
          { "var": "fields.nutrition_per_100g.fat_g" }
        ]
      },
      0.4
    ]
  },
  "message_template": "Chất béo bão hòa chiếm > 40% tổng chất béo — khuyến cáo giảm để hạn chế nguy cơ tim mạch"
}
```

### 6.4.3. Nhóm C — Validation rules cho thông tin định danh

```json
{
  "id": "OMI-EXPIRED-PRODUCT",
  "severity": "warning",
  "source": "advisory",
  "scope": {},
  "legal_ref": { "document": "—", "article": "Validation OmiScan" },
  "title": "Sản phẩm đã hết hạn sử dụng",
  "predicate": { "<": [{ "date_diff_days": [{ "var": "fields.hsd" }, "today"] }, 0] },
  "message_template": "Sản phẩm đã hết hạn sử dụng theo nhãn (HSD: {hsd}). Có thể là sản phẩm tồn kho cần báo cáo"
}
```

```json
{
  "id": "OMI-DECLARATION-NUMBER-FORMAT",
  "severity": "warning",
  "source": "advisory",
  "scope": { "product_categories": ["thực_phẩm", "đồ_uống_không_cồn", "thực_phẩm_BVSK"] },
  "legal_ref": { "document": "NĐ 15/2018/NĐ-CP", "article": "Điều 4, 6" },
  "title": "Số tự công bố / số đăng ký công bố nên có",
  "predicate": {
    "or": [
      { "exists": [{ "var": "fields.self_declaration_no" }] },
      { "exists": [{ "var": "fields.registration_no" }] }
    ]
  },
  "message_template": "Thiếu số tự công bố hoặc số đăng ký bản công bố — khuyến cáo có theo NĐ 15/2018"
}
```

### 6.4.4. Tóm tắt bộ rule v1

| Nhóm | Số lượng | Severity |
|---|---|---|
| Compliance NĐ 37/2026 | 12 rule | Mostly violation |
| Compliance TT 29/2023 | 5 rule | violation + advisory |
| Compliance NĐ 15/2018 | 1 rule | warning |
| Advisory Viện DD (dinh dưỡng) | 4 rule | advisory |
| Validation OmiScan (cấp metadata) | 1 rule | warning |
| **Tổng** | **23 rule** | |

→ Mục tiêu Section 6.4 đạt **15+ rule** (yêu cầu Section 01) — vượt mức.

---

## 6.5. Rule pack lifecycle

```
                      ┌────────────────────────────────────────┐
                      │  RULE PACK LIFECYCLE                   │
                      └────────────────────────────────────────┘

  P3 (Cán bộ rule)       CMS Backend          rule-pack-signer Lambda     CDN
       │                     │                          │                   │
   ┌───▼───┐                 │                          │                   │
   │ DRAFT │                 │                          │                   │
   └───┬───┘                 │                          │                   │
       │ Save / Edit         │                          │                   │
       ├────────────────────►│ Lưu PG bảng rule_drafts  │                   │
       │                     │                          │                   │
       │ Preview trên test set│                          │                   │
       ├────────────────────►│ Apply trên 50 sub gần    │                   │
       │                     │   nhất, hiện kết quả     │                   │
       │                     │                          │                   │
       │ Submit for approval │                          │                   │
       ├────────────────────►│ Status: PENDING_APPROVAL │                   │
       │                     │ Notify P3.5              │                   │
       │                     │                          │                   │
   ┌───┴────┐                │                          │                   │
   │ P3.5   │  Review diff   │                          │                   │
   │approver│ ◄──────────────┤                          │                   │
   └───┬────┘                │                          │                   │
       │ Reject              │                          │                   │
       │ ────► quay về DRAFT │                          │                   │
       │                     │                          │                   │
       │ Accept              │                          │                   │
       ├────────────────────►│ POST /rule-packs/publish │                   │
       │                     ├─────────────────────────►│                   │
       │                     │                          │ Bump semver       │
       │                     │                          │ Canonicalize JSON │
       │                     │                          │ KMS Sign ECDSA P-256│
       │                     │                          │ Upload S3         │
       │                     │                          ├──────────────────►│
       │                     │                          │ Update manifest   │
       │                     │                          ├──────────────────►│
       │                     │                          │ Invalidate cache  │
       │                     │                          ├──────────────────►│
       │                     │                          │ Audit log         │
       │                     │   200 + version          │                   │
       │                     │◄─────────────────────────┤                   │
       │  Status: PUBLISHED  │                          │                   │
       │◄────────────────────┤                          │                   │
       │                     │                          │                   │
       Mobile app (next pull) and validation-engine Lambda fetch new pack
       
       Rollback flow:
       P3 chọn version cũ → re-publish với metadata "rollback from X" → audit log đầy đủ
```

### 6.5.1. State machine của rule pack version

```
DRAFT ──submit──► PENDING_APPROVAL ──reject──► DRAFT
                       │
                       ├──approve + sign──► PUBLISHED
                                              │
                                              ├──supersede─► SUPERSEDED (version cũ)
                                              └──rollback──► PUBLISHED (re-promote)
```

### 6.5.2. PostgreSQL schema cho rule packs

```sql
CREATE TABLE rule_packs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  version VARCHAR(50) NOT NULL UNIQUE,    -- semver, ví dụ "2026.05.10"
  status VARCHAR(20) NOT NULL,            -- DRAFT / PENDING_APPROVAL / PUBLISHED / SUPERSEDED
  pack_json JSONB NOT NULL,               -- toàn bộ pack
  signature TEXT,                         -- ECDSA P-256 DER b64
  legal_basis TEXT[],
  changelog TEXT,
  author_id UUID NOT NULL REFERENCES users(id),
  approver_id UUID REFERENCES users(id),
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  approved_at TIMESTAMPTZ,
  published_at TIMESTAMPTZ,
  superseded_at TIMESTAMPTZ,
  superseded_by UUID REFERENCES rule_packs(id)
);

CREATE INDEX idx_rule_packs_status ON rule_packs(status);
CREATE INDEX idx_rule_packs_published_at ON rule_packs(published_at DESC) WHERE status = 'PUBLISHED';

CREATE TABLE rule_pack_test_results (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  rule_pack_id UUID NOT NULL REFERENCES rule_packs(id),
  submission_id UUID NOT NULL REFERENCES submissions(id),
  rule_id VARCHAR(100) NOT NULL,
  result JSONB NOT NULL,
  ran_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

---

## 6.6. Signing & distribution

### 6.6.1. Cryptography

- **Algorithm**: **ECDSA P-256** (NIST FIPS 186-4) — KMS native support `ECC_NIST_P256`, signature ~71 bytes (DER), verify trên mobile < 5ms
- **Key management**: AWS KMS Customer Managed Key, type `ECC_NIST_P256`, key policy chỉ cho `rule-pack-signer` Lambda role
- **Public key**: pre-pin trong app build (file `assets/public_keys/2026-q2.pem`); rotate qua publish app version mới (semver bump major)
- **Lý do KHÔNG chọn Ed25519**: AWS KMS chưa support Ed25519 native (2026-05). Tự host KMS hoặc dùng external HSM tăng cost vận hành ~20–40 USD/tháng và tăng attack surface — không đáng cho lợi ích perf marginal trên mobile (Ed25519 verify ~3ms vs ECDSA ~5ms, không user-perceptible)
- **Lý do KHÔNG chọn RSA-2048**: signature 256 bytes (3.6× ECDSA), verify chậm hơn, không có lợi ích bù trừ

**Cross-reference**: Mọi tài liệu khác (Section 08 ADR-007, Section 10 mục 10.5, Section 14 Q-LEG-07) đã thống nhất ECDSA P-256.

### 6.6.2. Canonical JSON

Trước khi sign, JSON phải canonicalize để tránh sign-mismatch do whitespace/order:

- Sort key alphabetically tại mọi level
- No whitespace
- UTF-8 encoding
- Loại field `signature` ra khỏi canonical body (tránh circular)

Dùng spec **JCS (JSON Canonicalization Scheme, RFC 8785)**.

### 6.6.3. Distribution

```
S3 bucket: omiscan-{env}-rulepacks/
├── manifest.json              # current pointer (TTL 5 phút trên CloudFront)
├── packs/
│   ├── 2026.05.10.json        # signed pack
│   ├── 2026.05.15.json
│   └── ...
└── public_keys/
    └── 2026-q2.pem            # public key, app sẽ pre-pin

manifest.json:
{
  "current_version": "2026.05.15",
  "min_app_version": "1.0.0",
  "rollback_to": null,
  "updated_at": "2026-05-15T10:30:00Z"
}
```

CloudFront caches `manifest.json` với TTL 5 phút. Khi publish mới → invalidation API.

### 6.6.4. Mobile pull strategy

| Trigger | Khi nào |
|---|---|
| App startup | Mỗi lần mở app, check manifest |
| Periodic | 6h interval khi app foreground hoặc background fetch |
| Silent push | Khi P3 publish urgent rule pack, server đẩy FCM/APNs silent → app pull ngay |
| Manual | User vào Settings → Refresh rule pack |

Mobile app keep **2 pack gần nhất** trong storage:
- `current` = đang active
- `previous` = fallback nếu `current` invalid signature hoặc parse error

### 6.6.5. Bundled fallback

App đóng gói pack mặc định trong `assets/rule_packs/bundled.json` — version 0.0.0, dùng khi:
- Lần đầu mở app, chưa kết nối mạng
- CDN down, app không pull được
- Verify signature fail trên cả `current` và `previous`

---

## 6.7. Mobile rule application

### 6.7.1. Khi nào mobile áp rule

Sau khi M3 (AI pipeline) trả ra JSON cấu trúc, **trước khi cho user nhấn Submit**, app áp rule pack local để hiển thị **preview compliance**. Mục đích:

- Cảnh báo user "thiếu trường bắt buộc" trước khi submit
- Giảm tải server review queue
- UX feedback nhanh

**Lưu ý**: Mobile preview KHÔNG phải authoritative. Server-side validation engine (M6) là single source of truth — vì rule pack có thể đã update giữa lúc mobile cache với lúc server validate.

### 6.7.2. Implementation Flutter

```dart
class MobileRuleEngine {
  final List<Rule> _rules;

  Future<ValidationResult> validate(ProductSubmission submission) async {
    final results = <RuleResult>[];
    for (final rule in _rules) {
      if (!_scopeMatches(rule.scope, submission)) continue;
      final passed = await _evaluatePredicate(rule.predicate, submission);
      if (!passed) {
        results.add(RuleResult(
          ruleId: rule.id,
          severity: rule.severity,
          message: _renderMessage(rule.messageTemplate, submission),
          legalRef: rule.legalRef,
        ));
      }
    }
    return ValidationResult(failed: results);
  }

  bool _scopeMatches(RuleScope scope, ProductSubmission s) {
    if (scope.productCategories != null &&
        !scope.productCategories!.contains(s.category)) return false;
    if (scope.productSubtypes != null &&
        !s.subtypes.any(scope.productSubtypes!.contains)) return false;
    if (scope.isImported != null && scope.isImported != s.isImported) return false;
    return true;
  }

  Future<bool> _evaluatePredicate(JsonLogicExpr expr, ProductSubmission s) {
    return JsonLogicEvaluator.evaluate(expr, s.toJson());
  }
}
```

### 6.7.3. UI hiển thị rule violation

Trong UC-04 (Edit JSON trước submit), nếu có rule fail:

- **Violation** (màu đỏ): "Submit có thể bị hệ thống flag review. Vui lòng bổ sung [field X]"
- **Warning** (màu vàng): "Cảnh báo: [message]. Bạn vẫn có thể submit"
- **Advisory** (màu xanh dương): "Nhận xét dinh dưỡng: [message]"

User thấy danh sách rule fail + nút "Submit ngay" hoặc "Sửa lại". Nếu chọn "Submit ngay" với violation → confirmation dialog.

---

## 6.8. Server validation engine (M6)

### 6.8.1. Đặc điểm khác mobile

| Thuộc tính | Mobile | Server |
|---|---|---|
| Authoritative? | Không | **Có** |
| Khi nào chạy | Ngay sau M3 | Khi nhận `submission.created` event |
| Output | UI hint cho user | Lưu vào `submissions.validation_result` + trigger workflow review |
| Rule pack cache | App SQLite | Lambda in-memory (5 phút TTL) |
| Latency target | < 200ms | < 1s |

### 6.8.2. Pseudocode Lambda M6

```typescript
import { eventBridge, s3, rds } from './aws-clients';
import { JsonLogic, OmiExtensions } from './rule-engine';
import { verifyEcdsa } from './crypto';

const RULE_PACK_CACHE = new Map<string, RulePack>();
const CACHE_TTL_MS = 5 * 60 * 1000;

export const handler = async (event: SubmissionCreatedEvent) => {
  const submission = await rds.getSubmission(event.submissionId);

  // 1. Load + verify rule pack
  let pack = RULE_PACK_CACHE.get('current');
  if (!pack || isExpired(pack, CACHE_TTL_MS)) {
    const manifest = await s3.getJson('omiscan-rulepacks/manifest.json');
    pack = await s3.getJson(`omiscan-rulepacks/packs/${manifest.current_version}.json`);
    if (!verifyEcdsa(pack)) {
      log.error('Rule pack signature invalid', { version: pack.pack_version });
      throw new Error('INVALID_RULE_PACK_SIGNATURE');
    }
    RULE_PACK_CACHE.set('current', pack);
  }

  // 2. Apply rules
  const failed: RuleResult[] = [];
  for (const rule of pack.rules) {
    if (!rule.enabled) continue;
    if (!scopeMatches(rule.scope, submission)) continue;
    const passed = JsonLogic.evaluate(rule.predicate, submission);
    if (!passed) {
      failed.push({
        rule_id: rule.id,
        severity: rule.severity,
        message: renderMessage(rule.message_template, submission),
        legal_ref: rule.legal_ref,
      });
    }
  }

  // 3. Persist result
  const validationResult = {
    pack_version: pack.pack_version,
    evaluated_at: new Date().toISOString(),
    failed_rules: failed,
    counts: {
      violation: failed.filter(r => r.severity === 'violation').length,
      warning: failed.filter(r => r.severity === 'warning').length,
      advisory: failed.filter(r => r.severity === 'advisory').length,
    },
  };

  const newStatus = validationResult.counts.violation > 0 || validationResult.counts.warning > 0
    ? 'pending_review'
    : 'approved';

  await rds.updateSubmission(submission.id, {
    validation_result: validationResult,
    status: newStatus,
  });

  // 4. Audit log
  await rds.insertAuditLog({
    user_id: 'system:validation-engine',
    action: 'validate',
    entity_type: 'submission',
    entity_id: submission.id,
    after: validationResult,
  });

  // 5. Notify if needs review
  if (newStatus === 'pending_review') {
    await eventBridge.publish('submission.needs_review', { submissionId: submission.id });
  }
};
```

---

## 6.9. Test strategy cho rule engine

### 6.9.1. 3 lớp test

| Layer | Mục tiêu | Tool |
|---|---|---|
| **Unit** | Mỗi rule evaluate đúng cho input cụ thể | Jest (TS), Dart test (mobile) |
| **Cross-platform parity** | Cùng input → cùng output trên mọi runtime | Shared fixture JSON, run trên cả TS + Dart |
| **Integration** | Rule pack → áp lên submission thật → kết quả đúng | Postgres test DB, fixture submissions |

### 6.9.2. Shared fixture format

```json
// test/fixtures/rule_evaluations.json
[
  {
    "id": "tc-001",
    "rule_id": "TT29-E5-1-NUTRITION-5-CORE",
    "input": {
      "category": "thực_phẩm",
      "fields": {
        "nutrition_per_100g": {
          "kcal": 245, "protein_g": 6.5, "carb_g": 28.0, "fat_g": 12.3
          // sodium_mg missing
        }
      }
    },
    "expected": {
      "passed": false,
      "message_contains": "Thiếu chỉ tiêu dinh dưỡng cơ bản"
    }
  },
  ...
]
```

Cả TypeScript runner và Dart runner đọc cùng fixture, assert kết quả khớp 100%.

### 6.9.3. Pre-publish test gauntlet

Khi P3 submit rule pack for approval, hệ thống tự động chạy:

1. **Schema validation**: rule pack đúng schema 1.0 không
2. **JsonLogic syntax check**: predicate parse được không
3. **Test set evaluation**: chạy trên 50 submission gần nhất, log kết quả
4. **No-op detection**: rule nào không ảnh hưởng (passed 100% submissions) — flag cảnh báo
5. **Catastrophic flag**: rule nào fail > 90% submission — flag warning to approver
6. **Performance test**: tổng thời gian evaluate 50 submission ≤ 5s

Approver thấy report này trước khi accept.

### 6.9.4. Property-based test

Dùng `fast-check` (TS) và `glados` (Dart) để generate input random, assert invariants:

- "Rule disabled không bao giờ tạo failed result"
- "Scope không match → rule không evaluate (skip)"
- "Cùng input → cùng output deterministic"

---

## 6.10. Performance

### 6.10.1. Mobile

- 1 submission × ~20–30 rules áp dụng (sau scope filter) ≈ ~50 predicate evaluation
- Mỗi evaluation < 1ms (JsonLogic interpreter rất nhẹ)
- **Tổng < 100ms** trên mobile flagship

### 6.10.2. Server

- Lambda cold start ~500ms (Node.js 22 + 512MB)
- Rule pack load từ cache: 0ms; từ S3: ~200ms với verify
- Evaluate ~50 rule × scope filter: < 50ms
- DB write: < 100ms với connection pooling
- **Tổng < 1s p95** sau warm

### 6.10.3. Optimization khi cần

Với volume hiện tại (3.300/tháng = ~5/giờ), KHÔNG cần optimize. Khi scale 100×:

- Pre-compile predicate (turn JsonLogic into JS function trên server)
- Index rule theo scope để filter nhanh hơn
- Materialized view cho dashboard query

---

## 6.11. Open questions

| # | Câu hỏi | Cần giải quyết |
|---|---|---|
| Q1 | Rule có đa ngôn ngữ message không (vi/en)? | Có khả năng cần — Phase 2 |
| Q2 | Rule có thể chạy trên ảnh raw (không qua VLM) không, ví dụ "kích thước font tên ≥ font khác" theo Điều 43? | Khó — cần OCR bbox info, defer Phase 2 |
| Q3 | Đã chốt ECDSA P-256 v0.2.1 (KMS native). Q3 này resolved. | Spike tuần 2 |
| Q4 | Rule có thể trigger external action (gọi API Cục ATTP cross-check) không? | Phase 2+ |
| Q5 | Nên có quota số rule/pack max (ví dụ 200) không? | Quyết định khi pack có > 100 rule |

---

## 6.12. Cross-references

- Văn bản gốc: [legal_sources/29_2023_TT-BYT.md](legal_sources/29_2023_TT-BYT.md), [legal_sources/37_2026_ND-CP.md](legal_sources/37_2026_ND-CP.md)
- Khung pháp lý: [04_legal_framework.md](04_legal_framework.md)
- M6 module spec: [05_functional_spec.md](05_functional_spec.md) mục 5.7
- M8 CMS spec: [05_functional_spec.md](05_functional_spec.md) mục 5.9
- Architecture: [08_architecture.md](08_architecture.md) ADR-007 + ADR-009
- Persona P3 (Cán bộ rule pack): [03_personas_usecases.md](03_personas_usecases.md) UC-12 đến UC-14
