@educa-corp/sdd-framework 0.9.4 → 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 (69) hide show
  1. package/bin/lint-trace.js +230 -2
  2. package/bin/self-check.js +24 -0
  3. package/bin/trace-schema.json +58 -4
  4. package/core/FRAMEWORK_VERSION +1 -1
  5. package/core/commands/generate-bdd.md +1 -0
  6. package/core/commands/generate-code.md +39 -2
  7. package/core/commands/generate-tech-docs.md +21 -2
  8. package/core/commands/map-testids.md +88 -8
  9. package/core/commands/qc-analyze.md +424 -354
  10. package/core/commands/qc-design-test.md +246 -182
  11. package/core/commands/qc-plan.md +92 -79
  12. package/core/commands/qc-report.md +76 -60
  13. package/core/commands/qc-review.md +130 -114
  14. package/core/commands/qc-run-test.md +230 -156
  15. package/core/commands/review-tech-docs.md +20 -0
  16. package/core/modules/qc-playwright/stack-profile.yaml +1 -1
  17. package/core/rules/data-protection.md +52 -0
  18. package/core/skills/qc/_shared/self-review-principles.md +112 -0
  19. package/core/skills/qc/qa-runner/e2e.md +2 -2
  20. package/core/skills/qc/qa-runner/functional/gui-feature.md +9 -3
  21. package/core/skills/qc/qa-runner/functional/gui-screen.md +9 -3
  22. package/core/skills/qc/qa-runner/integration.md +1 -1
  23. package/core/skills/qc/qa-runner/non-functional.md +1 -1
  24. package/core/skills/spec/SKILL.md +1 -1
  25. package/core/steps/context-loader.md +7 -2
  26. package/core/steps/gap-verify.md +67 -0
  27. package/core/steps/report-footer.md +3 -3
  28. package/core/templates/feature.template +1 -0
  29. package/core/templates/tech-design.template.md +1 -0
  30. package/docs/04-reference/commands.md +1 -1
  31. package/docs/04-reference/trace-schema.md +38 -0
  32. package/docs/explain/11-map-testids.md +70 -69
  33. package/docs/plans/qc-surgery/00-nhat-ky.md +497 -0
  34. package/docs/plans/qc-surgery/01-checklist.md +92 -0
  35. package/docs/plans/qc-surgery/02-lo-trinh.md +266 -0
  36. package/docs/plans/qc-surgery/buoc/0-01-testid-attr-co-cho-o.md +157 -0
  37. package/docs/plans/qc-surgery/buoc/0-02-mot-nguon-cho-testid-attr.md +135 -0
  38. package/docs/plans/qc-surgery/buoc/0-03-skill-thoi-day-do-dom.md +167 -0
  39. package/docs/plans/qc-surgery/buoc/0-04-may-canh-hop-dong.md +173 -0
  40. package/docs/plans/qc-surgery/buoc/0-05-don-nhan-cot-va-2b.md +133 -0
  41. package/docs/plans/qc-surgery/buoc/0-06-hop-dong-truoc-code.md +226 -0
  42. package/docs/plans/qc-surgery/buoc/1-01-guard-br-tag.md +156 -0
  43. package/docs/plans/qc-surgery/buoc/1-02-guard-sc-coverage.md +153 -0
  44. package/docs/plans/qc-surgery/buoc/1-03-fail-3-nhan.md +176 -0
  45. package/docs/plans/qc-surgery/buoc/1-04-self-review-dung-chung.md +175 -0
  46. package/docs/plans/qc-surgery/buoc/1-05-spec-la-du-lieu.md +164 -0
  47. package/docs/plans/qc-surgery/buoc/1-06-gap-verify-du-bo.md +162 -0
  48. package/docs/plans/qc-surgery/buoc/README.md +85 -0
  49. package/docs/plans/qc-surgery/exec-d0-b1-testid-attr-header.md +147 -0
  50. package/docs/plans/qc-surgery/exec-d0-b2-thong-nhat-nguon-testid-attr.md +152 -0
  51. package/docs/plans/qc-surgery/exec-d0-b3-sua-skill-probe-dom.md +173 -0
  52. package/docs/plans/qc-surgery/exec-d0-b4-may-canh-4-5-6.md +168 -0
  53. package/docs/plans/qc-surgery/exec-d0-b5-don-nhan-lech.md +196 -0
  54. package/docs/plans/qc-surgery/exec-d0-b6-contract-truoc-code.md +350 -0
  55. package/docs/plans/qc-surgery/exec-d1-b1-guard-br-tag.md +129 -0
  56. package/docs/plans/qc-surgery/exec-d1-b2-guard-sc-coverage.md +159 -0
  57. package/docs/plans/qc-surgery/exec-d1-b3-fail-3-bucket.md +158 -0
  58. package/docs/plans/qc-surgery/exec-d1-b4-self-review-principles.md +145 -0
  59. package/docs/plans/qc-surgery/exec-d1-b5-noi-quy-spec-la-du-lieu.md +156 -0
  60. package/docs/plans/qc-surgery/exec-d1-b6-gap-verify-mo-rong.md +179 -0
  61. package/docs/plans/qc-surgery/exec-d2-b1-tach-qc-review.md +166 -0
  62. package/docs/plans/qc-surgery/exec-d2-b2-tach-qc-run-test-atomic.md +267 -0
  63. package/docs/plans/qc-surgery/exec-d2-b3-qc-automation-assess.md +198 -0
  64. package/docs/plans/qc-surgery/exec-d3-b1-qc-report-gate-decision.md +209 -0
  65. package/docs/plans/qc-surgery/exec-d4-b1-qc-design-testdata.md +146 -0
  66. package/docs/plans/qc-surgery/exec-d4-b2-qc-smoke-test.md +179 -0
  67. package/docs/plans/qc-surgery/exec-d4-b3-qc-metrics-va-lint.md +198 -0
  68. package/docs/plans/qc-surgery/exec-d4-b4-lint-spec-injection.md +199 -0
  69. package/package.json +1 -1
package/bin/lint-trace.js CHANGED
@@ -807,6 +807,227 @@ function lintCodeVsLedger() {
807
807
  }
808
808
  }
809
809
 
