@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
package/bin/build.js CHANGED
@@ -56,6 +56,17 @@ const RUNTIME_READ = {
56
56
  'steps/report-footer.md':
57
57
  '**Đọc `.agent/steps/report-footer.md`** và áp đúng khuôn footer trong đó (Status Badge ·\n' +
58
58
  'Output Artifacts · Next) cho report cuối, kèm khối bên dưới.',
59
+
60
+ // Vào đây ở B12: nó được 5 lệnh qc-* include, nên inline là nhân 5 bản của cùng một
61
+ // quy trình — đúng loại phình mà test "mức slim" tồn tại để canh (nó bắt được thật).
62
+ 'steps/qc-scope.md':
63
+ '**BẮT BUỘC — đọc `.agent/steps/qc-scope.md` và thực thi TOÀN BỘ quy trình trong đó**,\n' +
64
+ 'rồi mới tiếp tục phần bên dưới.\n' +
65
+ '\n' +
66
+ 'Nó chốt bốn thứ mà mọi trạm QC đều cần: `TICKET-ID` · `active_platform` ·\n' +
67
+ '`qc_artifact_dir` · `uc_list` (kèm trạng thái BDD từng UC, và cờ `--include-draft`).\n' +
68
+ 'Bỏ qua thì artifact QC ghi vào **sai thư mục** và `qc_status` ghi vào **sai sổ trace** —\n' +
69
+ 'cả hai đều xảy ra trong im lặng, không có bước nào phía sau bắt được.',
59
70
  };
60
71
 
