@educa-corp/sdd-framework 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/bin/build.js +11 -0
  2. package/bin/lint-trace.js +230 -2
  3. package/bin/qc-base-map.json +119 -49
  4. package/bin/self-check.js +54 -0
  5. package/bin/trace-schema.json +58 -4
  6. package/core/FRAMEWORK_VERSION +1 -1
  7. package/core/commands/generate-bdd.md +1 -0
  8. package/core/commands/generate-code.md +39 -2
  9. package/core/commands/generate-tech-docs.md +21 -2
  10. package/core/commands/map-testids.md +88 -8
  11. package/core/commands/qc-analyze.md +429 -472
  12. package/core/commands/qc-design-test.md +251 -207
  13. package/core/commands/qc-plan.md +97 -197
  14. package/core/commands/qc-report.md +76 -60
  15. package/core/commands/qc-review.md +135 -185
  16. package/core/commands/qc-run-test.md +235 -274
  17. package/core/commands/review-tech-docs.md +20 -0
  18. package/core/commands/setup-ai-first.md +5 -5
  19. package/core/commands/update-framework.md +1 -1
  20. package/core/commands/validate-traces.md +1 -1
  21. package/core/modules/qc-playwright/stack-profile.yaml +1 -1
  22. package/core/rules/data-protection.md +52 -0
  23. package/core/rules/workflow.md +1 -1
  24. package/core/skills/qc/_shared/self-review-principles.md +112 -0
  25. package/core/skills/qc/qa-analyst/DOC_GAP.template.md +1 -1
  26. package/core/skills/qc/qa-analyst/spec-breakdown.md +2 -2
  27. package/core/skills/qc/qa-designer/api/auth-chain.md +155 -0
  28. package/core/skills/qc/qa-designer/api/auth-sequence.md +75 -0
  29. package/core/skills/qc/qa-designer/api/common-headers.md +61 -0
  30. package/core/skills/qc/qa-designer/api/crud-sequence.md +122 -0
  31. package/core/skills/qc/qa-designer/api/endpoint.md +231 -0
  32. package/core/skills/qc/qa-designer/api/http-status-codes.md +102 -0
  33. package/core/skills/qc/qa-designer/e2e/journey.md +13 -8
  34. package/core/skills/qc/qa-designer/exploratory/charter.md +2 -0
  35. package/core/skills/qc/qa-designer/exploratory/explore-to-functional.md +7 -4
  36. package/core/skills/qc/qa-designer/functional/api.md +87 -18
  37. package/core/skills/qc/qa-designer/functional/gui-feature.md +12 -9
  38. package/core/skills/qc/qa-designer/functional/gui-screen.md +12 -10
  39. package/core/skills/qc/qa-designer/integration/api.md +12 -5
  40. package/core/skills/qc/qa-designer/integration/db.md +12 -6
  41. package/core/skills/qc/qa-designer/integration/gui.md +12 -5
  42. package/core/skills/qc/qa-designer/integration/kafka.md +12 -5
  43. package/core/skills/qc/qa-designer/non-functional.md +12 -5
  44. package/core/skills/qc/qa-designer/shared/action-keywords-glossary.md +91 -0
  45. package/core/skills/qc/qa-designer/shared/duplicate-check-procedure.md +105 -0
  46. package/core/skills/qc/qa-designer/shared/implicit-scenarios.md +22 -0
  47. package/core/skills/qc/qa-designer/shared/precision-rules.md +198 -0
  48. package/core/skills/qc/qa-designer/shared/read-doc-gap-inputs.md +25 -0
  49. package/core/skills/qc/qa-designer/shared/skill-decision-tree.md +93 -0
  50. package/core/skills/qc/qa-designer/shared/tc-metadata-format.md +243 -0
  51. package/core/skills/qc/qa-planner/risk-model.md +1 -1
  52. package/core/skills/qc/qa-reviewer/script/e2e.md +9 -1
  53. package/core/skills/qc/qa-reviewer/script/exploratory.md +9 -1
  54. package/core/skills/qc/qa-reviewer/script/functional.md +9 -1
  55. package/core/skills/qc/qa-reviewer/script/integration.md +9 -1
  56. package/core/skills/qc/qa-reviewer/script/non-functional.md +9 -1
  57. package/core/skills/qc/qa-reviewer/shared/read-doc-gap-inputs.md +26 -0
  58. package/core/skills/qc/qa-reviewer/shared/review-check-groups.md +207 -0
  59. package/core/skills/qc/qa-reviewer/shared/review-file-template.md +228 -0
  60. package/core/skills/qc/qa-reviewer/test-case/e2e.md +71 -13
  61. package/core/skills/qc/qa-reviewer/test-case/exploratory.md +53 -4
  62. package/core/skills/qc/qa-reviewer/test-case/functional.md +63 -15
  63. package/core/skills/qc/qa-reviewer/test-case/integration.md +64 -12
  64. package/core/skills/qc/qa-reviewer/test-case/non-functional.md +72 -13
  65. package/core/skills/qc/qa-runner/e2e.md +3 -3
  66. package/core/skills/qc/qa-runner/functional/gui-feature.md +9 -3
  67. package/core/skills/qc/qa-runner/functional/gui-screen.md +9 -3
  68. package/core/skills/qc/qa-runner/integration.md +1 -1
  69. package/core/skills/qc/qa-runner/non-functional.md +1 -1
  70. package/core/skills/spec/SKILL.md +1 -1
  71. package/core/steps/context-loader.md +7 -2
  72. package/core/steps/gap-verify.md +67 -0
  73. package/core/steps/report-footer.md +3 -3
  74. package/core/templates/feature.template +1 -0
  75. package/core/templates/tech-design.template.md +1 -0
  76. package/docs/02-concepts/pipeline-steps/09-validate-traces.md +1 -1
  77. package/docs/04-reference/commands.md +1 -1
  78. package/docs/04-reference/trace-schema.md +39 -1
  79. package/docs/explain/00-setup-ai-first.md +1 -1
  80. package/docs/explain/11-map-testids.md +70 -69
  81. package/docs/plans/qc-implementation-log.md +145 -3
  82. package/docs/plans/qc-surgery/00-nhat-ky.md +497 -0
  83. package/docs/plans/qc-surgery/01-checklist.md +92 -0
  84. package/docs/plans/qc-surgery/02-lo-trinh.md +266 -0
  85. package/docs/plans/qc-surgery/buoc/0-01-testid-attr-co-cho-o.md +157 -0
  86. package/docs/plans/qc-surgery/buoc/0-02-mot-nguon-cho-testid-attr.md +135 -0
  87. package/docs/plans/qc-surgery/buoc/0-03-skill-thoi-day-do-dom.md +167 -0
  88. package/docs/plans/qc-surgery/buoc/0-04-may-canh-hop-dong.md +173 -0
  89. package/docs/plans/qc-surgery/buoc/0-05-don-nhan-cot-va-2b.md +133 -0
  90. package/docs/plans/qc-surgery/buoc/0-06-hop-dong-truoc-code.md +226 -0
  91. package/docs/plans/qc-surgery/buoc/1-01-guard-br-tag.md +156 -0
  92. package/docs/plans/qc-surgery/buoc/1-02-guard-sc-coverage.md +153 -0
  93. package/docs/plans/qc-surgery/buoc/1-03-fail-3-nhan.md +176 -0
  94. package/docs/plans/qc-surgery/buoc/1-04-self-review-dung-chung.md +175 -0
  95. package/docs/plans/qc-surgery/buoc/1-05-spec-la-du-lieu.md +164 -0
  96. package/docs/plans/qc-surgery/buoc/1-06-gap-verify-du-bo.md +162 -0
  97. package/docs/plans/qc-surgery/buoc/README.md +85 -0
  98. package/docs/plans/qc-surgery/exec-d0-b1-testid-attr-header.md +147 -0
  99. package/docs/plans/qc-surgery/exec-d0-b2-thong-nhat-nguon-testid-attr.md +152 -0
  100. package/docs/plans/qc-surgery/exec-d0-b3-sua-skill-probe-dom.md +173 -0
  101. package/docs/plans/qc-surgery/exec-d0-b4-may-canh-4-5-6.md +168 -0
  102. package/docs/plans/qc-surgery/exec-d0-b5-don-nhan-lech.md +196 -0
  103. package/docs/plans/qc-surgery/exec-d0-b6-contract-truoc-code.md +350 -0
  104. package/docs/plans/qc-surgery/exec-d1-b1-guard-br-tag.md +129 -0
  105. package/docs/plans/qc-surgery/exec-d1-b2-guard-sc-coverage.md +159 -0
  106. package/docs/plans/qc-surgery/exec-d1-b3-fail-3-bucket.md +158 -0
  107. package/docs/plans/qc-surgery/exec-d1-b4-self-review-principles.md +145 -0
  108. package/docs/plans/qc-surgery/exec-d1-b5-noi-quy-spec-la-du-lieu.md +156 -0
  109. package/docs/plans/qc-surgery/exec-d1-b6-gap-verify-mo-rong.md +179 -0
  110. package/docs/plans/qc-surgery/exec-d2-b1-tach-qc-review.md +166 -0
  111. package/docs/plans/qc-surgery/exec-d2-b2-tach-qc-run-test-atomic.md +267 -0
  112. package/docs/plans/qc-surgery/exec-d2-b3-qc-automation-assess.md +198 -0
  113. package/docs/plans/qc-surgery/exec-d3-b1-qc-report-gate-decision.md +209 -0
  114. package/docs/plans/qc-surgery/exec-d4-b1-qc-design-testdata.md +146 -0
  115. package/docs/plans/qc-surgery/exec-d4-b2-qc-smoke-test.md +179 -0
  116. package/docs/plans/qc-surgery/exec-d4-b3-qc-metrics-va-lint.md +198 -0
  117. package/docs/plans/qc-surgery/exec-d4-b4-lint-spec-injection.md +199 -0
  118. package/package.json +1 -1