810
+ // ── T15/T16 — hợp đồng test-id: bảng §4.5.6 vs .feature ─────────────────────
811
+ //
812
+ // Vì sao là PASS RIÊNG, không đi cùng vòng lặp sổ: hai rule này so tech-doc với .feature,
813
+ // không đụng một ô nào của sổ trace. Neo chúng vào sổ là làm chúng câm ở repo chưa có sổ.
814
+ //
815
+ // PHẠM VI neo vào SỰ TỒN TẠI CỦA HỢP ĐỒNG: doc không có block §4.5 client thì không kiểm gì.
816
+ // Dự án backend-only, hay dự án chưa từng chạy /map-testids, phải IM LẶNG HOÀN TOÀN. Hai rule
817
+ // này nói *"chỗ nào đã hứa thì phải giữ"*, KHÔNG nói *"mọi chỗ đều phải có hợp đồng"* — nếu
818
+ // không thì mọi dự án đang chạy đỏ ngay ngày nâng version, và việc đầu tiên người ta làm là
819
+ // thêm --warn-only vào CI. Mất luôn cả hai rule.
820
+
821
+ // Ô/dòng còn `{…}` là PLACEHOLDER của template. MỌI tech-doc vừa sinh ra đều mang dòng mẫu
822
+ // `| {uc}-{screen}-{element}-{type} | … | {UC1 · SC1, UC1 · SC3} |`. Không bỏ qua thì doc mới
823
+ // tinh đã đỏ vì "SC1 của UC1 không tồn tại" — báo oan ngay lần chạy đầu tiên.
824
+ const hasPlaceholder = s => /\{[^}\n]*\}/.test(s);
825
+
826
+ // Hợp đồng đã khai, gom cho T17/T18: attr → { ids: Set, byDoc: Map(doc → Set) }.
827
+ // T18 phải so với HỢP NHẤT mọi bảng, không so từng doc: một monorepo có nhiều PRD, và id của
828
+ // PRD khác nằm trong code là chuyện bình thường — so per-doc sẽ báo oan gần hết.
829
+ const declared = new Map();
830
+
831
+ function lintTestidContract() {
832
+ if (!fs.existsSync(SPECS_DIR)) return;
833
+
834
+ const docs = walk(SPECS_DIR).filter(f => /-tech-design\.md$/.test(path.basename(f)));
835
+ if (!docs.length) {
836
+ infos.push(`Không có tech-doc nào dưới ${rel(SPECS_DIR)} — bỏ qua T15/T16 (hợp đồng test-id).`);
837
+ return;
838
+ }
839
+
840
+ for (const doc of docs) {
841
+ const lines = readLines(doc);
842
+
843
+ // Block §4.5 theo nền: "### 4.5 … — {platform} ({framework})".
844
+ // Chỉ nền CLIENT mới có test-id; `system` không có UI nên nằm ngoài phạm vi.
845
+ const blocks = [];
846
+ lines.forEach((l, i) => {
847
+ const m = /^###\s+4\.5\b[^—]*—\s*([A-Za-z0-9_-]+)/.exec(l);
848
+ if (m && m[1] !== 'system') blocks.push({ platform: m[1], at: i });
849
+ });
850
+ if (!blocks.length) continue;
851
+
852
+ // ── T16 — có §4.5 client thì header PHẢI khai TÊN THUỘC TÍNH test-id ──
853
+ const attr = lines.find(l => /^@trace\.testid_attr\s*:/.test(l.trim()));
854
+ if (!attr) {
855
+ err('T16', rel(doc),
856
+ 'có block §4.5 (client) nhưng header thiếu @trace.testid_attr',
857
+ 'Đây là TÊN THUỘC TÍNH chứa test-id (data-testid | data-test | ValueKey…).\n' +
858
+ ' Thiếu nó thì QC đoán theo nền — dự án dùng data-test sẽ TRƯỢT 100% locator,\n' +
859
+ ' và test đỏ "element not found" trông y hệt một bug sản phẩm.\n' +
860
+ ' Sửa: chạy /map-testids {UC-ID}.');
861
+ } else if (hasPlaceholder(attr)) {
862
+ warn('T16', rel(doc),
863
+ '@trace.testid_attr còn ở dạng placeholder — chưa ai điền',
864
+ 'Tech-doc vừa sinh ra thì đây là trạng thái BÌNH THƯỜNG (nên là WARN, không ERROR).\n' +
865
+ ' Điền bằng /map-testids {UC-ID} trước khi QC dựng locator.');
866
+ }
867
+
868
+ // Giá trị THẬT của tên thuộc tính. Thiếu hoặc còn placeholder → null, và khi đó không gom
869
+ // id để so với code: không biết tìm thuộc tính nào trong code thì mọi so sánh đều là đoán.
870
+ const attrVal = attr && !hasPlaceholder(attr)
871
+ ? attr.split(':').slice(1).join(':').trim()
872
+ : null;
873
+
874
+ // ── T15 — mọi SC ở cột "Phục vụ SC" phải CÓ THẬT trong .feature ──
875
+ const parts = path.relative(SPECS_DIR, doc).replace(/\\/g, '/').split('/');
876
+ if (parts.length < 3) continue; // không theo {domain}/{prd-slug}/ — bố cục cũ
877
+ const [domain, prdSlug] = parts;
878
+ const ticket = path.basename(doc).replace(/-tech-design\.md$/, '');
879
+
880
+ for (const b of blocks) {
881
+ const bddDir = path.join(SPECS_DIR, domain, prdSlug, 'bdd', b.platform);
882
+ if (!fs.existsSync(bddDir)) continue;
883
+
884
+ const known = new Set();
885
+ for (const f of fs.readdirSync(bddDir).filter(n => n.endsWith('.feature'))) {
886
+ const t = fs.readFileSync(path.join(bddDir, f), 'utf8');
887
+ for (const m of t.matchAll(/@trace\.scenario\s*:\s*(\S+)/g)) known.add(m[1]);
888
+ }
889
+ if (!known.size) continue; // chưa có scenario nào → không có gì để đối chiếu
890
+
891
+ const end = lines.findIndex((l, i) => i > b.at && /^###\s/.test(l));
892
+ const body = lines.slice(b.at, end === -1 ? lines.length : end);
893
+
894
+ body.forEach((line, k) => {
895
+ if (!line.trim().startsWith('|')) return;
896
+ if (/^\|[\s|:-]+\|$/.test(line.trim())) return; // dòng phân cách markdown |---|---|
897
+ if (hasPlaceholder(line)) return; // dòng mẫu của template
898
+ const cells = line.split('|').map(s => s.trim());
899
+ const last = cells[cells.length - 2] || ''; // cột "Phục vụ SC"
900
+
901
+ // Gom id đã khai (cột 1) cho T17/T18. Làm ở đây vì đây là chỗ duy nhất đã lọc xong
902
+ // dòng mẫu và dòng tiêu đề/phân cách.
903
+ const idCell = (cells[1] || '').replace(/`/g, '').trim();
904
+ // Phải BẮT ĐẦU bằng chữ/số: chặn dòng phân cách (`---`) và mọi rác không phải id.
905
+ if (attrVal && /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(idCell) && idCell !== 'Test-ID') {
906
+ if (!declared.has(attrVal)) declared.set(attrVal, { ids: new Set(), byDoc: new Map() });
907
+ const d = declared.get(attrVal);
908
+ d.ids.add(idCell);
909
+ if (!d.byDoc.has(doc)) d.byDoc.set(doc, new Set());
910
+ d.byDoc.get(doc).add(idCell);
911
+ }
912
+
913
+ if (!last) return;
914
+
915
+ // Hai dạng đã gặp: rút gọn "UC1 · SC1" và đầy đủ "FT-101-UC1-SC1".
916
+ // KHÔNG parse được token nào → BỎ QUA, không đoán. Biến thể định dạng phải thành
917
+ // "im lặng", không thành "báo oan" — đó là cách một rule mới mất niềm tin trong tuần đầu.
918
+ const ids = [
919
+ ...[...last.matchAll(/\bUC(\d+)\s*[·.]\s*SC(\d+)\b/g)].map(m => `${ticket}-UC${m[1]}-SC${m[2]}`),
920
+ ...[...last.matchAll(/\b([A-Za-z]+-\d+-UC\d+-SC\d+)\b/g)].map(m => m[1]),
921
+ ];
922
+ for (const id of ids) {
923
+ if (known.has(id)) continue;
924
+ err('T15', `${rel(doc)}:${b.at + k + 1}`,
925
+ `§4.5.6 (${b.platform}) trỏ tới ${id} — scenario này KHÔNG có trong .feature`,
926
+ `Đã tìm ở: ${rel(bddDir)}/\n` +
927
+ ` Consumer per-UC LỌC theo cột "Phục vụ SC". Trỏ vào SC không tồn tại\n` +
928
+ ` ⇒ không khớp row nào ⇒ QC tưởng element không có test-id rồi đi dò DOM.\n` +
929
+ ` Nguyên nhân thường gặp: SC bị gộp/xoá mà bảng không được cập nhật.`);
930
+ }
931
+ });
932
+ }
933
+ }
934
+ }
935
+
936
+ // ── T17/T18 — hợp đồng §4.5.6 vs CODE thật ──────────────────────────────────
937
+ //
938
+ // T15/T16 canh bảng với .feature (hai tài liệu). Hai rule này canh bảng với CODE — chiều duy
939
+ // nhất phát hiện được "FE chưa gắn" và "FE gắn ngoài hợp đồng".
940
+ //
941
+ // CẢ HAI LÀ WARN, không ERROR. Id đoán từ thiết kế KHÔNG sống sót 100%: lúc implement, dev có
942
+ // thể gộp hai element thành một component hoặc tách một thành hai. Đây là NỢ CẦN THẤY, không
943
+ // phải cái sai chặn người — cùng nhóm với TECHDOC_DRIFT.
944
+
945
+ // Dựng regex tìm test-id trong code theo ĐÚNG tên thuộc tính mà tech-doc khai.
946
+ // Không đoán được dạng → trả null, và lệnh sẽ BỎ QUA attr đó thay vì so bừa.
947
+ function testidPattern(attr) {
948
+ if (/^[A-Za-z][A-Za-z0-9_-]*$/.test(attr)) {
949
+ // web `data-testid`/`data-test`/`data-qa` · RN `testID` · iOS `accessibilityIdentifier`
950
+ // Phủ: attr="x" · attr='x' · attr={"x"} · attr={'x'} · attr: "x"
951
+ return new RegExp(`\\b${attr.replace(/-/g, '\\-')}\\s*[=:]\\s*\\{?\\s*['"\`]([^'"\`]+)['"\`]`, 'g');
952
+ }
953
+ if (/Key|Semantics/i.test(attr)) {
954
+ // Flutter: Key('x') · ValueKey('x') · Semantics(identifier: 'x')
955
+ return /(?:\bValueKey|\bKey)\s*\(\s*['"]([^'"]+)['"]|identifier\s*:\s*['"]([^'"]+)['"]/g;
956
+ }
957
+ return null;
958
+ }
959
+
960
+ function lintTestidVsCode() {
961
+ if (!declared.size) return; // chưa có hợp đồng nào → ngoài phạm vi
962
+
963
+ if (!CODE_DIRS.length) {
964
+ infos.push(`T17/T18 BỎ QUA — không có --code nên chưa đối chiếu hợp đồng test-id với code. ` +
965
+ `Ca "FE chưa gắn id đã khai" và "FE gắn id ngoài hợp đồng" đang KHÔNG được kiểm. ` +
966
+ `Truyền --code {gốc mã nguồn} để bật.`);
967
+ return;
968
+ }
969
+ const roots = CODE_DIRS.filter(d => fs.existsSync(path.resolve(d)));
970
+ if (!roots.length) return; // T14 đã báo gốc code không tồn tại
971
+
972
+ const files = [];
973
+ const walkCode = dir => {
974
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
975
+ if (e.isDirectory()) { if (!CODE_SKIP_DIR.has(e.name)) walkCode(path.join(dir, e.name)); }
976
+ else if (CODE_EXT.includes(path.extname(e.name))) files.push(path.join(dir, e.name));
977
+ }
978
+ };
979
+ for (const r of roots) walkCode(path.resolve(r));
980
+ if (!files.length) return;
981
+
982
+ for (const [attr, d] of declared) {
983
+ const re = testidPattern(attr);
984
+ if (!re) {
985
+ infos.push(`T17/T18 BỎ QUA cho @trace.testid_attr = "${attr}" — không dựng được phép tìm ` +
986
+ `xác định cho dạng này. So bừa thì báo oan, nên im lặng và nói ra.`);
987
+ continue;
988
+ }
989
+
990
+ const inCode = new Map(); // id → file đầu tiên thấy
991
+ for (const f of files) {
992
+ const txt = fs.readFileSync(f, 'utf8');
993
+ for (const m of txt.matchAll(re)) {
994
+ const v = m[1] || m[2];
995
+ if (v && !inCode.has(v)) inCode.set(v, f);
996
+ }
997
+ }
998
+
999
+ // Code CHƯA có id nào của thuộc tính này → nhiều khả năng UI chưa viết, hoặc --code trỏ
1000
+ // sai gốc. Phán "FE chưa gắn" lúc này là báo oan cả bảng.
1001
+ if (!inCode.size) continue;
1002
+
1003
+ // ── T17 — bảng có, code không có ──
1004
+ for (const [doc, ids] of d.byDoc) {
1005
+ const missing = [...ids].filter(id => !inCode.has(id));
1006
+ if (!missing.length) continue;
1007
+ const SHOW = 5;
1008
+ warn('T17', rel(doc),
1009
+ `${missing.length} test-id đã khai ở §4.5.6 mà KHÔNG tìm thấy trong code: ` +
1010
+ missing.slice(0, SHOW).join(', ') + (missing.length > SHOW ? ` (+${missing.length - SHOW})` : ''),
1011
+ `Thuộc tính đang tìm: ${attr} (theo @trace.testid_attr của doc này).\n` +
1012
+ ` FE chưa gắn, gắn sai giá trị, hoặc element đã đổi lúc implement.\n` +
1013
+ ` QC sẽ trượt locator ở đúng những id này. Sửa: /generate-code, hoặc\n` +
1014
+ ` /map-testids {UC-ID} --from-code nếu element thật đã đổi.`);
1015
+ }
1016
+
1017
+ // ── T18 — code có, bảng không có ──
1018
+ const undeclared = [...inCode.keys()].filter(id => !d.ids.has(id));
1019
+ if (undeclared.length) {
1020
+ const SHOW = 5;
1021
+ warn('T18', rel(path.dirname([...inCode.values()][0])),
1022
+ `${undeclared.length} test-id nằm trong code mà KHÔNG có trong §4.5.6 nào: ` +
1023
+ undeclared.slice(0, SHOW).join(', ') + (undeclared.length > SHOW ? ` (+${undeclared.length - SHOW})` : ''),
1024
+ `Thuộc tính: ${attr}. Ai đó gắn id ngoài hợp đồng — thường là /generate-code chạy khi\n` +
1025
+ ` §4.5.6 rỗng (id TẠM), hoặc sửa tay. Hợp đồng không còn đủ, và QC không biết\n` +
1026
+ ` những id này tồn tại. Sửa: /map-testids {UC-ID} --from-code để đưa vào bảng.`);
1027
+ }
1028
+ }
1029
+ }
1030
+
810
1031
  // ── Main ──────────────────────────────────────────────────────────────────────
811
1032
 
812
1033
  let scanned = { books: 0, seams: 0, history: 0, files: 0 };
@@ -863,6 +1084,11 @@ for (const dir of TRACE_DIRS) {
863
1084
  // phán khi chưa đọc hết sổ là phán trên dữ liệu thiếu.
864
1085
  lintCodeVsLedger();
865
1086
 
1087
+ // T15/T16 độc lập với sổ trace — xem doc-comment của lintTestidContract().
1088
+ lintTestidContract();
1089
+ // T17/T18 SAU nó: cần `declared` mà nó vừa gom.
1090
+ lintTestidVsCode();
1091
+
866
1092
  // ── Report ────────────────────────────────────────────────────────────────────
867
1093
 
868
1094
  if (AS_JSON) {
@@ -880,9 +1106,11 @@ console.log('');
880
1106
 
881
1107
  if (!foundDirs.length) {
882
1108
  console.log(` ℹ️ Không tìm thấy trace dir nào (đã thử: ${TRACE_DIRS.join(', ')}).`);
883
- console.log(` Chưa sổ trace thì chưa gì để kiểm — chạy /generate-bdd để khởi tạo.`);
1109
+ console.log(` Bỏ qua T1–T14 (rule về sổ trace) — chạy /generate-bdd để khởi tạo.`);
884
1110
  console.log('');
885
- process.exit(0);
1111
+ // T15/T16 KHÔNG cần sổ trace: chúng so tech-doc với .feature, và tech-doc có TRƯỚC sổ ở
1112
+ // repo chỉ-spec. Thoát vô điều kiện ở đây là làm hai rule đó câm đúng nơi chúng có việc.
1113
+ if (!errors.length && !warns.length && !infos.length) process.exit(0);
886
1114
  }
887
1115
 
888
1116
  const show = (list, icon) => {
package/bin/self-check.js CHANGED
@@ -425,6 +425,30 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
425
425
  }
426
426
  }
427
427
 
428
+ // ── R8e — rule khai trong `testid_contract` phải được lint-trace PHÁT RA ──
429
+ //
430
+ // Cùng khuôn R14(d): khai một rule id trong schema mà checker không phát ra nó thì khối
431
+ // schema đó chỉ là chữ. Đây là nửa "máy canh" của luật "khai tường minh + để máy canh" —
432
+ // và hợp đồng test-id là chỗ luật đó bị thiếu lâu nhất (3 lệnh đọc, 2 lệnh ghi, 0 nơi kiểm).
433
+ {
434
+ const TC = schema.testid_contract;
435
+ if (TC && Array.isArray(TC.lint_rules)) {
436
+ const emits = new Set((lintText.match(/\b(?:err|warn)\('(T\d+)'/g) || [])
437
+ .map(m => /T\d+/.exec(m)[0]));
438
+ for (const r of TC.lint_rules) {
439
+ if (!r.why) {
440
+ err('R8', `\`testid_contract.lint_rules\` mục \`${r.rule}\` thiếu \`why\``,
441
+ 'Một rule không nói được VÌ SAO sẽ bị người sau coi là tuỳ ý và gỡ đi.');
442
+ }
443
+ if (!emits.has(r.rule)) {
444
+ err('R8', `schema khai \`testid_contract\` rule \`${r.rule}\` mà ${LINT_REL} KHÔNG phát ra`,
445
+ 'Khai rule rồi mà checker không có nó = hợp đồng test-id không được kiểm ở đâu cả,\n'
446
+ + ' trong khi bảng §4.5.6 có 3 lệnh đọc và 2 lệnh ghi.');
447
+ }
448
+ }
449
+ }
450
+ }
451
+
428
452
  // ── R8c — `n` phải là 1..N liền mạch, không trùng ──
429
453
  //
430
454
  // lint-trace dựng header bằng cách SORT theo `n` (thứ tự array KHÔNG phải thứ tự cột —
@@ -782,11 +782,12 @@
782
782
  {
783
783
  "n": 10,
784
784
  "name": "qc_status",
785
- "$comment": "Chủ (ghi pass/fail/skip): qc-run-test — DUY NHẤT. Invalidator (chỉ hạ về not_run khi spec/code vừa đổi): generate-bdd · generate-code. Xem dev_selftest + rules/workflow.md 'Làm mất hiệu lực ≠ ghi đè'.",
785
+ "$comment": "Chủ (ghi pass/fail/skip): qc-run-test — DUY NHẤT. Invalidator (chỉ hạ về not_run khi spec/code/HỢP ĐỒNG TEST-ID vừa đổi): generate-bdd · generate-code · map-testids. Xem dev_selftest + rules/workflow.md 'Làm mất hiệu lực ≠ ghi đè'. map-testids vào danh sách này vì đổi một test-id trong §4.5.6 làm script QC bám id cũ HẾT ĐÚNG: nó định vị một element không còn mang id đó, nên `pass` cũ không còn nghĩa 'scenario đã được nghiệm thu theo spec hiện tại'. Cột 'Phục vụ SC' của row chính là chỉ mục ngược để biết SC nào bị ảnh hưởng.",
786
786
  "written_by": [
787
787
  "qc-run-test",
788
788
  "generate-bdd",
789
- "generate-code"
789
+ "generate-code",
790
+ "map-testids"
790
791
  ],
791
792
  "read_by": [
792
793
  "validate-traces",
@@ -799,11 +800,12 @@
799
800
  {
800
801
  "n": 11,
801
802
  "name": "qc_run_at",
802
- "$comment": "Chủ: qc-run-test. Invalidator (hạ về —): generate-bdd · generate-code. Xem qc_status.",
803
+ "$comment": "Chủ: qc-run-test. Invalidator (hạ về —): generate-bdd · generate-code · map-testids. Xem qc_status.",
803
804
  "written_by": [
804
805
  "qc-run-test",
805
806
  "generate-bdd",
806
- "generate-code"
807
+ "generate-code",
808
+ "map-testids"
807
809
  ],
808
810
  "read_by": [
809
811
  "validate-traces",
@@ -1775,6 +1777,58 @@
1775
1777
  }
1776
1778
  ]
1777
1779
  },
1780
+ "testid_contract": {
1781
+ "$comment": [
1782
+ "HỢP ĐỒNG TEST-ID FE↔QC — phần được MÁY canh.",
1783
+ "",
1784
+ "Contract gồm hai nửa, ở hai chỗ khác nhau trong CÙNG một tech-doc:",
1785
+ " @trace.testid_attr (header, scope file) → TÊN THUỘC TÍNH, MỘT giá trị cho cả doc",
1786
+ " bảng §4.5.6 (thân, theo nền) → GIÁ TRỊ test-id từng element, N dòng",
1787
+ "Consumer per-UC lọc row của mình qua cột 'Phục vụ SC (UC · SC)'.",
1788
+ "",
1789
+ "VÌ SAO CẦN CANH: trước khối này, bảng §4.5.6 được 3 lệnh ĐỌC (generate-code, qc-run-test,",
1790
+ "qc-design-test) và 2 lệnh GHI (generate-tech-docs, map-testids) — mà 0 nơi kiểm. Luật chống",
1791
+ "giẫm chân giữa hai người ghi là một CÂU VĂN XUÔI ở map-testids Step 5. Đây đúng hình dạng",
1792
+ "đã gặp bốn lần trong loạt GAP: luật ĐÚNG, viết RÕ, và KHÔNG AI CANH (G1 · G28 · G41 · G55).",
1793
+ "",
1794
+ "PHẠM VI NEO VÀO SỰ TỒN TẠI CỦA HỢP ĐỒNG — điều khoản quan trọng nhất của khối này:",
1795
+ "doc không có block §4.5 client thì KHÔNG kiểm gì. Dự án backend-only, hay dự án chưa từng",
1796
+ "chạy /map-testids, phải im lặng hoàn toàn. Hai rule nói 'chỗ nào đã hứa thì phải giữ',",
1797
+ "KHÔNG nói 'mọi chỗ đều phải có hợp đồng'. Thiếu điều khoản này thì mọi dự án đang chạy đỏ",
1798
+ "ngay ngày nâng version, và việc đầu tiên người ta làm là thêm --warn-only vào CI.",
1799
+ "",
1800
+ "KHÔNG vào gate.blocking: đây là nợ cần thấy, không phải 'đang có cái sai' chặn PR.",
1801
+ "Cùng nhóm với TECHDOC_DRIFT / BDD_DRIFT — 13/17 cờ audit hiện tại cũng không chặn."
1802
+ ],
1803
+ "artifact": "tech-design.md",
1804
+ "header_field": "@trace.testid_attr",
1805
+ "table_section": "4.5.6",
1806
+ "serves_column": "Phục vụ SC",
1807
+ "lint_rules": [
1808
+ {
1809
+ "rule": "T15",
1810
+ "level": "error",
1811
+ "why": "Row §4.5.6 trỏ tới SC không tồn tại trong .feature. Consumer per-UC LỌC theo cột 'Phục vụ SC' — trỏ vào SC đã bị gộp/xoá thì không khớp row nào, QC tưởng element không có test-id rồi đi dò DOM. Đây là phép so khớp chuỗi, không có chỗ cho suy luận, nên ERROR."
1812
+ },
1813
+ {
1814
+ "rule": "T16",
1815
+ "level": "error+warn",
1816
+ "why": "Có §4.5 client mà header thiếu @trace.testid_attr → ERROR: QC sẽ đoán tên thuộc tính theo nền, và dự án dùng data-test/data-qa TRƯỢT 100% locator trong im lặng (test đỏ 'element not found' trông y hệt bug sản phẩm). Field còn ở dạng placeholder → WARN, không ERROR: tech-doc vừa sinh ra chưa chạy /map-testids là trạng thái HỢP LỆ trong quy trình, báo đỏ ở đó là bắt oan."
1817
+ },
1818
+ {
1819
+ "rule": "T17",
1820
+ "level": "warn",
1821
+ "needs": "--code",
1822
+ "why": "Id đã khai ở §4.5.6 mà KHÔNG có trong code: FE chưa gắn, gắn sai giá trị, hoặc element đã đổi lúc implement. QC sẽ trượt locator đúng ở những id đó. WARN chứ không ERROR vì id đoán từ thiết kế không sống sót 100% — dev có thể gộp/tách element lúc implement, và đó là nợ cần thấy chứ không phải cái sai chặn người."
1823
+ },
1824
+ {
1825
+ "rule": "T18",
1826
+ "level": "warn",
1827
+ "needs": "--code",
1828
+ "why": "Id nằm trong code mà không có trong §4.5.6 nào: ai đó gắn ngoài hợp đồng — thường là /generate-code chạy khi bảng rỗng (người dùng chọn 'vẫn sinh với id TẠM'), hoặc sửa tay. Đây là lưới bắt phía sau cho quyết định đó: hợp đồng không còn đủ, và QC không biết những id này tồn tại. Chính vì có T18 mà /generate-code được phép hỏi rồi đi tiếp thay vì chặn cứng."
1829
+ }
1830
+ ]
1831
+ },
1778
1832
  "strict_use_check": {
1779
1833
  "$comment": [
1780
1834
  "R3 CANH 'CÓ NHẮC TÊN', KHÔNG CANH 'CÓ DÙNG'. Khối này siết đúng những field mà trả lời",
@@ -1 +1 @@
1
- 0.9.4
1
+ 0.9.5
@@ -653,6 +653,7 @@ Với mỗi UC, ghi vào path trên và set `# @trace.platform: {active_platform
653
653
  # @trace.prd_version: {đọc từ metadata PRD "| **Version** |"}
654
654
  # @trace.bdd_version: {cấp FILE — 1.0 nếu gen mới; tăng 0.1 khi gen lại. Khác @trace.sc_version (cấp từng SC) bên dưới}
655
655
  # @trace.business_rules: {TICKET-ID}-UC{N}-BR{m}, {TICKET-ID}-UC{N}-BR{m+1} ← {m} lấy NGUYÊN từ PRD §3: BR đánh số LIÊN TỤC toàn PRD, KHÔNG reset theo UC
656
+ # @trace.api_source: existing ← CÓ ĐIỀU KIỆN: chỉ giữ dòng này khi @trace.platform=system VÀ PRD Metadata có "| **API Source** | existing |". Mọi ca khác (greenfield · web · app) → XOÁ HẲN DÒNG NÀY; đừng để trống, đừng ghi "—". Vắng là ĐÚNG (review-context Nhóm C)
656
657
  # @trace.dataset: {domain}.testdata.yaml
657
658
  # ============================================================
658
659
 
@@ -657,8 +657,45 @@ DTOs → Entity/Model → Repository → Service interface → Service impl →
657
657
 
658
658
  Mỗi element **có action** (button, input, link, select, toggle, form-submit) PHẢI mang một **test-id ổn định** để QC định vị trực tiếp (không scan runtime):
659
659
 
660
- 1. **Nguồn id.** Nếu tech-doc gộp `{paths.tech_docs_dir}/{domain}/{prd-slug}/tech-docs/{TICKET-ID}-tech-design.md` **§4.5.6 Test Selectors** cho platform này, lấy id **nguyên văn** từ bảng đó (contract). Nếu chưa có (vd `--phase=ui` trước khi §4.5 được vẽ), **sinh id theo quy ước** `{uc-lower}-{screen}-{element}-{type}` (vd `ft001-login-submit-btn`) để QC vẫn có handle ổn định — chúng sẽ được đối chiếu với §4.5.6 của tech-design lúc integration.
661
- 2. **Emit qua attribute platform** — chọn attribute theo **`active_module`** (bảng dưới, mặc định). Chỉ override khi header `.feature` khai tường minh `@trace.testid_attr={attr}` (hiếm; dùng cho stack lai). Không có khai báo → theo module:
660
+ 1. **Nguồn id bảng §4.5.6 là HỢP ĐỒNG, không phải gợi ý.** Đọc **§4.5.6 Test Selectors** cho platform này ở tech-doc gộp `{paths.tech_docs_dir}/{domain}/{prd-slug}/tech-docs/{TICKET-ID}-tech-design.md`, lọc theo cột "Phục vụ SC" khớp SC của UC này, rồi lấy id **nguyên văn**. Bảng do `/map-testids` ghi phase Tech Design **trước** lệnh này.
661
+
662
+ **Bảng rỗng hoặc không có row nào cho SC của UC này** → *không tự sinh id rồi đi tiếp*. Cảnh báo rồi **để người quyết**:
663
+
664
+ ```
665
+ ⚠️ §4.5.6 Test Selectors RỖNG cho {platform} của {TICKET-ID} — chưa có hợp đồng test-id.
666
+
667
+ Sinh code bây giờ nghĩa là mỗi id ở đây là id TẠM do lệnh này tự đặt:
668
+ · QC chưa bám vào được (họ đọc §4.5.6, không đọc code)
669
+ · /map-testids sau này phải đối chiếu lại, và có thể phải SỬA CODE nếu lệch
670
+ · lint-trace T18 sẽ báo "code có test-id mà bảng không có"
671
+
672
+ Cách đúng: dừng ở đây → /map-testids {UC-ID} → /review-tech-docs → chạy lại lệnh này.
673
+
674
+ Vẫn sinh code bây giờ với id tạm theo quy ước {uc-lower}-{screen}-{element}-{type}? (Y/N)
675
+ ```
676
+
677
+ - **N** → dừng, không sinh gì.
678
+ - **Y** → sinh id theo quy ước, **và report cuối phải ghi rõ**: `⚠️ {n} test-id TẠM (chưa vào §4.5.6) — chạy /map-testids {UC-ID} để đưa vào hợp đồng`.
679
+ - **`--yes` (headless)** → coi như **Y**, nhưng dòng cảnh báo ở report là **bắt buộc**. Không được im lặng: `lint-trace --code` T18 là lưới bắt phía sau, và nó chỉ có nghĩa khi người ta biết có gì để tìm.
680
+
681
+ > **Vì sao hỏi chứ không tự sinh như trước.** Tự sinh rồi *"đối chiếu lúc integration"* nghe hợp lý nhưng thực tế là **code quyết định hợp đồng**: QC đọc §4.5.6 thấy rỗng nên đi dò DOM, còn FE đã gắn một bộ id không ai biết. Đến lúc đối chiếu thì cả hai bên đều đã làm xong theo hai hướng khác nhau. Hỏi ở đây là đặt quyết định đó vào tay người, **đúng lúc nó còn rẻ**.
682
+ 2. **TÊN THUỘC TÍNH: đọc `@trace.testid_attr` từ header tech-doc gộp — KHÔNG tự suy từ module.** Đây là **nửa FE của contract FE↔QC**: `/qc-run-test` đọc **chính field này** để cấu hình locator, và `bin/trace-schema.json` khai `artifact: tech-design.md` (`written_by: map-testids`). Hai bên phải đọc **cùng một bản** — nếu FE suy từ module còn QC đọc tech-doc thì FE gắn một kiểu, QC tìm một kiểu, và **không trùng một element nào**.
683
+
684
+ | Đọc được gì | Làm gì |
685
+ |---|---|
686
+ | Header tech-doc có `@trace.testid_attr` | Dùng **nguyên văn** giá trị đó |
687
+ | **Không tìm thấy field** | **Cảnh báo mềm, KHÔNG im lặng hardcode** (khối dưới), rồi mới fallback theo `active_module` |
688
+ | Header `.feature` cũng khai và **LỆCH** với tech-doc | Ưu tiên tech-doc, nhưng **in cả hai giá trị** — không im lặng chọn một bên. `.feature` là lối cũ, chỉ còn cho stack lai |
689
+
690
+ Cảnh báo khi thiếu field (cùng khuôn `/qc-run-test` dùng, để hai nửa của contract nói cùng một giọng):
691
+ ```
692
+ ⚠️ Tech-doc thiếu @trace.testid_attr — fallback theo module ({attr mặc định}).
693
+ Nếu FE dùng thuộc tính khác thì MỌI locator của QC sẽ trượt, và test sẽ đỏ với
694
+ "element not found" — trông y hệt một bug sản phẩm, nên QC đi mở bug thay vì sửa selector.
695
+ Chạy /map-testids {UC-ID} để ghi field này.
696
+ ```
697
+
698
+ **Fallback theo `active_module`** (chỉ khi tech-doc không có field):
662
699
  - web (`react`/`nextjs`/`vue`/`angular`) → `data-testid="..."`
663
700
  - React Native → `testID="..."`
664
701
  - Flutter → `Key('...')` (+ `Semantics(identifier: '...')` khi action cần)
@@ -232,7 +232,7 @@ Kiểm tra `output_path` đã tồn tại chưa.
232
232
 
233
233
  - **Chưa tồn tại → chế độ FRESH.** Tạo doc từ template, chỉ điền (các) UC trong `input_features`. (Section của các UC không thuộc batch này giữ placeholder `{…}` / được thêm ở lần chạy sau.)
234
234
  - **Đã tồn tại → chế độ APPEND.** Doc là tăng dần — không bao giờ regenerate từ đầu (sẽ đè mất chỉnh tay và sign-off của reviewer). Đọc bảng **§10 UC Coverage** và **Changelog** hiện có → `covered_ucs`. Với mỗi UC trong `input_features`, phân loại:
235
- - **UC mới** (không có trong `covered_ucs`) → **thêm** các section của nó: sequence diagram §5 mới **đúng lane platform** (5.A/5.B/5.C, đánh số sau cái cuối cùng hiện có *trong lane đó*); với §4.5 — nếu **platform** này mới với doc → nhóm `### 4.5 — {platform}` mới, ngược lại thêm sub-block `§4.5.1.x {Screen} — {UC}` + row vào §4.5.6 dùng chung của nhóm platform đó (đừng lặp nhóm); row mới ở §3/§4.3/§8/§9. Rồi cập nhật §10 (row khoá theo platform×SC) và thêm một row Changelog **theo format ở Bước 1b**.
235
+ - **UC mới** (không có trong `covered_ucs`) → **thêm** các section của nó: sequence diagram §5 mới **đúng lane platform** (5.A/5.B/5.C, đánh số sau cái cuối cùng hiện có *trong lane đó*); với §4.5 — nếu **platform** này mới với doc → nhóm `### 4.5 — {platform}` mới, ngược lại thêm sub-block `§4.5.1.x {Screen} — {UC}` (đừng lặp nhóm). **§4.5.6: chỉ tạo KHUNG bảng rỗng nếu chưa — KHÔNG ghi row nào**, xem §Phân vai §4.5.6; row mới ở §3/§4.3/§8/§9. Rồi cập nhật §10 (row khoá theo platform×SC) và thêm một row Changelog **theo format ở Bước 1b**.
236
236
  - **UC đã phủ được trỏ lại** (có trong `covered_ucs`) → đây là refresh/mở rộng có chủ đích (vd tech lead giờ trỏ vào BDD `web/` của một UC mà backend đã thiết kế, hoặc BDD bump version). Xác nhận trước khi đụng nội dung có sẵn:
237
237
  ```
238
238
  ↻ {UC-ID} đã có trong {TICKET-ID}-tech-design.md.
@@ -378,7 +378,25 @@ Ghi/mở rộng `{output_path}` dùng template dưới đây, chỉ sinh **nội
378
378
  - **§1/§2** (Overview/Actors, Architecture) là cấp PRD: viết ở lần chạy đầu; các lần sau chỉ mở rộng nếu batch thêm actor/integration thật sự mới.
379
379
  - **§10 UC Coverage** — một row UC (có cột Platforms) + bảng con coverage-scenario khoá theo **(platform, SC)** — mỗi platform×SC một row, vì cùng số SC ở platform khác nhau là scenario khác nhau. Đây là mỏ neo mà chế độ APPEND đọc. Luôn cập nhật nó cho (các) UC/platform của batch.
380
380
 
381
- **Chế độ APPEND (doc đã tồn tại):** **đừng** viết lại section có sẵn. Chèn diagram §5 của UC batch **vào đúng lane platform** (5.A/5.B/5.C, đánh số sau cái cuối trong lane đó, tiêu đề `platform · SC`), các row mới ở §3/§4.3/§8/§9; với §4.5 — platform mới → nhóm `### 4.5 — {platform}` mới, ngược lại thêm sub-block `§4.5.1.x {Screen} — {UC}` + row vào §4.5.6 dùng chung của nhóm (không lặp nhóm); rồi cập nhật §10 (row khoá theo platform×SC) và thêm một row Changelog **theo format ở Bước 1b**. Bump `@trace.revision` và làm mới `@trace.ucs` / `@trace.platforms` ở header, và cập nhật entry của platform vừa đụng trong map `@trace.bdd_versions` (vd set `web=2.0`, giữ nguyên `system`).
381
+ **Chế độ APPEND (doc đã tồn tại):** **đừng** viết lại section có sẵn. Chèn diagram §5 của UC batch **vào đúng lane platform** (5.A/5.B/5.C, đánh số sau cái cuối trong lane đó, tiêu đề `platform · SC`), các row mới ở §3/§4.3/§8/§9; với §4.5 — platform mới → nhóm `### 4.5 — {platform}` mới, ngược lại thêm sub-block `§4.5.1.x {Screen} — {UC}` (không lặp nhóm). **§4.5.6: chỉ khung rỗng, KHÔNG ghi row** — xem §Phân vai §4.5.6; rồi cập nhật §10 (row khoá theo platform×SC) và thêm một row Changelog **theo format ở Bước 1b**. Bump `@trace.revision` và làm mới `@trace.ucs` / `@trace.platforms` ở header, và cập nhật entry của platform vừa đụng trong map `@trace.bdd_versions` (vd set `web=2.0`, giữ nguyên `system`).
382
+
383
+ ## Phân vai §4.5.6 Test Selectors — lệnh này KHÔNG ghi row
384
+
385
+ Bảng §4.5.6 là **hợp đồng test-id FE↔QC**, và nó có **đúng một người ghi**: `/map-testids`.
386
+ Lệnh này chỉ:
387
+
388
+ - tạo **khung bảng rỗng** (dòng tiêu đề + dòng phân cách) trong mỗi nhóm `### 4.5 — {platform}` client;
389
+ - ghi `@trace.testid_attr` ở header dưới dạng **placeholder** (giá trị thật do `/map-testids` điền).
390
+
391
+ > **Vì sao tách người ghi.** Trước đây cả hai lệnh cùng ghi bảng, và luật chống giẫm chân là một
392
+ > câu văn xuôi. Quan trọng hơn: hợp đồng phải chốt **trước** `/generate-code`, để FE và QC cùng
393
+ > đọc một bản đã đóng băng rồi **chạy song song**. Nếu lệnh này dự đoán id còn `/map-testids` sửa
394
+ > lại sau khi code xong thì hợp đồng thành thứ **do code quyết định** — đúng cái nó sinh ra để
395
+ > chống.
396
+ >
397
+ > **Next của lệnh này là `/map-testids`**, không phải `/review-tech-docs`. `/review-tech-docs`
398
+ > sẽ **NEEDS_FIX** nếu có §4.5 client mà §4.5.6 rỗng (T6) — tech-doc có phần UI mà không khai
399
+ > test selector thì chưa viết xong, như có §4 API mà không khai endpoint.
382
400
 
383
401
  <!--
384
402
  ════════════════════════════════════════════════════════════════════════════
@@ -422,6 +440,7 @@ Ghi/mở rộng `{output_path}` dùng template dưới đây, chỉ sinh **nội
422
440
  @trace.service: {service — từ header BDD @trace.service}
423
441
  @trace.module: {module liên quan — vd dotnet, angular}
424
442
  @trace.platforms: {system | web | app | webview | … — tuỳ thư mục BDD nào tồn tại}
443
+ @trace.testid_attr: {TÊN THUỘC TÍNH chứa test-id của stack client — web `data-testid`|`data-test`|`data-qa` · React Native `testID` · Flutter `Key`/`Semantics(identifier:)` · native iOS `accessibilityIdentifier`. MỘT giá trị cho cả doc (khác GIÁ TRỊ test-id từng element — cái đó ở §4.5.6). Do `/map-testids` ghi. Để trống nếu doc chỉ phủ platform `system`.}
425
444
  @trace.bdd_versions: {MAP theo từng platform — số nhiều, KHÁC @trace.bdd_version (scalar) của .feature — vd system=1.5, web=1.9, app=1.7; chỉ platform có mặt. Mỗi feature mang bdd_version riêng; đừng gộp về một số.}
426
445
  @trace.api_source: {existing | —}
427
446
  @trace.revision: 1
@@ -8,7 +8,31 @@
8
8
  > forwarding vào figma-components catalog, patch các usage site, và ghi map §4.5.6 — để QC
9
9
  > định vị element bằng id thay vì scan lúc runtime.
10
10
 
11
- Usage: `/map-testids {UC-ID}`
11
+ Usage: `/map-testids {UC-ID}` · `/map-testids {UC-ID} --from-code`
12
+
13
+ ## Hai chế độ — chọn theo việc code đã có hay chưa
14
+
15
+ | Chế độ | Khi nào | Nguồn element | Đụng code? | Ai chạy |
16
+ |---|---|---|---|---|
17
+ | **mặc định** (không cờ) | Feature mới — **chạy TRƯỚC `/generate-code`** | design-spec + step `When` của `.feature` | **Không** | Người viết tech-doc |
18
+ | `--from-code` | Brownfield — màn đã có code từ trước framework | **đọc code thật** + design-spec + BDD | Có (patch attribute) | Dev · chạy **một lần** mỗi UC cũ |
19
+
20
+ Chế độ mặc định **bỏ qua Step 3 và Step 4** (patch catalog / patch usage site): chưa có code để
21
+ patch, mọi element đều là `new`. Chỉ chạy Step 1 → 2 → 5.
22
+
23
+ > **Vì sao lệnh này chạy TRƯỚC `/generate-code`.** Nguyên liệu để **đặt tên** test-id có từ
24
+ > trước code — Step 1 lấy element từ design-spec + step `When`, cả hai đều thuộc phase Tech
25
+ > Design. Chốt hợp đồng ở đây rồi thì:
26
+ >
27
+ > ```
28
+ > /review-tech-docs (APPROVED)
29
+ > ├──→ /generate-code FE gắn attribute theo hợp đồng
30
+ > └──→ /qc-design-test QC viết test case + script theo CÙNG hợp đồng
31
+ > ```
32
+ >
33
+ > Hai nhánh **không chờ nhau** vì cùng đọc một bản đã đóng băng, không đọc output của nhau.
34
+ > Chạy sau code thì QC phải xếp hàng, và `/generate-code` không có gì để đọc nên sẽ tự sinh id
35
+ > — hợp đồng thành thứ do code quyết định.
12
36
 
13
37
  ## Gate
14
38
  # Gate — Quy trình vào chuẩn cho mọi lệnh
@@ -164,7 +188,7 @@ Mỗi dòng ⚠️/🔴 phải ứng với một trạng thái **context-loader
164
188
  🔴/⚠️ (không chặn ≠ không báo — người đọc log sau này vẫn cần thấy).
165
189
 
166
190
 
167
- *Lưu ý: Với lệnh này, target ở Bước 1 là một UC-ID. Đọc `.feature` FE của UC (web/app), các màn Design Spec của nó, tech-doc gộp `{paths.tech_docs_dir}/{domain}/{prd-slug}/tech-docs/{TICKET-ID}-tech-design.md` (§4.5.6 của platform, nếu có — bảng này gộp mọi UC của platform, **lọc theo cột "Serves SC" khớp SC của UC này** qua §10), và figma-components catalog cho `active_module`.*
191
+ *Lưu ý: Với lệnh này, target ở Bước 1 là một UC-ID. Đọc `.feature` FE của UC (web/app), các màn Design Spec của nó, tech-doc gộp `{paths.tech_docs_dir}/{domain}/{prd-slug}/tech-docs/{TICKET-ID}-tech-design.md` (§4.5.6 của platform, nếu có — bảng này gộp mọi UC của platform, **lọc theo cột "Phục vụ SC" khớp SC của UC này** qua §10), và figma-components catalog cho `active_module`.*
168
192
 
169
193
  ## Context
170
194
  **BẮT BUỘC — đọc `.agent/steps/context-loader.md` và thực thi TOÀN BỘ quy trình trong đó**,
@@ -188,8 +212,15 @@ Phân giải attribute test-id từ `@trace.testid_attr` (hoặc theo module): w
188
212
 
189
213
  Từ các step `When` trong `.feature` FE của UC + các màn Design Spec, liệt kê mọi element **có action** mà scenario chạm tới (button, input, link, select, toggle, form-submit). Bỏ qua text/label tĩnh. Với mỗi cái, phân giải component render và phân loại:
190
214
  - **reused** — khớp một row trong figma-components catalog (component design-system dùng chung);
191
- - **existing** — component riêng của feature đã có trong codebase (brownfield);
192
- - **new** — chưa code (để `/generate-code` lo; chỉ ghi lại id dự kiến).
215
+ - **existing** — component riêng của feature đã có trong codebase (brownfield); *chỉ gặp ở `--from-code`*;
216
+ - **new** — chưa code (để `/generate-code` lo; chỉ ghi lại id dự kiến). *Chế độ mặc định: **mọi** element đều là nhóm này.*
217
+
218
+ Phân loại này là **nội bộ lúc chạy** — dùng để rẽ nhánh Step 2–4. **Không ghi vào bảng §4.5.6**
219
+ (xem Step 5): nó đổi theo thời gian, một element `new` thành `existing` ngay khi dev viết code.
220
+
221
+ **Cột `Component` của bảng** lấy từ **§4.5.1 Cây Component trong chính tech-doc này** (do
222
+ `/generate-tech-docs` vẽ từ design-spec) — trỏ `§4.5.1.x`. §4.5.1 chưa có (ca `--from-code` ghi
223
+ file tối thiểu) thì ghi tên component.
193
224
 
194
225
  ## Step 2 — Phân giải test-id ổn định cho mỗi element
195
226
 
@@ -197,7 +228,7 @@ Từ các step `When` trong `.feature` FE của UC + các màn Design Spec, li
197
228
  - **Reused:** id được áp ở **usage site** (không bake vào component dùng chung) → gán theo cùng quy ước.
198
229
  - **Cross-platform:** nếu §4.5.6 của platform **kia** (block `web`/`app` trong cùng tech-doc gộp) đã có id cho cùng element logic, **dùng lại id value đó** (chỉ attribute khác theo platform) để web và app nhất quán và logic QC tái dùng được.
199
230
 
200
- ## Step 3 — Đảm bảo component tái dùng forward được test-id (catalog)
231
+ ## Step 3 — Đảm bảo component tái dùng forward được test-id (catalog) *(chỉ `--from-code`)*
201
232
 
202
233
  Với mỗi component **reused** có action, tra section **`## Test-ID Forwarding`** của catalog (`{paths.domain_knowledge_dir}/figma-components/{active_module}.md`):
203
234
  - **Đã ghi prop forwarding** → dùng nó ở usage site (Step 4).
@@ -207,17 +238,66 @@ Với mỗi component **reused** có action, tra section **`## Test-ID Forwardin
207
238
 
208
239
  In mọi row catalog được thêm và mọi component dùng chung được patch (chúng đụng code dùng chung — nêu ra để review).
209
240
 
210
- ## Step 4 — Patch usage site (chỉ EXTEND)
241
+ ## Step 4 — Patch usage site (chỉ EXTEND) *(chỉ `--from-code`)*
211
242
 
212
243
  Với mỗi element có action trong các màn **existing/reused** của UC này, thêm test-id ở usage site — attribute thô cho element thường, hoặc prop forwarding cho component tái dùng — với id từ Step 2. **EXTEND mode:** chỉ đụng attribute/prop; không refactor gì khác. Bỏ qua element đã mang đúng id (idempotent).
213
244
 
214
245
  ## Step 5 — Ghi/làm mới map §4.5.6 Test Selectors
215
246
 
216
247
  Tạo hoặc cập nhật §4.5.6 (block platform tương ứng) trong tech-doc gộp `{paths.tech_docs_dir}/{domain}/{prd-slug}/tech-docs/{TICKET-ID}-tech-design.md`:
217
- - Nếu tech-doc tồn tại → cập nhật bảng §4.5.6 của platform này (thêm block §4.5 cho platform nếu chưa có).
248
+ - Nếu tech-doc tồn tại → **hai việc, không bỏ việc nào**:
249
+ - **(a) Header.** Đảm bảo khối `@trace` có `@trace.testid_attr` mang đúng giá trị đã phân giải ở Step 0. Thiếu hẳn, hoặc còn placeholder `{…}` → điền. **Đã có giá trị thật mà LỆCH với giá trị vừa phân giải → DỪNG, in cả hai giá trị và hỏi người dùng chọn**; không tự ghi đè.
250
+ - **(b) Bảng.** Cập nhật bảng §4.5.6 của platform này (thêm block §4.5 cho platform nếu chưa có).
218
251
  - Nếu **chưa** tồn tại (pure brownfield) → ghi một file tối thiểu: header `@trace` (gồm `@trace.testid_attr`) + §4.5.6. `/generate-tech-docs` điền các section còn lại sau; nó không được ghi đè các id §4.5.6 mà lệnh này đã ghi.
219
252
 
220
- Mỗi row: `Test-ID | Element | Component (reused/existing/new) | Action | Serves SC`.
253
+ > **Vì sao (a) lệch thì DỪNG chứ không ghi đè.** `@trace.testid_attr` là *tên thuộc tính* mà **mọi** locator QC của PRD này bám vào (`/qc-run-test` đọc để cấu hình `get_by_test_id`). Ghi đè sai một lần là làm **trượt toàn bộ** script của PRD — và test sẽ đỏ với `element not found`, trông y hệt một bug sản phẩm, nên QC đi mở bug thay vì sửa selector. Lệch nghĩa là một trong hai đang sai: FE vừa đổi convention, hoặc `active_module` khai sai. Cả hai đều cần người nhìn, không đoán được từ đây.
254
+
255
+ Mỗi row — **5 cột, đúng thứ tự của template** (`templates/tech-design.template.md` §4.5.6):
256
+
257
+ `Test-ID | Element | Component | Action | Phục vụ SC (UC · SC)`
258
+
259
+ - **Component** — trỏ `§4.5.1.x` nếu §4.5.1 đã vẽ; chưa có (ca brownfield ghi file tối thiểu) thì ghi tên component.
260
+ - **Phục vụ SC** — danh sách `(UC · SC)` mà id này phục vụ. Một id phục vụ nhiều UC là **bình thường**: tiền tố UC trong tên id chỉ nói UC nào giới thiệu element đó đầu tiên.
261
+
262
+ > **KHÔNG ghi phân loại `reused`/`existing`/`new` vào bảng.** Nó là phân loại **lúc chạy** của Step 1 (dùng để rẽ nhánh Step 2–4), và nó **đổi theo thời gian** — một element `new` thành `existing` ngay khi dev viết code. Nhét dữ liệu biến thiên vào bảng hợp đồng là làm bảng sai dần mà không ai cập nhật.
263
+ >
264
+ > **Thứ tự cột là load-bearing:** `lint-trace` **T15** đọc cột "Phục vụ SC" theo **vị trí** (ô nội dung cuối cùng) để đối chiếu với `.feature`. Viết sai thứ tự là T15 đọc nhầm ô.
265
+
266
+ ## Step 5b — Làm mất hiệu lực `qc_status` của SC bị ảnh hưởng *(chỉ khi ĐỔI id đã có)*
267
+
268
+ Chạy **chỉ khi** Step 5 làm đổi giá trị một test-id **đã tồn tại** trong bảng (thêm row mới
269
+ không kích hoạt bước này — chưa có script nào bám id mới).
270
+
271
+ Đổi một id nghĩa là mọi script QC bám id cũ **hết đúng**: nó đang định vị một element không còn
272
+ mang id đó. Giữ `qc_status = pass` ở đó là **báo cáo sai** — `rules/workflow.md` §*"Làm mất hiệu
273
+ lực ≠ ghi đè"*: *"lệnh nào làm giá trị đó HẾT ĐÚNG thì BẮT BUỘC hạ nó về giá trị 'chưa biết'"*.
274
+
275
+ ```
276
+ với mỗi id ĐỔI:
277
+ đọc cột "Phục vụ SC" của row đó → danh sách {UC-ID}-SC{N}
278
+ mở sổ {paths.trace_dir}/{domain}/{prd-slug}/{UC-ID}-{platform}.tsv
279
+ với mỗi SC trong danh sách:
280
+ qc_status → not_run (chỉ hạ từ `pass`/`fail`; đang `not_run`/`skip` thì để yên)
281
+ qc_run_at → —
282
+ ```
283
+
284
+ **KHÔNG đụng `qc_owner` và `qc_blocked_by`.** `rules/workflow.md` miễn trừ tường minh hai cột
285
+ này: chúng là **con trỏ tới bug**, và đổi một test-id không làm con bug biến mất. Xoá đi là mất
286
+ đường về bug đang mở.
287
+
288
+ **Không có sổ trace cho SC đó** (chưa chạy `/generate-bdd`, hoặc SC mới) → bỏ qua, không tạo sổ.
289
+
290
+ In ra ở report:
291
+ ```
292
+ ⚠️ {n} scenario có script QC bám id CŨ — qc_status hạ về not_run: {danh sách SC}
293
+ Chạy /qc-design-test (hoặc /qc-run-test) lại cho các UC đó.
294
+ ```
295
+
296
+ > **Vì sao hạ chứ không giữ.** `rules/workflow.md`: *"`pass` **không** mang nghĩa 'test đã chạy
297
+ > xanh' — nó mang nghĩa 'scenario này đã được nghiệm thu theo spec **hiện tại**'."* Một script
298
+ > định vị bằng id không còn tồn tại thì không nghiệm thu được gì cả. Và QC biết phải chạy lại
299
+ > bằng đúng cách họ vẫn biết với mọi thay đổi spec khác: dashboard hiện SC đó `not_run` thay vì
300
+ > `pass` — không phải học cơ chế mới nào.
221
301
 
222
302
  ## Step 6 — Handoff
223
303