61
72
  /**
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) => {
@@ -47,7 +47,7 @@
47
47
  {
48
48
  "upstream": "agents/4-qa-tc-designer.md",
49
49
  "targets": [],
50
- "state": "undecided",
50
+ "state": "skipped",
51
51
  "why": "dinh nghia agent cua upstream; framework dung command + gate _agent_mode thay cho agent file"
52
52
  },
53
53
  {
@@ -329,58 +329,83 @@
329
329
  },
330
330
  {
331
331
  "upstream": "skills/qa-tc-designer/api-testcase-designer.md",
332
- "targets": [],
333
- "state": "undecided"
332
+ "targets": [
333
+ "skills/qc/qa-designer/functional/api.md"
334
+ ],
335
+ "state": "ported",
336
+ "base_sha": "211c18728baf029a7572f0ffb161e92a933fa8fb"
334
337
  },
335
338
  {
336
339
  "upstream": "skills/qa-tc-designer/api-testcase-designer/api/flows/auth-sequence.md",
337
- "targets": [],
338
- "state": "undecided"
340
+ "targets": [
341
+ "skills/qc/qa-designer/api/auth-sequence.md"
342
+ ],
343
+ "state": "ported",
344
+ "base_sha": "69d60c12fe6d641638132581cce1dc6c955354a6"
339
345
  },
340
346
  {
341
347
  "upstream": "skills/qa-tc-designer/api-testcase-designer/api/flows/common-flows.md",
342
348
  "targets": [],
343
- "state": "undecided"
349
+ "state": "skipped",
350
+ "why": "B12: ban nhap cu, nam gon trong auth-sequence.md + crud-sequence.md (dai va chi tiet hon)."
344
351
  },
345
352
  {
346
353
  "upstream": "skills/qa-tc-designer/api-testcase-designer/api/flows/crud-sequence.md",
347
- "targets": [],
348
- "state": "undecided"
354
+ "targets": [
355
+ "skills/qc/qa-designer/api/crud-sequence.md"
356
+ ],
357
+ "state": "ported",
358
+ "base_sha": "65bf214d9f9beabe3c40b8697ba7c058adfd8bb5"
349
359
  },
350
360
  {
351
361
  "upstream": "skills/qa-tc-designer/api-testcase-designer/api/references/common-headers.md",
352
- "targets": [],
353
- "state": "undecided"
362
+ "targets": [
363
+ "skills/qc/qa-designer/api/common-headers.md"
364
+ ],
365
+ "state": "ported",
366
+ "base_sha": "74f10a36bec7b40f4d9de2d0928798cb6fad0c2c"
354
367
  },
355
368
  {
356
369
  "upstream": "skills/qa-tc-designer/api-testcase-designer/api/references/error-codes.md",
357
370
  "targets": [],
358
- "state": "undecided"
371
+ "state": "skipped",
372
+ "why": "B12: ban nhap cu, nam gon trong http-status-codes.md (95 dong, day du hon). Phan header cua no nam trong common-headers.md. Lay ca hai la co hai bang tra cung mot thu roi chung lech nhau."
359
373
  },
360
374
  {
361
375
  "upstream": "skills/qa-tc-designer/api-testcase-designer/api/references/http-status-codes.md",
362
- "targets": [],
363
- "state": "undecided"
376
+ "targets": [
377
+ "skills/qc/qa-designer/api/http-status-codes.md"
378
+ ],
379
+ "state": "ported",
380
+ "base_sha": "a82baa99350f63955245764a6aa57c9dca6d055f"
364
381
  },
365
382
  {
366
383
  "upstream": "skills/qa-tc-designer/api-testcase-designer/api/templates/auth-chain.md",
367
- "targets": [],
368
- "state": "undecided"
384
+ "targets": [
385
+ "skills/qc/qa-designer/api/auth-chain.md"
386
+ ],
387
+ "state": "ported",
388
+ "base_sha": "ef8f9761f1b53d0ccd752f8fe0c13a427fa15f45"
369
389
  },
370
390
  {
371
391
  "upstream": "skills/qa-tc-designer/api-testcase-designer/api/templates/endpoint.md",
372
- "targets": [],
373
- "state": "undecided"
392
+ "targets": [
393
+ "skills/qc/qa-designer/api/endpoint.md"
394
+ ],
395
+ "state": "ported",
396
+ "base_sha": "7b38edd94e5efd37ff561f53cff92adedabaff7f"
374
397
  },
375
398
  {
376
399
  "upstream": "skills/qa-tc-designer/e2e-testcase-designer.md",
377
400
  "targets": [],
378
- "state": "undecided"
401
+ "state": "skipped",
402
+ "why": "B12: tuong ung skills/qc/qa-designer/e2e/journey.md. Chua doi chieu noi dung — de danh cho lan ra tang E2E rieng."
379
403
  },
380
404
  {
381
405
  "upstream": "skills/qa-tc-designer/integration-testcase-designer.md",
382
406
  "targets": [],
383
- "state": "undecided"
407
+ "state": "skipped",
408
+ "why": "B12: tuong ung 4 skill integration/{api,db,gui,kafka}.md. Chua doi chieu noi dung — de danh."
384
409
  },
385
410
  {
386
411
  "upstream": "skills/qa-tc-designer/manual-excel-testcase.md",
@@ -391,47 +416,70 @@
391
416
  {
392
417
  "upstream": "skills/qa-tc-designer/nfr-testcase-designer.md",
393
418
  "targets": [],
394
- "state": "undecided"
419
+ "state": "skipped",
420
+ "why": "B12: tuong ung skills/qc/qa-designer/non-functional.md. Chua doi chieu noi dung — de danh."
395
421
  },
396
422
  {
397
423
  "upstream": "skills/qa-tc-designer/shared/action-keywords-glossary.md",
398
- "targets": [],
399
- "state": "undecided"
424
+ "targets": [
425
+ "skills/qc/qa-designer/shared/action-keywords-glossary.md"
426
+ ],
427
+ "state": "ported",
428
+ "base_sha": "df56df91eea42342537ccf67b5946df32d98dbf4"
400
429
  },
401
430
  {
402
431
  "upstream": "skills/qa-tc-designer/shared/duplicate-check-procedure.md",
403
- "targets": [],
404
- "state": "undecided"
432
+ "targets": [
433
+ "skills/qc/qa-designer/shared/duplicate-check-procedure.md"
434
+ ],
435
+ "state": "ported",
436
+ "base_sha": "c59357a56b289bb9fb7fae8490db37109209cb3a"
405
437
  },
406
438
  {
407
439
  "upstream": "skills/qa-tc-designer/shared/implicit-scenarios.md",
408
- "targets": [],
409
- "state": "undecided"
440
+ "targets": [
441
+ "skills/qc/qa-designer/shared/implicit-scenarios.md"
442
+ ],
443
+ "state": "ported",
444
+ "base_sha": "6eb801a137ef4dfda97870c1eb36db62f3c782bc"
410
445
  },
411
446
  {
412
447
  "upstream": "skills/qa-tc-designer/shared/precision-rules.md",
413
- "targets": [],
414
- "state": "undecided"
448
+ "targets": [
449
+ "skills/qc/qa-designer/shared/precision-rules.md"
450
+ ],
451
+ "state": "ported",
452
+ "base_sha": "1c2005b55bd94c3e8fa25dfa12a29c8b23f7c01e"
415
453
  },
416
454
  {
417
455
  "upstream": "skills/qa-tc-designer/shared/read-doc-gap-inputs.md",
418
- "targets": [],
419
- "state": "undecided"
456
+ "targets": [
457
+ "skills/qc/qa-designer/shared/read-doc-gap-inputs.md"
458
+ ],
459
+ "state": "ported",
460
+ "base_sha": "a7e113b19f35aa47acf91e55cdfb3f630539806c"
420
461
  },
421
462
  {
422
463
  "upstream": "skills/qa-tc-designer/shared/skill-decision-tree.md",
423
- "targets": [],
424
- "state": "undecided"
464
+ "targets": [
465
+ "skills/qc/qa-designer/shared/skill-decision-tree.md"
466
+ ],
467
+ "state": "ported",
468
+ "base_sha": "57125f0c21512f2abcc55d00420e92c84c95fc8f"
425
469
  },
426
470
  {
427
471
  "upstream": "skills/qa-tc-designer/shared/tc-metadata-format.md",
428
- "targets": [],
429
- "state": "undecided"
472
+ "targets": [
473
+ "skills/qc/qa-designer/shared/tc-metadata-format.md"
474
+ ],
475
+ "state": "ported",
476
+ "base_sha": "a31d66a7a8dab19cf821c9102a88282b09e16cb9"
430
477
  },
431
478
  {
432
479
  "upstream": "skills/qa-tc-designer/ui-testcase-designer.md",
433
480
  "targets": [],
434
- "state": "undecided"
481
+ "state": "skipped",
482
+ "why": "B12: framework tach tang UI thanh 3 skill rieng (functional/gui-screen · gui-feature · integration/gui) theo cay quyet dinh, thay vi mot file 341 dong. Ky thuat EP/BVA/Decision-Table/State-Transition da co trong 3 skill do; khuon TC + do chinh xac da tach ra shared/. Con lai la phan khai agent + orchestrator — framework khong co."
435
483
  },
436
484
  {
437
485
  "upstream": "skills/qa-tc-reviewer/api-test-reviewer.md",
@@ -440,23 +488,35 @@
440
488
  },
441
489
  {
442
490
  "upstream": "skills/qa-tc-reviewer/e2e-test-reviewer.md",
443
- "targets": [],
444
- "state": "undecided"
491
+ "targets": [
492
+ "skills/qc/qa-reviewer/test-case/e2e.md"
493
+ ],
494
+ "state": "ported",
495
+ "base_sha": "276d0516f95421c726c8c55dc84c3c9244cf70e1"
445
496
  },
446
497
  {
447
498
  "upstream": "skills/qa-tc-reviewer/exploratory-test-reviewer.md",
448
- "targets": [],
449
- "state": "undecided"
499
+ "targets": [
500
+ "skills/qc/qa-reviewer/test-case/exploratory.md"
501
+ ],
502
+ "state": "ported",
503
+ "base_sha": "4e068bb896739b977a6a5b1f638010d498c46aef"
450
504
  },
451
505
  {
452
506
  "upstream": "skills/qa-tc-reviewer/integration-test-reviewer.md",
453
- "targets": [],
454
- "state": "undecided"
507
+ "targets": [
508
+ "skills/qc/qa-reviewer/test-case/integration.md"
509
+ ],
510
+ "state": "ported",
511
+ "base_sha": "639b54afb68df83b22168581c5c57b0f91d9cf6a"
455
512
  },
456
513
  {
457
514
  "upstream": "skills/qa-tc-reviewer/nfr-test-reviewer.md",
458
- "targets": [],
459
- "state": "undecided"
515
+ "targets": [
516
+ "skills/qc/qa-reviewer/test-case/non-functional.md"
517
+ ],
518
+ "state": "ported",
519
+ "base_sha": "5747c7a6bdfaaea1ff3e958688b38b3aeaf9b15a"
460
520
  },
461
521
  {
462
522
  "upstream": "skills/qa-tc-reviewer/script-e2e-reviewer.md",
@@ -485,18 +545,28 @@
485
545
  },
486
546
  {
487
547
  "upstream": "skills/qa-tc-reviewer/shared/read-doc-gap-inputs.md",
488
- "targets": [],
489
- "state": "undecided"
548
+ "targets": [
549
+ "skills/qc/qa-reviewer/shared/read-doc-gap-inputs.md"
550
+ ],
551
+ "state": "ported",
552
+ "base_sha": "3caa1562399ef1167ac2dc620add2c6c729e55df"
490
553
  },
491
554
  {
492
555
  "upstream": "skills/qa-tc-reviewer/shared/review-file-template.md",
493
- "targets": [],
494
- "state": "undecided"
556
+ "targets": [
557
+ "skills/qc/qa-reviewer/shared/review-file-template.md"
558
+ ],
559
+ "state": "ported",
560
+ "base_sha": "bd596393ecc016cbea106835681a4979c0a9ca0e"
495
561
  },
496
562
  {
497
563
  "upstream": "skills/qa-tc-reviewer/ui-test-reviewer.md",
498
- "targets": [],
499
- "state": "undecided"
564
+ "targets": [
565
+ "skills/qc/qa-reviewer/test-case/functional.md",
566
+ "skills/qc/qa-reviewer/shared/review-check-groups.md"
567
+ ],
568
+ "state": "ported",
569
+ "base_sha": "54f81eb6a132d3fb331f18ce0f1a7eb3a9cf19df"
500
570
  }
501
571
  ],
502
572
  "$decisions": {
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 —
@@ -1198,6 +1222,12 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
1198
1222
  // sau đều đoán. Đây đúng loại việc con người không làm đáng tin bằng sự cẩn thận.
1199
1223
  // → R16(b).
1200
1224
  //
1225
+ // (3) Một file NHẬN nội dung port mà KHÔNG đóng dấu nguồn gốc. Bản đồ khai đầy đủ, sha
1226
+ // đúng, self-check xanh — nhưng người mở file không biết đây là bản port, nên lần
1227
+ // đồng bộ sau bản đồ biết BASE mà người sửa file thì không. Ca thật: B12 gộp nội dung
1228
+ // `api-testcase-designer.md` vào `functional/api.md`. (b) và (e) đều mù vì chúng chỉ
1229
+ // so KHI frontmatter có khai. → R16(f).
1230
+ //
1201
1231
  // (a) là cái quan trọng nhất về lâu dài: file upstream KHÔNG có entry trong bản đồ =
1202
1232
  // bị bỏ quên trong im lặng. `state: skipped` (có lý do) là câu trả lời hợp lệ; VẮNG MẶT
1203
1233
  // thì không. Cùng khuôn với `checkpoint_levels` và `strict_use_check`: khai tường minh
@@ -1324,6 +1354,30 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
1324
1354
  }
1325
1355
  }
1326
1356
  }
1357
+
1358
+ // ── (f) target `ported` phải KHAI dấu nguồn gốc, không chỉ "khai đúng nếu có khai" ──
1359
+ //
1360
+ // Vì sao cần nhánh riêng: (b) và (e) đều chạy trong `if (m)` / `if (mp)` — chúng so
1361
+ // frontmatter với bản đồ KHI frontmatter có khai. File không khai gì thì cả hai im lặng.
1362
+ // Ca thật (B12): `functional/api.md` nhận nội dung port từ `api-testcase-designer.md`,
1363
+ // vào bản đồ đầy đủ với base_sha đúng, self-check XANH — mà đọc file thì không ai biết
1364
+ // nó là bản port. Mất `upstream_path` + `upstream_sha` ở file đích là mất BASE của merge
1365
+ // ba chiều lần sau: bản đồ biết, còn người sửa file thì không.
1366
+ for (const e of entries.filter(x => x.state === 'ported')) {
1367
+ for (const t of (e.targets || [])) {
1368
+ const tp = path.join(ROOT, t);
1369
+ if (!fs.existsSync(tp)) continue; // đã báo ở (c)
1370
+ const txt = fs.readFileSync(tp, 'utf8');
1371
+ const lack = ['upstream_path', 'upstream_sha']
1372
+ .filter(f => !new RegExp(`^${f}: .+$`, 'm').test(txt));
1373
+ if (lack.length) {
1374
+ err('R16', `${t} là target \`ported\` nhưng frontmatter THIẾU ${lack.join(' + ')}`,
1375
+ `Bản đồ khai nó port từ \`${e.upstream}\`, còn chính file thì không đóng dấu.
1376
+ ` +
1377
+ ' Người mở file không biết đây là bản port, và lần đồng bộ sau mất một chiều BASE.');
1378
+ }
1379
+ }
1380
+ }
1327
1381
  }
1328
1382
  }
1329
1383
  }