GitLab MR Diagram Pairing¶
Tài liệu này mô tả cách browser extension nhận diện và ghép diagram giữa hai phiên bản Markdown Before và After trong GitLab Merge Request.
- Thuật toán ghép cặp:
browser-extension/src/diagram-parser.js. - Thuật toán đặt nút vào GitLab diff:
browser-extension/src/diff-locator.js. - Ghép cặp và đặt nút là hai giai đoạn độc lập.
1. Vì sao cần stable diagram ID?¶
Nếu chỉ dựa vào engine, title hoặc thứ tự, extension có thể ghép nhầm khi người dùng:
- di chuyển diagram sang vị trí khác;
- đổi heading hoặc title;
- thêm một diagram vào giữa nhiều diagram cùng engine;
- đổi renderer, ví dụ PlantUML sang BlockDiag.
id tạo một định danh ổn định, sống cùng diagram qua các lần chỉnh sửa:
```plantuml {id=payment-flow theme=dark scale=2}
@startuml
Customer -> PaymentService
@enduml
```
ID nằm trong cùng {...} với các render param để cú pháp ngắn và thống nhất. Browser extension chuyển toàn bộ
khối này thành %%krokup {...}. Rendering server loại id như một unknown option, ghi IGNORED_OPTION, rồi vẫn
áp dụng các option hợp lệ như theme và scale.
Đây là breaking change của browser extension: chỉ id nằm trong {...} được nhận diện. Cú pháp cũ
diagram-id=... ở ngoài {...} bị bỏ qua và không tham gia ghép cặp.
2. Quy tắc của diagram ID¶
| Quy tắc | Cách xử lý |
|---|---|
| Độ dài | 1–64 ký tự |
| Ký tự hợp lệ | a-z, 0-9, ., _, - |
| Ký tự đầu tiên | Chữ hoặc số |
| Chữ hoa/chữ thường | Chuẩn hóa thành lowercase |
| Phạm vi duy nhất | Trong một file Markdown, xét riêng từng phía Before/After |
| Có bắt buộc không? | Không; Markdown cũ vẫn dùng các matching fallback |
Ví dụ, {id=Payment-Flow} được chuẩn hóa thành payment-flow.
Mỗi diagram nhận một trạng thái ID:
Một ID xuất hiện một lần ở Before và một lần ở After là bình thường. Nó chỉ bị coi là duplicate khi xuất hiện nhiều lần trong cùng phía của cùng file.
3. Dữ liệu nhận diện một diagram¶
Parser tạo một bản ghi cho mỗi fenced code block được hỗ trợ:
| Trường | Ý nghĩa |
|---|---|
diagramId |
ID đã trim và lowercase |
diagramIdStatus |
missing, valid, invalid hoặc duplicate |
engine |
Renderer sau khi chuẩn hóa alias, ví dụ puml thành plantuml |
title |
Tên hiển thị được chọn theo thứ tự ưu tiên mô tả bên dưới |
identity |
Composite legacy identity gồm engine + namedTitle đã chuẩn hóa; không dùng title mặc định Diagram N |
source |
Nội dung nguyên bản bên trong code fence |
directive |
Nội dung trong {...}, gồm id và các render param |
renderSource |
Payload render được tổng hợp từ directive đầy đủ và source |
comparisonSource |
Source so sánh gồm các render param nhưng đã loại riêng id |
startLine, endLine |
Khoảng dòng dùng để đặt action trong GitLab diff |
Title được chọn theo đúng thứ tự ưu tiên của parser:
Metadata dạng text thuần chỉ được dùng làm title khi phần metadata không chứa =, { hoặc }.
Ví dụ, metadata Checkout sequence sau ngôn ngữ plantuml tạo title Checkout sequence, còn
metadata {id=checkout} không được lấy làm title. Title nhận diện từ source gồm tên sau
@startuml, @startmindmap, @startwbs, hoặc dòng title:.
4. Thuật ngữ ghép cặp¶
Tài liệu và UI dùng các thuật ngữ sau:
| Thuật ngữ | Nghĩa chính xác |
|---|---|
| Pair | Một kết quả logic có cả diagram Before và After |
Stable-ID matching (pairing = id) |
Ghép hai diagram có cùng id hợp lệ |
Legacy-identity matching (pairing = identity) |
Ghép theo composite engine + namedTitle khi composite đó xuất hiện đúng một lần ở mỗi phía |
Same-engine occurrence fallback (pairing = occurrence) |
Duyệt After theo thứ tự và ghép với diagram Before chưa dùng đầu tiên có cùng engine; đây là fallback tham lam, không phải đối sánh tối ưu theo vị trí dòng |
Unpaired (pairing = unpaired) |
Không tìm được diagram đối ứng; kết quả một phía sẽ có status added hoặc removed |
| Status | Kết quả so sánh sau khi ghép: changed, unchanged, added hoặc removed; status độc lập với chiến lược pairing |
| Matching fallback | Hai chiến lược độ tin cậy thấp hơn stable ID: legacy identity, sau đó same-engine occurrence |
Trong tài liệu này, title duy nhất không đủ để ghép. Chỉ composite engine + namedTitle duy nhất
ở cả hai phía mới tạo được legacy identity pair. Từ fallback trong phần này luôn nói về matching;
nó không phải header fallback dùng khi GitLab chưa hiển thị diff row để đặt nút preview.
5. Thuật toán ghép cặp¶
Extension chạy ba vòng ghép theo độ tin cậy giảm dần.
Vòng 1 — Ghép bằng stable ID¶
Một cặp được tạo khi:
- ID ở Before có trạng thái
valid; - ID ở After có trạng thái
valid; - hai ID giống nhau sau khi lowercase.
Kết quả có pairing = id và không phụ thuộc vào title, vị trí hoặc engine.
Vòng 2 — Ghép bằng legacy identity¶
Các diagram chưa được ghép ở vòng 1 có thể ghép bằng engine + namedTitle khi identity đó xuất hiện đúng
một lần ở mỗi phía.
Vòng này phục vụ:
- Markdown cũ chưa có ID;
- PR baseline mới thêm ID ở một phía;
- duplicate/invalid ID nhưng vẫn còn composite
engine + namedTitleduy nhất ở cả hai phía.
Hai diagram đều có ID valid nhưng khác nhau bị chặn ở vòng này, kể cả khi title và engine giống hệt.
Vòng 3 — Same-engine occurrence fallback¶
Extension duyệt các diagram After chưa ghép theo thứ tự xuất hiện. Mỗi diagram được ghép với diagram Before chưa dùng đầu tiên có cùng engine. Đây là phép ghép tham lam theo thứ tự duyệt, không so sánh khoảng cách dòng và không tìm phương án ghép tối ưu toàn cục.
Same-engine occurrence fallback bị cấm khi một trong hai diagram có ID duplicate hoặc invalid, hoặc khi cả hai
có ID valid. Nếu có nhiều ứng viên cùng engine ở một phía, kết quả được đánh dấu ambiguous và UI
hiện cảnh báo.
6. Cây quyết định cho một diagram After¶
Sau khi xử lý hết After, mọi diagram Before chưa được ghép trở thành removed.
7. Các case và kết quả¶
| Case | Before | After | Kết quả |
|---|---|---|---|
| 1. Cùng ID, engine và source không đổi | valid: flow |
valid: flow |
Ghép bằng ID, unchanged |
| 2. Cùng ID, source hoặc render param đổi | valid: flow |
valid: flow |
Ghép bằng ID, changed |
| 3. Cùng ID, đổi vị trí/title | valid: flow |
valid: flow |
Vẫn là một cặp |
| 4. Cùng ID, đổi engine | PlantUML flow |
BlockDiag flow |
Vẫn ghép bằng ID và được phân loại changed; panel ghi rõ hai engine |
| 5. Hai valid ID khác nhau | flow-v1 |
flow-v2 |
Không dùng matching fallback: một added, một removed |
| 6. Chỉ một phía có ID | Không ID | valid: flow |
Matching fallback; migration = true nếu ghép được |
| 7. Duplicate ID, legacy identity duy nhất | Hai duplicate: flow có composite identity khác nhau |
Tương tự | Có thể ghép theo unique engine + namedTitle và hiện cảnh báo |
| 8. Duplicate ID, không có legacy identity duy nhất | Nhiều duplicate: flow |
Nhiều duplicate: flow |
Không dùng same-engine occurrence fallback; tách added/removed |
| 9. Invalid ID, legacy identity duy nhất | ID sai định dạng | ID sai định dạng | Có thể ghép theo unique engine + namedTitle và hiện cảnh báo |
| 10. Không có ID, legacy identity duy nhất | Cùng engine/named title | Cùng engine/named title | Legacy-identity matching |
| 11. Không có ID/named title, một diagram cùng engine | Diagram thứ nhất | Diagram thứ nhất | Same-engine occurrence fallback |
| 12. Không có ID/named title, nhiều diagram cùng engine | Nhiều diagram | Nhiều diagram | Same-engine occurrence fallback với ambiguous = true |
| 13. Chỉ có ở After | Không tồn tại | Diagram mới | added |
| 14. Chỉ có ở Before | Diagram cũ | Không tồn tại | removed |
Case 1–4: Cùng stable ID¶
Stable ID được ưu tiên trước hai matching fallback:
Đổi renderer không phá vỡ quan hệ cặp. Engine là một phần của kết quả so sánh,
vì vậy chỉ cần engine thay đổi thì cặp được phân loại changed và action diff
vẫn xuất hiện, kể cả khi comparisonSource giống nhau.
Case 5: Hai valid ID khác nhau¶
Extension coi đổi ID là thay thế định danh, không phải đổi nội dung của cùng một diagram. Điều này ngăn title hoặc vị trí vô tình kéo hai diagram khác nhau vào cùng một cặp.
Case 6: Migration một phía có ID¶
Nếu PR vừa thêm ID vừa đổi engine, legacy identity và same-engine occurrence fallback đều không thể ghép vì hai cơ chế này yêu cầu cùng engine. Để migration an toàn, nên tạo MR baseline chỉ thêm ID trước.
Case 7–8: Duplicate ID¶
Ví dụ lỗi:
## Login flow
```plantuml {id=shared-flow}
...
```
## Payment flow
```d2 {id=shared-flow}
...
```
Extension không tự chọn diagram đầu tiên mang ID trùng, vì lựa chọn đó có thể tạo diff sai nhưng trông vẫn hợp lệ. Người dùng phải sửa ID để khôi phục stable matching.
Case 9: Invalid ID¶
ID được đánh dấu invalid khi có id trong {...} nhưng giá trị không thỏa quy tắc độ dài hoặc ký tự.
Ví dụ sau không hợp lệ vì chứa khoảng trắng:
```plantuml {id="payment flow"}
@startuml
Customer -> PaymentService
@enduml
```
Invalid ID không được dùng để stable-ID matching. Extension vẫn có thể ghép bằng legacy identity nếu
composite engine + namedTitle là duy nhất ở cả Before và After; panel đồng thời hiển thị cảnh báo ID.
Nếu không có legacy identity duy nhất, extension không đoán tiếp theo thứ tự xuất hiện vì occurrence
fallback bị chặn khi một phía có ID invalid. Người dùng cần sửa ID về định dạng hợp lệ để khôi phục
stable-ID matching.
Case 10–12: Markdown cũ không có ID¶
Khả năng tương thích này giúp repository cũ tiếp tục hoạt động, nhưng không đảm bảo chính xác tuyệt đối khi reorder hoặc chèn diagram giữa nhiều diagram cùng engine.
8. Phân loại trạng thái sau khi ghép¶
Sau khi tạo cặp, extension so sánh cả engine và comparisonSource:
comparisonSource được tạo từ source và directive render theo các bước:
- bỏ riêng
idvì đây là metadata ghép cặp, không phải thuộc tính hình ảnh; - giữ các render param như
theme,scale,backgroundđể thay đổi giao diện vẫn được phân loạichanged; CRLFthànhLF;- khoảng trắng cuối dòng;
- newline thừa ở cuối source.
Khi render, extension dùng renderSource: toàn bộ {...}, bao gồm cả id, được chuyển thành directive
%%krokup {...} và đặt trước source. Nếu source đã có directive %%krokup hoặc %%kroki ở đầu, directive
từ code fence thay thế nó để tránh gửi hai directive. Rendering server bỏ qua id với cảnh báo
IGNORED_OPTION, nhưng vẫn áp dụng các render param hợp lệ.
Do đó, chỉ thay đổi id không làm thay đổi hình ảnh; thay đổi theme, scale, background hoặc source sẽ
làm cặp được phân loại changed. Các cặp unchanged bị loại khỏi danh sách action hiển thị trong tab Changes.
9. Cảnh báo trên giao diện¶
Extension hiển thị cảnh báo ở hai mức:
- Cấp file: badge
Diagram ID issuescạnh nút preview, kèm loại lỗi và số dòng. - Cấp diagram: cảnh báo trong panel khi dùng duplicate/invalid ID, matching fallback khi migration hoặc same-engine occurrence fallback có nhiều ứng viên.
Khi một stable ID ghép hai renderer khác nhau, tiêu đề panel hiển thị dạng plantuml → blockdiag và
mỗi card Before/After ghi rõ engine riêng.
10. Migration repository hiện có¶
- Tạo một MR baseline chỉ thêm
{id=...}, không đổi source, title, engine hay render param. - Vì nhánh đích chưa có ID, MR baseline dùng matching fallback và hiện migration warning.
- Merge baseline vào nhánh chính.
- Các branch mới phải giữ nguyên ID khi sửa source, di chuyển, đổi title hoặc đổi renderer.
Không cần tạo ID mới sau mỗi lần sửa. ID chỉ thay đổi khi diagram cũ thực sự bị thay thế bởi một diagram có danh tính khác.
11. Ghép cặp và placement là hai bước độc lập¶
MutationObserver chạy lại placement khi Rapid Diffs hoặc virtual scrolling thay đổi DOM. Việc này
chỉ di chuyển action tới đúng code block, không chạy lại hoặc thay đổi quyết định ghép cặp.
12. Khuyến nghị sử dụng¶
- Dùng một ID mô tả nghiệp vụ, ví dụ
checkout.payment-sequence. - Không nhúng vị trí hoặc số thứ tự vào ID nếu diagram có thể được di chuyển.
- Không tái sử dụng ID của diagram đã xóa cho một diagram có ý nghĩa khác.
- Khi bổ sung ID cho repository cũ, thực hiện MR baseline riêng.
- Sửa ngay duplicate/invalid warning thay vì dựa lâu dài vào matching fallback.