@@ -0,0 +1,173 @@
1
+ ---
2
+ buoc: Đợt 0 — Bước 3
3
+ title: Sửa 2 skill qa-runner đang dạy dò DOM ngược với command
4
+ phu_thuoc: không (chạy song song b1/b2 được)
5
+ trang_thai: đã làm
6
+ ---
7
+
8
+ # Đợt 0 · Bước 3 — Command nói "đừng dò DOM", skill nói "dò DOM đi"
9
+
10
+ ← [`01-checklist.md`](01-checklist.md) · [`02-lo-trinh.md`](02-lo-trinh.md)
11
+
12
+ > ✅ **Đã làm rồi.** File này là bản **ĐỊNH làm**. Cái **ĐÃ xảy ra** — gồm chỗ lệch
13
+ > so với kế hoạch này và lý do — ghi ở [`buoc/0-03-skill-thoi-day-do-dom.md`](buoc/0-03-skill-thoi-day-do-dom.md).
14
+
15
+ > **Đây là nguyên nhân gốc của câu hỏi chị QC** *("có thể dùng prompt trích xuất element/locator
16
+ > từ source code thành 1 file không?")*. Đội QC tin là phải tự dò selector, vì **framework đang
17
+ > dạy họ làm vậy**.
18
+
19
+ ## 1. Vì sao
20
+
21
+ Framework có hai tầng chỉ dẫn cho cùng một việc:
22
+
23
+ ```
24
+ commands/qc-run-test.tmpl ← lệnh, agent đọc đầu tiên
25
+ └─ nạp ──→ skills/qc/qa-runner/functional/gui-screen.md ← skill, agent đọc khi làm
26
+ ```
27
+
28
+ Lệnh nói: lấy locator từ hợp đồng, **không** dò runtime.
29
+ Skill nói: **dò DOM thật trước khi viết selector**.
30
+
31
+ Agent đọc lệnh rồi nạp skill để làm việc. Skill là thứ nó đọc **sau** và **gần lúc viết code
32
+ nhất** — nên skill thắng. Kết quả: QC dò DOM, ghi selector kiểu `.login-form__submit`, và mọi
33
+ người kết luận "framework không có gì để dựa vào".
34
+
35
+ Cái giá của việc dò DOM: dev đổi tên class là test vỡ. Mà dev **không hề hứa** giữ tên class —
36
+ họ chỉ hứa giữ `data-testid` (nếu có hợp đồng). Selector dò được là selector không ai bảo hành.
37
+
38
+ ## 2. Tình trạng hiện tại
39
+
40
+ **Lệnh nói không dò** — `commands/qc-run-test.tmpl:43`:
41
+
42
+ > *"**Locator từ test-id contract (không scan runtime).** Đọc bảng *Test Selectors* §4.5.6
43
+ > (block platform) của tech-doc gộp … và dựng mỗi Page Object locator từ test-id ổn định của
44
+ > nó. **Ưu tiên map; fallback** về role/label/text/CSS (scan chậm hơn) chỉ cho một element có
45
+ > action mà **không** có test-id trong §4.5.6 — và ghi chú để gap được thêm vào tech-design."*
46
+
47
+ **Skill nói dò** — `skills/qc/qa-runner/functional/gui-screen.md:26`:
48
+
49
+ > *"**Probe DOM thật trước khi viết selector** (SPA không `data-testid`): dump class/
50
+ > `aria-label`/role bằng script Playwright tạm → ghi selector đúng (BEM `feature__el`; element
51
+ > interactive có thể `role="tab/menuitem"` + class `--active`)."*
52
+
53
+ **Và skill thứ hai cũng vậy** — `skills/qc/qa-runner/functional/gui-feature.md:25`:
54
+
55
+ > *"**Probe DOM thật trước khi viết selector** (SPA React/Next không `data-testid`): dump class/
56
+ > `aria-label`/role → BEM `feature__el`, carousel dot thường `role="tab"` + class `--active`
57
+ > (không `aria-selected`)."*
58
+
59
+ Cả hai câu nằm ở **Phase 1 — Clarify**, tức là **bước đầu tiên** của việc viết script. Không
60
+ phải fallback, không phải "nếu thiếu test-id thì" — mà là việc đầu tiên phải làm.
61
+
62
+ Chú ý cụm trong ngoặc: *"(SPA không `data-testid`)"*. Câu này được viết cho **hoàn cảnh không có
63
+ hợp đồng test-id**, và ở hoàn cảnh đó nó **hợp lý**. Vấn đề là nó được viết như **mặc định**,
64
+ không như ngoại lệ — nên nó áp cả cho dự án đã có §4.5.6 đầy đủ.
65
+
66
+ ## 3. Sẽ đổi thành gì
67
+
68
+ Đảo thứ tự, giữ lại kiến thức kỹ thuật (phần BEM / `role="tab"` là kinh nghiệm thật, đáng giữ),
69
+ nhưng đặt nó đúng vị trí là **fallback có ghi chú**:
70
+
71
+ ```
72
+ Phase 1 — Clarify
73
+ Đọc `.Test.md` (confirm Reviewed) · platform · Page Object đã có chưa · fixture setup data?
74
+
75
+ **Locator: đọc hợp đồng TRƯỚC, dò DOM là bước cuối.**
76
+ 1. Mở bảng §4.5.6 Test Selectors của tech-doc gộp, lọc theo cột "Serves SC" khớp SC của UC này
77
+ → dựng locator từ test-id. Đọc `@trace.testid_attr` ở header để biết TÊN thuộc tính.
78
+ 2. Element có action mà §4.5.6 KHÔNG có test-id → thử role + accessible name
79
+ (`getByRole('button', { name: 'Đăng nhập' })`).
80
+ 3. Vẫn không định vị được → MỚI dò DOM (dump class/`aria-label`/role bằng script Playwright
81
+ tạm; BEM `feature__el`; element interactive có thể `role="tab/menuitem"` + class `--active`).
82
+ Mỗi lần phải dò tới bước này là **ghi một gap**: element nào, màn nào, thiếu test-id.
83
+ Đề nghị chạy `/map-testids {UC-ID}` để dev bổ sung, đừng im lặng sống với selector giòn.
84
+ ```
85
+
86
+ Điểm quan trọng của cách viết mới: **dò DOM không bị cấm** — nó vẫn cần cho dự án brownfield
87
+ chưa có hợp đồng. Nhưng mỗi lần dò là một **tín hiệu được ghi lại**, thay vì một thói quen. Nhờ
88
+ vậy số lần dò giảm dần theo thời gian (dev bổ sung test-id), chứ không giữ nguyên mãi.
89
+
90
+ Ở pipeline mới (Đợt 2), gap này thành `IMPROVE-{date}-{NN}` và được đẩy về spec repo cho dev —
91
+ nhưng câu chữ ở bước này chưa cần nhắc `IMPROVE`, vì lệnh đó chưa tồn tại.
92
+
93
+ ## 3b. Ba ca dễ bị gộp nhầm — bậc 3 CHỈ dành cho ca B
94
+
95
+ Câu hỏi trong lúc review bước này: *"tự select locator này sẽ phục vụ QC trong việc có code, lấy
96
+ test-id nhưng trong bảng chưa có, đúng không?"* — **gần đúng, nhưng đó là ca A, không phải việc
97
+ của bậc 3.** Ba ca đi ba đường khác nhau:
98
+
99
+ | | Code có test-id? | §4.5.6 có? | Đường đúng |
100
+ |---|:---:|:---:|---|
101
+ | **A** | ✅ **có** | ❌ chưa | **`/map-testids`** — nhánh `existing` (`map-testids.tmpl:40`): *"Nếu element **đã** có test-id → **reverse-document** nó (dùng lại as-is)"*. Đưa id vào bảng rồi quay lại **bậc 1** |
102
+ | **B** | ❌ **không có gì** (chỉ class/role) | ❌ chưa | **Bậc 3** — dò DOM, ghi gap, đề nghị dev gắn test-id |
103
+ | **C** | ✅ có | ✅ có nhưng **LỆCH** | Không phải bậc 3 — là ca đối chiếu, người quyết bên nào đúng ([`exec-d0-b6`](exec-d0-b6-contract-truoc-code.md) §3.7) |
104
+
105
+ **Vì sao phải tách A khỏi B.** Bản nháp đầu của bước này viết bậc 3 thành một khối, nên nó sẽ
106
+ **nuốt luôn ca A mà không ai biết**:
107
+
108
+ ```
109
+ §4.5.6 rỗng → bậc 1 trượt → bậc 2 trượt → bậc 3 dump DOM
110
+ → thấy data-testid="login-submit-btn" → dùng luôn
111
+ ```
112
+
113
+ Trông có vẻ ổn (script vẫn bám test-id ổn định), nhưng mất ba thứ:
114
+
115
+ 1. **Id không bao giờ vào §4.5.6** → lần sau lại phải đi khám phá lại từ đầu.
116
+ 2. **Không ai review id đó** → nó không thành hợp đồng, dev chưa hề hứa giữ nó.
117
+ 3. **Ca A bị nguỵ trang thành ca B** → nhìn script không phân biệt được "id từ hợp đồng" với
118
+ "id nhặt từ DOM".
119
+
120
+ Nên bậc 3 tách thành **3a** (đã có test-id → DỪNG, đẩy ngược về `/map-testids`) và **3b** (không
121
+ có gì → dùng class/role + ghi gap). Khác biệt thực tế: 3a **đẩy việc ngược về hợp đồng**, 3b
122
+ chấp nhận selector giòn nhưng để lại dấu vết.
123
+
124
+ > **Về thời điểm:** ở bước này `/map-testids` chưa có cờ `--from-code` (cờ đó thuộc
125
+ > [`exec-d0-b6`](exec-d0-b6-contract-truoc-code.md) §3.3), nhưng nhánh xử lý ca A **đã tồn tại
126
+ > sẵn** trong lệnh (`map-testids.tmpl:34-40`, phân loại `reused`/`existing`/`new`). Nên skill
127
+ > trỏ `/map-testids {UC-ID}` chung chung là chạy đúng ngay bây giờ; b6 sẽ làm nó tường minh hơn.
128
+
129
+ ## 4. Sửa file nào
130
+
131
+ | File | Sửa gì |
132
+ |---|---|
133
+ | `skills/qc/qa-runner/functional/gui-screen.md` | Dòng `:26` — viết lại Phase 1 theo thứ tự 3 bậc ở trên |
134
+ | `skills/qc/qa-runner/functional/gui-feature.md` | Dòng `:25` — tương tự |
135
+
136
+ Rà thêm cho chắc (grep, sửa nếu cùng lỗi):
137
+
138
+ ```bash
139
+ grep -rn "Probe DOM\|probe DOM\|dump class" skills/qc/
140
+ grep -rn "data-testid" skills/qc/qa-designer/ skills/qc/qa-reviewer/
141
+ ```
142
+
143
+ Hai chỗ đã biết là **không** cùng lỗi, chỉ là tiêu chí review, giữ nguyên:
144
+ - `skills/qc/qa-reviewer/script/functional.md:34` — *"XPath dài/phức tạp → đề xuất data-testid
145
+ / role"*
146
+ - `skills/qc/qa-reviewer/shared/review-check-groups.md:110` — *"định vị element bằng chỉ số /
147
+ vị trí (`nth(3)`) thay vì test-id → `[FLAKY_RISK]`"*
148
+
149
+ ## 5. Kiểm thế nào để biết đã xong
150
+
151
+ ```bash
152
+ # 1. Không còn chỉ dẫn dò DOM ở vị trí "việc đầu tiên"
153
+ grep -n "Probe DOM" skills/qc/qa-runner/functional/gui-screen.md skills/qc/qa-runner/functional/gui-feature.md
154
+ # → nếu còn, phải nằm ở bậc 3 của thứ tự ưu tiên, kèm câu "ghi một gap"
155
+
156
+ # 2. Build + máy tự kiểm (R16(d) canh ref skill vẫn tồn tại)
157
+ node bin/build.js && node bin/self-check.js && node test/run.js
158
+ ```
159
+
160
+ Kiểm bằng tay — trên một UC **đã có** §4.5.6 đầy đủ:
161
+
162
+ Chạy `/qc-run-test {UC-ID}` và đọc script sinh ra. Locator phải là `getByTestId('...')` với giá
163
+ trị **trùng khớp** bảng §4.5.6. Nếu thấy `page.locator('.some-class')` hoặc thấy agent chạy
164
+ script dump DOM trong khi §4.5.6 có sẵn id cho element đó → chưa xong.
165
+
166
+ ## 6. Nếu bỏ qua thì hỏng gì
167
+
168
+ Bước 1 và 2 sửa được **hợp đồng**, nhưng QC vẫn không dùng hợp đồng — vì skill vẫn bảo dò DOM.
169
+ Tức là làm b1 + b2 mà bỏ b3 thì đầu tư vào hợp đồng không thu lại gì: §4.5.6 đầy đủ, header có
170
+ `@trace.testid_attr`, và script vẫn bám class BEM.
171
+
172
+ Xa hơn: Đợt 2 sinh script tự động ở quy mô lớn hơn (`/qc-design-script` cho cả web + mobile).
173
+ Nếu thói quen dò DOM còn nguyên, ta chỉ đang **nhân bản selector giòn nhanh hơn**.
@@ -0,0 +1,168 @@
1
+ ---
2
+ buoc: Đợt 0 — Bước 4
3
+ title: Thêm máy canh cho bảng §4.5.6 Test Selectors
4
+ phu_thuoc: d0-b1, d0-b2
5
+ trang_thai: đã làm
6
+ ---
7
+
8
+ # Đợt 0 · Bước 4 — Bảng hợp đồng hiện không ai canh
9
+
10
+ ← [`01-checklist.md`](01-checklist.md) · [`02-lo-trinh.md`](02-lo-trinh.md)
11
+
12
+ > ✅ **Đã làm rồi.** File này là bản **ĐỊNH làm**. Cái **ĐÃ xảy ra** — gồm chỗ lệch
13
+ > so với kế hoạch này và lý do — ghi ở [`buoc/0-04-may-canh-hop-dong.md`](buoc/0-04-may-canh-hop-dong.md).
14
+
15
+ ## 1. Vì sao
16
+
17
+ Framework có một nguyên tắc đã học đúng 4 lần (G1, G28, G41, G55), và lần nào cũng cùng một
18
+ hình dạng:
19
+
20
+ > **Luật ĐÚNG, viết RÕ, và KHÔNG AI CANH.**
21
+
22
+ Bốn lần đó đều được chữa bằng cùng một cách: *khai tường minh + để máy canh*. Bảng §4.5.6 là
23
+ trường hợp thứ năm, chưa được chữa.
24
+
25
+ Ba kiểu hỏng có thể xảy ra hôm nay mà không có gì báo:
26
+
27
+ | Kiểu hỏng | Chuyện gì xảy ra | Ai phát hiện |
28
+ |---|---|---|
29
+ | **Bảng thiếu row** | Màn hình có 8 nút, bảng chỉ ghi 5. QC viết test cho 3 nút còn lại bằng cách dò DOM → 3 test giòn | Không ai. Đến khi test vỡ vài tuần sau |
30
+ | **Row trỏ SC không tồn tại** | Cột "Phục vụ SC" ghi `UC1 · SC7` nhưng `.feature` chỉ có SC1–SC5 (SC7 đã bị xoá/gộp) | Không ai. QC lọc theo cột này → không khớp row nào → tưởng element không có test-id |
31
+ | **Có §4.5 mà header thiếu attr** | Bảng có giá trị test-id đầy đủ, nhưng không ai biết *tên thuộc tính* chứa chúng | Không ai — cho tới khi test đỏ hàng loạt (xem `exec-d0-b1`) |
32
+
33
+ Điểm chung: cả ba đều hỏng **im lặng**, và cả ba đều **kiểm được bằng máy** — không cần agent
34
+ "để ý", chỉ cần đếm và so khớp.
35
+
36
+ ## 2. Tình trạng hiện tại
37
+
38
+ **Không có script nào biết đến §4.5.6:**
39
+
40
+ ```bash
41
+ grep -n "testid\|test_id\|4.5.6" bin/*.js
42
+ # → 0 kết quả (build.js, gate-trace.js, index.js, lint-trace.js, self-check.js)
43
+ ```
44
+
45
+ **Các lệnh review cũng không kiểm:**
46
+
47
+ ```bash
48
+ grep -n "Selector\|testid\|4.5.6" commands/review-tech-docs.tmpl commands/validate-traces.tmpl
49
+ # → 0 kết quả
50
+ ```
51
+
52
+ Tức là bảng này được **3 lệnh đọc** (`generate-code`, `qc-run-test`, `qc-design-test`) và
53
+ **2 lệnh ghi** (`generate-tech-docs`, `map-testids`), nhưng **0 nơi kiểm**.
54
+
55
+ ## 3. Sẽ đổi thành gì
56
+
57
+ > ⚠️ **Ba chỗ dưới đây đã được sửa lại lúc triển khai (2026-09-11)** — bản gốc viết trước khi
58
+ > đọc kỹ `bin/lint-trace.js`. Giữ lại nguyên văn phần cũ để thấy vì sao đổi.
59
+ >
60
+ > **(1) Bỏ rule thứ ba.** Bản gốc đề xuất T15 = *"element có action trong step `When` mà không
61
+ > có row §4.5.6 → WARN"*. **Đã bỏ.** Nhận diện "element có action" từ văn xuôi Gherkin tiếng
62
+ > Việt là suy luận, không phải phép đếm — sẽ hoặc bỏ sót gần hết, hoặc báo loạn; mà báo loạn thì
63
+ > người ta thêm `--warn-only` vào CI và mất luôn hai rule kia. Ca nó nhắm đã có hai lớp phủ tốt
64
+ > hơn: [`exec-d0-b3`](exec-d0-b3-sua-skill-probe-dom.md) (skill bậc 3b bắt ghi gap mỗi lần phải
65
+ > dò DOM) và [`exec-d0-b6`](exec-d0-b6-contract-truoc-code.md) (`TESTID_UNDECLARED` so bảng với
66
+ > **code thật**). Phần còn lại — element không có id ở đâu cả — **không kiểm được bằng máy** ở
67
+ > tầng này (phải parse JSX/Vue/Flutter); câu trả lời đúng là gate `/review-tech-docs` ở b6.
68
+ > → Còn **2 rule**, đánh số **T15** (SC có thật) và **T16** (header attr).
69
+ >
70
+ > **(2) Không cần cờ `--tech-docs`.** `templates/project-context.yaml:100` khai
71
+ > `tech_docs_dir: "specs"` — trùng `specs_dir` trong layout feature-package. Cờ `--specs` sẵn có
72
+ > là đủ.
73
+ >
74
+ > **(3) T16 có HAI mức, không phải một.** Thiếu hẳn field → ERROR; field có nhưng còn placeholder
75
+ > `{…}` → **WARN**. Sau `exec-d0-b1`, tech-doc mới sinh ra **có** field ở dạng placeholder — đó
76
+ > là *"chưa chạy `/map-testids`"*, một trạng thái hợp lệ trong quy trình, không phải *"thiếu"*.
77
+ > Báo ERROR ở đó là làm mọi tech-doc mới đỏ ngay khi vừa tạo.
78
+ >
79
+ > **Hai chi tiết phát hiện lúc viết code:**
80
+ > - **`lint-trace` thoát sớm khi không có sổ trace** (`bin/lint-trace.js:881`). Hai rule mới
81
+ > không cần sổ → phải chạy ở **pass riêng, trước** khối đó, và sửa câu thông báo *"chưa có gì
82
+ > để kiểm"* → *"bỏ qua T1–T14"*.
83
+ > - **Dòng mẫu của template phải được bỏ qua.** Mọi tech-doc mang
84
+ > `| {uc}-{screen}-{element}-{type} | … | {UC1 · SC1, UC1 · SC3} |`. Không bỏ qua thì doc mới
85
+ > tinh đỏ ngay vì *"SC1 của UC1 không tồn tại"*.
86
+ > - **Biến thể định dạng → im lặng, không báo oan.** Cột "Phục vụ SC" parse được hai dạng
87
+ > (`UC1 · SC1` rút gọn và `FT-101-UC1-SC1` đầy đủ). Không parse được token nào → **bỏ qua**,
88
+ > không đoán.
89
+
90
+ *(Bản gốc, giữ để đối chiếu:)* Thêm 3 rule vào **`bin/lint-trace.js`**, đặt tên tiếp số hiện có
91
+ (T1–T14) → **T15, T16, T17**.
92
+
93
+ ### Vì sao chọn `lint-trace.js` chứ không phải `self-check.js`
94
+
95
+ Hai file có phân vai rõ, ghi sẵn trong doc-comment của `self-check.js:47-49`:
96
+
97
+ - `self-check.js` canh **CONTRACT** — đọc file nguồn của framework, hỏi "lệnh có gọi đúng tên
98
+ cột không". Nó **không mở file dữ liệu của dự án**.
99
+ - `lint-trace.js` canh **DỮ LIỆU** — mở sổ `.tsv` thật, `.feature` thật của dự án.
100
+
101
+ §4.5.6 là dữ liệu của dự án → thuộc `lint-trace.js`.
102
+
103
+ Thuận lợi: `lint-trace.js` **đã** nhận `--specs DIR` (`bin/lint-trace.js:61`, mặc định `specs`)
104
+ và đã đọc `.feature` cho rule T9 (sổ mồ côi). Nên không phải dựng hạ tầng mới.
105
+
106
+ ### Ba rule
107
+
108
+ | Rule | Kiểm gì | Mức | Vì sao mức đó |
109
+ |---|---|---|---|
110
+ | **T15** | Mỗi element có action xuất hiện trong step `When` của `.feature` phải có ≥1 row §4.5.6 trỏ tới SC đó | **WARN** | Nhận diện "element có action" từ prose Gherkin là **suy luận**, không phải phép đếm chắc chắn → WARN để không chặn oan. Vẫn in ra để người thấy |
111
+ | **T16** | Mọi giá trị trong cột "Phục vụ SC" của §4.5.6 phải là SC **có thật** trong `.feature` của platform đó | **ERROR** | Đây là **phép so khớp chuỗi**, không có chỗ cho suy luận. Row trỏ SC không tồn tại là sai dứt khoát |
112
+ | **T17** | Tech-doc có block §4.5 (platform client) mà header thiếu `@trace.testid_attr` → lỗi | **ERROR** | Cũng là phép kiểm có/không. Thiếu field là đảm bảo trượt locator (xem `exec-d0-b1`) |
113
+
114
+ **Tham số:** tech-doc nằm ở `{paths.tech_docs_dir}`, mặc định trùng `specs_dir` trong layout một
115
+ repo. Nếu dự án tách submodule thì khác → thêm cờ `--tech-docs DIR`, **mặc định lấy theo
116
+ `--specs`**, và nếu không tìm thấy thư mục `tech-docs/` nào thì **in một dòng nói rõ là đã bỏ
117
+ qua** — đúng cách T14 đang làm với `--code` (`bin/lint-trace.js:65`):
118
+
119
+ > *"KHÔNG truyền → T14 bị bỏ qua, và file này IN RA MỘT DÒNG nói rõ là đã bỏ qua. Im lặng rồi
120
+ > báo 'sạch' đúng là cách 910 row kia lọt qua."*
121
+
122
+ ### Khai vào schema
123
+
124
+ `bin/trace-schema.json` phải khai 3 rule mới để R8 (rule canh "lint-trace tụt lại sau schema")
125
+ không báo lệch. Kiểm lại chỗ khai của T12/T14 rồi làm theo đúng khuôn đó.
126
+
127
+ ## 4. Sửa file nào
128
+
129
+ | File | Sửa gì |
130
+ |---|---|
131
+ | `bin/lint-trace.js` | Thêm T15/T16/T17; thêm cờ `--tech-docs` (mặc định theo `--specs`); thêm dòng "đã bỏ qua" khi không tìm thấy tech-doc |
132
+ | `bin/trace-schema.json` | Khai 3 rule mới theo khuôn T12/T14 đã có |
133
+ | `templates/ci/trace-gate.yml` | Nếu file này liệt kê rule theo tên → thêm 3 rule mới (R10 trong `self-check.js` canh đúng chuyện này) |
134
+ | `docs/04-reference/trace-schema.md` | Cập nhật danh sách rule |
135
+
136
+ ## 5. Kiểm thế nào để biết đã xong
137
+
138
+ Kiểm bằng **ca lỗi cố ý** — quan trọng hơn kiểm ca sạch, vì rule mới dễ viết thành "luôn xanh":
139
+
140
+ ```bash
141
+ # Ca sạch → phải PASS
142
+ node bin/lint-trace.js --specs specs
143
+
144
+ # Ca T16: sửa 1 row §4.5.6 cho trỏ tới SC không tồn tại (vd UC1 · SC99) → phải ERROR
145
+ # Ca T17: xoá dòng @trace.testid_attr khỏi header 1 tech-doc có §4.5 → phải ERROR
146
+ # Ca T15: xoá 1 row §4.5.6 của element có trong step When → phải WARN (không ERROR)
147
+ # Ca bỏ qua: chạy trên repo không có thư mục tech-docs/ → phải IN DÒNG "đã bỏ qua", exit 0
148
+ ```
149
+
150
+ Rồi:
151
+
152
+ ```bash
153
+ node bin/build.js && node bin/self-check.js && node test/run.js
154
+ ```
155
+
156
+ Nếu có test tự động trong `test/run.js` cho lint-trace (đã có cụm T2/T3/T4/T7 ở đó), thêm 3–4
157
+ test cho T15/T16/T17 theo cùng khuôn.
158
+
159
+ ## 6. Nếu bỏ qua thì hỏng gì
160
+
161
+ Bước 1, 2, 3 sửa được hợp đồng và sửa được thói quen — nhưng **không có gì giữ nó không mục
162
+ dần**. Sáu tháng sau: SC bị gộp, row §4.5.6 trỏ vào SC đã xoá; màn hình thêm 3 nút, bảng không
163
+ ai cập nhật; một tech-doc mới tạo tay thiếu header. Từng chuyện nhỏ, không ai báo, và QC lại
164
+ quay về dò DOM vì "hợp đồng không đáng tin".
165
+
166
+ Đây đúng là cái vòng mà nguyên tắc số 3 của đợt mổ tồn tại để chặn: *"luật gì cũng phải có máy
167
+ canh — thêm luật mới thì thêm rule trong `bin/` cùng lúc, không để sau"*
168
+ ([`00-nhat-ky.md` §6](00-nhat-ky.md)).
@@ -0,0 +1,196 @@
1
+ ---
2
+ buoc: Đợt 0 — Bước 5
3
+ title: Dọn nhãn cột lệch và tên mục "§2b" cũ
4
+ phu_thuoc: d0-b1 → d0-b4
5
+ trang_thai: đã làm
6
+ ---
7
+
8
+ # Đợt 0 · Bước 5 — Dọn phần lệch còn lại
9
+
10
+ ← [`01-checklist.md`](01-checklist.md) · [`02-lo-trinh.md`](02-lo-trinh.md)
11
+
12
+ > ✅ **Đã làm rồi.** File này là bản **ĐỊNH làm**. Cái **ĐÃ xảy ra** — gồm chỗ lệch
13
+ > so với kế hoạch này và lý do — ghi ở [`buoc/0-05-don-nhan-cot-va-2b.md`](buoc/0-05-don-nhan-cot-va-2b.md).
14
+
15
+ > ⚠️ **Phạm vi đã thu hẹp sau vòng hỏi–đáp ngày 2026-09-11.** Hai phần vốn thuộc bước này —
16
+ > *"đưa `/map-testids` vào golden path"* (§3.3) và *"chặn cứng ở phía tiêu thụ"* — đã **chuyển
17
+ > hẳn** sang [`exec-d0-b6`](exec-d0-b6-contract-truoc-code.md) §3.2 + §3.4, vì ở đó chúng được
18
+ > giải quyết triệt để hơn (đổi thứ tự lệnh + gate ở `/review-tech-docs`, thay vì chỉ sửa tài
19
+ > liệu). **Làm b6 thì bỏ §3.3 của file này** — đừng sửa `steps/report-footer.md` hai lần theo
20
+ > hai hướng. Phần còn lại của bước này (§3.1 nhãn cột, §3.2 tên mục "§2b") vẫn giữ nguyên.
21
+ > Lý do đầy đủ: [`00-nhat-ky.md` §3.2](00-nhat-ky.md).
22
+
23
+ ## 1. Vì sao
24
+
25
+ Bốn bước trước sửa phần *hoạt động*. Bước này sửa phần *cách gọi tên* và *vị trí trong quy
26
+ trình* — nghe nhỏ, nhưng cả hai đều đã gây hậu quả thật:
27
+
28
+ **Nhãn lệch** — cùng một bảng, hai chỗ mô tả cột thứ ba bằng hai cái tên khác nhau. Ai đọc một
29
+ chỗ rồi làm theo chỗ kia sẽ ghi sai định dạng, và không có gì báo (Bước 4 chỉ canh cột "Phục vụ
30
+ SC", không canh cột này).
31
+
32
+ **Tên mục cũ** — ba file vẫn gọi bảng này là "§2b Test Selectors". Số mục đã đổi thành §4.5.6 từ
33
+ lâu. Agent đọc "§2b" rồi đi tìm §2b trong tech-doc → không có → kết luận tech-doc không có bảng
34
+ test selector → dò DOM. Chính là vòng lỗi mà Bước 3 vừa chặn, nhưng qua một cửa khác.
35
+
36
+ **Không nằm trong golden path** — `/map-testids` là lệnh **duy nhất** biết đọc source code
37
+ brownfield để rút test-id. Nó không có trong danh sách bước bắt buộc, nên trong thực tế bị bỏ.
38
+ Chính tài liệu của framework đã thừa nhận điều này.
39
+
40
+ ## 2. Tình trạng hiện tại
41
+
42
+ ### 2.1 Nhãn cột thứ ba lệch nhau
43
+
44
+ | Nơi | Ghi cột 3 là |
45
+ |---|---|
46
+ | `templates/tech-design.template.md:276` | `Component (§4.5.1.x)` |
47
+ | `commands/map-testids.tmpl:64` | `Component (reused/existing/new)` |
48
+
49
+ Nguyên văn `map-testids.tmpl:64`:
50
+
51
+ > *"Mỗi row: `Test-ID | Element | Component (reused/existing/new) | Action | Serves SC`."*
52
+
53
+ Nguyên văn template (`:276`):
54
+
55
+ ```
56
+ | Test-ID | Element | Component (§4.5.1.x) | Action | Phục vụ SC (UC · SC) |
57
+ ```
58
+
59
+ Hai cái này **không phải cùng thứ**: một cái muốn *tên component* (trỏ tới §4.5.1.x — cây
60
+ component), cái kia muốn *phân loại nguồn gốc* (reused/existing/new). Cả hai đều có ích, nhưng
61
+ phải chọn — hoặc tách thành 2 cột.
62
+
63
+ ### 2.2 Tên mục cũ "§2b" còn sót 3 chỗ
64
+
65
+ | File:dòng | Nội dung |
66
+ |---|---|
67
+ | `skills/spec/SKILL.md:19` | gọi "§2b Test Selectors" |
68
+ | `modules/qc-playwright/stack-profile.yaml:22` | ghi **cả hai**: *"FE tech-design §2b Test Selectors contract (… bảng Test Selectors §4.5.6 …)"* |
69
+ | `specs/domain-knowledge/figma-components/react.md:91` | gọi "§2b" |
70
+
71
+ ### 2.3 `/map-testids` không bắt buộc
72
+
73
+ Vị trí của nó trong pipeline **mâu thuẫn giữa 3 tài liệu**:
74
+
75
+ | Nơi | Đặt `/map-testids` ở đâu |
76
+ |---|---|
77
+ | `steps/report-footer.md:63` | Phase **Tech Design** (trước `/generate-code`) |
78
+ | `GAPS-v4.md:724` | trước `/generate-code --phase=ui` |
79
+ | `docs/explain/11-map-testids.md:1,69` | **sau** `/generate-code` + `/review-code` |
80
+
81
+ Và `docs/explain/11-map-testids.md:60` tự thừa nhận sự mờ này:
82
+
83
+ > *"vị trí pipeline hơi mờ (giữa Tech Design & Code). Chạy sớm quá thì UI chưa xong, muộn quá
84
+ > thì QC phải chờ."*
85
+
86
+ Rồi `:63` nói thẳng hậu quả:
87
+
88
+ > *"**Không bắt buộc trong golden path** — dễ bị bỏ qua, khiến QC selector giòn."*
89
+
90
+ ## 3. Sẽ đổi thành gì
91
+
92
+ ### 3.1 Chốt 5 cột, một định nghĩa
93
+
94
+ Giữ đúng 5 cột theo template (đang là bản chuẩn vì `generate-tech-docs` include nó), và **chuyển
95
+ phân loại `reused/existing/new` sang cột Ghi chú** thay vì cột Component:
96
+
97
+ ```
98
+ | Test-ID | Element | Component (§4.5.1.x) | Action | Phục vụ SC (UC · SC) |
99
+ ```
100
+
101
+ Sửa `map-testids.tmpl:64` cho khớp, và nói rõ phân loại nguồn gốc ghi ở đâu (Ghi chú, hoặc
102
+ không ghi vào bảng — nó chỉ là phân loại nội bộ lúc chạy Step 1).
103
+
104
+ Lý do chọn template làm chuẩn thay vì `map-testids`: `templates/tech-design.template.md` được
105
+ `generate-tech-docs.tmpl:227` include trực tiếp, nên nó là thứ **sinh ra bảng lần đầu**.
106
+ `/map-testids` chỉ cập nhật bảng đã có → phải theo.
107
+
108
+ ### 3.2 Đổi "§2b" → "§4.5.6"
109
+
110
+ Sửa 3 chỗ ở §2.2. Riêng `modules/qc-playwright/stack-profile.yaml:22` đang ghi cả hai tên —
111
+ bỏ tên cũ, giữ một.
112
+
113
+ > **Lưu ý thứ tự:** file `modules/qc-playwright/stack-profile.yaml` sẽ được **thay hẳn** ở Đợt 2
114
+ > bước 2 (Python → TypeScript). Nếu Đợt 2 chạy ngay sau Đợt 0 thì sửa ở đây là công cốc — cân
115
+ > nhắc bỏ qua chỗ này và ghi vào việc dọn của `exec-d2-b2`. Hai chỗ còn lại (`skills/spec/`,
116
+ > `specs/domain-knowledge/`) thì sửa bình thường.
117
+
118
+ ### 3.3 Đưa `/map-testids` thành bắt buộc
119
+
120
+ Chốt vị trí: **sau `/generate-code --phase=ui`, trước `/qc-design-test`**. Lý do:
121
+
122
+ - Cần code UI tồn tại để nhánh `existing`/`reused` đọc được file component
123
+ (`map-testids.tmpl:40`, `:48`).
124
+ - Phải xong trước khi QC thiết kế test case, vì `/qc-design-test` đọc §4.5.6 để trích test-id
125
+ vào bước test (`commands/qc-design-test.tmpl:128`).
126
+
127
+ Hai việc:
128
+
129
+ 1. Sửa `steps/report-footer.md` (bảng Phase `:63` + bảng Next `:98`) và
130
+ `docs/explain/11-map-testids.md` cho **cùng nói một vị trí**. Gỡ câu tự thừa nhận "vị trí hơi
131
+ mờ" ở `:60` sau khi đã chốt.
132
+ 2. **Chặn cứng ở phía tiêu thụ** — đây là phần có hiệu lực thật, vì tài liệu thì đọc rồi bỏ
133
+ qua được: `/qc-design-test` (và `/qc-design-script` ở Đợt 2) khi thấy §4.5.6 **rỗng hoặc
134
+ không tồn tại** cho platform client thì **DỪNG** với thông báo rõ:
135
+
136
+ ```
137
+ ❌ §4.5.6 Test Selectors rỗng cho platform {platform} của {TICKET-ID}.
138
+ QC không có hợp đồng locator để bám → script sinh ra sẽ giòn.
139
+ Chạy /map-testids {UC-ID} trước, rồi chạy lại lệnh này.
140
+ (Cố ý bỏ qua để prototype: thêm --no-testid-contract, và artifact sẽ được đánh dấu
141
+ là dựa trên selector dò runtime.)
142
+ ```
143
+
144
+ Cờ `--no-testid-contract` theo đúng tinh thần `--include-draft` của `steps/qc-scope.md`:
145
+ *"cái thường gặp là cái an toàn, và cái sớm phải nói ra"*.
146
+
147
+ ## 4. Sửa file nào
148
+
149
+ | File | Sửa gì |
150
+ |---|---|
151
+ | `commands/map-testids.tmpl` | `:64` — nhãn 5 cột khớp template; nói rõ phân loại `reused/existing/new` ghi ở đâu |
152
+ | `skills/spec/SKILL.md` | `:19` — "§2b" → "§4.5.6" |
153
+ | `specs/domain-knowledge/figma-components/react.md` | `:91` — "§2b" → "§4.5.6" |
154
+ | `modules/qc-playwright/stack-profile.yaml` | `:22` — bỏ tên cũ *(hoặc hoãn sang `exec-d2-b2`, xem §3.2)* |
155
+ | `steps/report-footer.md` | `:63` bảng Phase + `:98` bảng Next — chốt một vị trí cho `/map-testids` |
156
+ | `docs/explain/11-map-testids.md` | `:1`, `:60`, `:63`, `:69` — sửa vị trí pipeline, gỡ câu "vị trí hơi mờ" và "không bắt buộc" |
157
+ | `commands/qc-design-test.tmpl` | Thêm guard §4.5.6 rỗng → DỪNG + cờ `--no-testid-contract` |
158
+
159
+ ## 5. Kiểm thế nào để biết đã xong
160
+
161
+ ```bash
162
+ # 1. Không còn "§2b" nào
163
+ grep -rn "§2b\|section 2b" --include="*.md" --include="*.yaml" . | grep -v "^./GAPS"
164
+ # → 0 kết quả (trừ file GAPS lịch sử, không sửa)
165
+
166
+ # 2. Nhãn cột khớp nhau
167
+ grep -n "Test-ID | Element" templates/tech-design.template.md commands/map-testids.tmpl
168
+ # → 5 cột giống nhau
169
+
170
+ # 3. Vị trí /map-testids nhất quán
171
+ grep -rn "map-testids" steps/report-footer.md docs/explain/11-map-testids.md
172
+ # → cùng nói một vị trí
173
+
174
+ # 4. Build + máy tự kiểm
175
+ node bin/build.js && node bin/self-check.js && node test/run.js
176
+ ```
177
+
178
+ Kiểm bằng tay:
179
+
180
+ - Trên một UC **chưa** chạy `/map-testids`, chạy `/qc-design-test {UC-ID}` → phải **DỪNG** với
181
+ thông báo ở §3.3.
182
+ - Thêm `--no-testid-contract` → phải chạy được, và artifact phải có dấu ghi rõ là dựa trên
183
+ selector dò runtime.
184
+
185
+ ## 6. Nếu bỏ qua thì hỏng gì
186
+
187
+ Ba hậu quả, giảm dần theo mức nghiêm trọng:
188
+
189
+ 1. **Không chặn cứng ở phía tiêu thụ (§3.3) là mất phần lớn giá trị của cả Đợt 0.** Sửa hợp
190
+ đồng, sửa skill, thêm máy canh — nhưng nếu QC vẫn **chạy được** `/qc-design-test` khi §4.5.6
191
+ rỗng thì trong lúc gấp việc, người ta vẫn bỏ `/map-testids`. Tài liệu nói "nên chạy" thì đọc
192
+ rồi bỏ qua; lệnh dừng lại thì không.
193
+ 2. **"§2b" còn sót** → agent đi tìm mục không tồn tại → kết luận tech-doc không có bảng → dò
194
+ DOM. Đúng vòng lỗi Bước 3 vừa chặn, đi qua cửa khác.
195
+ 3. **Nhãn cột lệch** → hai người ghi bảng theo hai định dạng. Bước 4 chỉ canh cột "Phục vụ SC",
196
+ nên cột này lệch mà vẫn xanh.