Bỏ qua

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:

Kroki

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:

Kroki

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.

Kroki

Vòng 1 — Ghép bằng stable ID

Một cặp được tạo khi:

  1. ID ở Before có trạng thái valid;
  2. ID ở After có trạng thái valid;
  3. 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.

Kroki

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 + namedTitle duy 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

Kroki

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:

Kroki

Đổ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

Kroki

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

Kroki

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}
...
```

Kroki

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
```

Kroki

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

Kroki

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:

Kroki

comparisonSource được tạo từ source và directive render theo các bước:

  • bỏ riêng id vì đâ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ại changed;
  • CRLF thành LF;
  • 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 issues cạ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ó

  1. Tạo một MR baseline chỉ thêm {id=...}, không đổi source, title, engine hay render param.
  2. Vì nhánh đích chưa có ID, MR baseline dùng matching fallback và hiện migration warning.
  3. Merge baseline vào nhánh chính.
  4. 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

Kroki

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.