@educa-corp/sdd-framework 0.8.1 → 0.9.1

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 (66) hide show
  1. package/bin/lint-trace.js +200 -1
  2. package/bin/self-check.js +0 -146
  3. package/bin/trace-schema.json +12 -3
  4. package/core/FRAMEWORK_VERSION +1 -1
  5. package/core/commands/generate-code.md +45 -2
  6. package/core/commands/propose-scenario.md +1 -1
  7. package/core/commands/qc-analyze.md +22 -260
  8. package/core/commands/qc-design-test.md +1 -1
  9. package/core/commands/qc-plan.md +4 -7
  10. package/core/commands/qc-run-test.md +1 -1
  11. package/core/commands/refine-prd.md +20 -47
  12. package/core/commands/report-bug.md +1 -1
  13. package/core/commands/review-context.md +1 -27
  14. package/core/commands/validate-traces.md +178 -3
  15. package/core/skills/qc/qa-analyst/DOC_GAPS.template.md +63 -0
  16. package/core/skills/qc/qa-analyst/acceptance-criteria.md +2 -4
  17. package/core/skills/qc/qa-analyst/business-rules.md +4 -38
  18. package/core/skills/qc/qa-analyst/data-flow.md +3 -5
  19. package/core/skills/qc/qa-analyst/spec-breakdown.md +7 -9
  20. package/core/skills/qc/qa-designer/e2e/journey.md +2 -2
  21. package/core/skills/qc/qa-designer/exploratory/charter.md +1 -1
  22. package/core/skills/qc/qa-designer/exploratory/explore-to-functional.md +1 -1
  23. package/core/skills/qc/qa-designer/functional/api.md +2 -2
  24. package/core/skills/qc/qa-designer/functional/gui-feature.md +2 -2
  25. package/core/skills/qc/qa-designer/functional/gui-screen.md +2 -2
  26. package/core/skills/qc/qa-designer/integration/api.md +2 -2
  27. package/core/skills/qc/qa-designer/integration/db.md +2 -2
  28. package/core/skills/qc/qa-designer/integration/gui.md +2 -2
  29. package/core/skills/qc/qa-designer/integration/kafka.md +2 -2
  30. package/core/skills/qc/qa-designer/non-functional.md +2 -2
  31. package/core/skills/qc/qa-planner/test-plan.md +10 -13
  32. package/core/skills/qc/qa-reviewer/script/e2e.md +1 -1
  33. package/core/skills/qc/qa-reviewer/script/exploratory.md +1 -1
  34. package/core/skills/qc/qa-reviewer/script/functional.md +1 -1
  35. package/core/skills/qc/qa-reviewer/script/integration.md +1 -1
  36. package/core/skills/qc/qa-reviewer/script/non-functional.md +1 -1
  37. package/core/skills/qc/qa-reviewer/test-case/e2e.md +1 -1
  38. package/core/skills/qc/qa-reviewer/test-case/exploratory.md +1 -1
  39. package/core/skills/qc/qa-reviewer/test-case/functional.md +1 -1
  40. package/core/skills/qc/qa-reviewer/test-case/integration.md +2 -2
  41. package/core/skills/qc/qa-reviewer/test-case/non-functional.md +1 -1
  42. package/core/skills/qc/qa-runner/e2e.md +1 -1
  43. package/core/skills/qc/qa-runner/exploratory/session.md +1 -1
  44. package/core/skills/qc/qa-runner/functional/api.md +1 -1
  45. package/core/skills/qc/qa-runner/functional/gui-feature.md +1 -1
  46. package/core/skills/qc/qa-runner/functional/gui-screen.md +1 -1
  47. package/core/skills/qc/qa-runner/integration.md +1 -1
  48. package/core/skills/qc/qa-runner/non-functional.md +1 -1
  49. package/core/skills/qc/qa-runner/report/report.md +1 -1
  50. package/core/steps/review-fanout.md +1 -27
  51. package/core/templates/project-context.yaml +2 -2
  52. package/docs/02-concepts/pipeline-steps/08-qc-automation.md +2 -2
  53. package/docs/04-reference/commands.md +1 -1
  54. package/docs/explain/03-refine-prd.md +6 -8
  55. package/docs/explain/15-qc-analyze.md +7 -10
  56. package/docs/explain/16-qc-plan.md +2 -2
  57. package/package.json +3 -2
  58. package/bin/qc-base-map.json +0 -595
  59. package/core/skills/qc/qa-analyst/DOC_GAP.template.md +0 -117
  60. package/core/skills/qc/qa-analyst/exhaustive-gap-scanner.md +0 -174
  61. package/core/skills/qc/qa-analyst/spec-issue-reporter.md +0 -100
  62. package/core/skills/qc/qa-planner/risk-model.md +0 -106
  63. package/core/steps/gap-verify.md +0 -231
  64. package/docs/plans/qc-implementation-log.md +0 -1446
  65. package/docs/plans/qc-merge-plan.md +0 -502
  66. package/docs/plans/qc-sync-command.md +0 -358
package/bin/lint-trace.js CHANGED
@@ -27,6 +27,28 @@
27
27
  * T8 trace-history*.jsonl: mỗi dòng một JSON object đủ key bắt buộc → ERROR
28
28
  * T9 mỗi sổ TSV có .feature đúng platform tồn tại (sổ mồ côi) → WARN
29
29
  * T10 sổ có luật merge (merge=union + text eol=lf) — hỏi `git check-attr` → WARN
30
+ * T14 code mang @trace.implements={SC} mà sổ ghi implemented_by trống → WARN
31
+ *
32
+ * T14 là chiều NGƯỢC của mọi rule khác: T1–T13 chỉ mở SỔ. T14 mở KHO (code) rồi đối chiếu
33
+ * với sổ — chiều duy nhất bắt được ca "làm rồi mà không ghi sổ".
34
+ *
35
+ * Vì sao nó phải tồn tại (G60, 2026-08-26 — xem docs/update-framework-daily-logs.md):
36
+ * `implemented_by` có ĐÚNG MỘT producer (`/generate-code` §Write Trace State), và producer
37
+ * đó là bước CUỐI của lệnh dài nhất, nằm SAU cổng build — mà cổng build có đường thoát hợp
38
+ * lệ *"fail 3 lần → DỪNG, KHÔNG ghi trace"*. Nên "code có, sổ trống" là trạng thái tài liệu
39
+ * CÔNG NHẬN là tới được, và KHÔNG bước nào sau đó dọn nó.
40
+ *
41
+ * Ba lưới đều xanh, và đều đúng theo định nghĩa của chính nó: self-check R1 hỏi *"ai được
42
+ * phép ghi"* (có producer ⇒ xanh); file này trước T14 không có chìa khoá kho (chỉ nhận
43
+ * --trace/--specs, không đọc project-context.yaml); `/validate-traces` Step 2b CÓ vào kho và
44
+ * CÓ đọc đúng tag cần, nhưng chỉ hỏi một trong hai câu (*"tag trỏ vào SC có tồn tại không"*
45
+ * → TRACE_ORPHAN) rồi dừng. Đo thật trên một project: 910/1946 row `UNTRACKED` là ghi oan
46
+ * (47%) — lệch 28 điểm phủ code trên dashboard.
47
+ *
48
+ * WARN, KHÔNG phải ERROR — có chủ ý. `/validate-traces` Step 1.0 DỪNG cả lệnh khi lint exit
49
+ * 1. Biến nợ tồn thành error là ngày đầu bật lên cả team mất lệnh audit, rồi ai cũng gõ
50
+ * `--warn-only` mãi và mất luôn T1–T13 thật. Cùng lập luận đã dùng để giữ
51
+ * PRD_UNTRACKED_EDIT ngoài `gate.blocking`.
30
52
  *
31
53
  * T4 + T7 + T10 là cụm G40 (luật merge git): `merge=union` đổi *mất-row-im-lặng* thành
32
54
  * *trùng-row-bắt-được*, nên union KHÔNG được bật mà thiếu T4. T10 canh chính luật đó — đặt
@@ -36,8 +58,12 @@
36
58
  * bin/trace-schema.json. Thêm cột/vocabulary mới vào schema mà quên dạy file này →
37
59
  * self-check.js R8 fail build.
38
60
  *
39
- * Chạy: node bin/lint-trace.js [--trace DIR[,DIR]] [--specs DIR] [--warn-only] [--json]
61
+ * Chạy: node bin/lint-trace.js [--trace DIR[,DIR]] [--specs DIR] [--code DIR[,DIR]]
62
+ * [--warn-only] [--json]
40
63
  * exit 1 nếu có ERROR (0 với --warn-only)
64
+ *
65
+ * --code là gốc mã nguồn để T14 quét. KHÔNG truyền → T14 bị bỏ qua, và file này IN RA MỘT
66
+ * DÒNG nói rõ là đã bỏ qua. Im lặng rồi báo "sạch" đúng là cách 910 row kia lọt qua.
41
67
  */
42
68
 
43
69
  const fs = require('fs');
@@ -69,6 +95,8 @@ const WARN_ONLY = argv.includes('--warn-only');
69
95
  const AS_JSON = argv.includes('--json');
70
96
  const TRACE_DIRS = flag('--trace', '.trace').split(',').map(s => s.trim()).filter(Boolean);
71
97
  const SPECS_DIR = flag('--specs', 'specs');
98
+ // Không có default: quét sai chỗ còn tệ hơn không quét. Thiếu cờ → T14 skip + in một dòng.
99
+ const CODE_DIRS = (flag('--code', '') || '').split(',').map(s => s.trim()).filter(Boolean);
72
100
 
73
101
  // ── Contract dẫn xuất từ schema ───────────────────────────────────────────────
74
102
  //
@@ -148,6 +176,11 @@ const infos = [];
148
176
  const err = (rule, where, msg, hint) => errors.push({ rule, where, msg, hint });
149
177
  const warn = (rule, where, msg, hint) => warns.push({ rule, where, msg, hint });
150
178
 
179
+ // Sổ đã đọc, gom cho T14. Khoá là sc_id THUẦN (không kèm platform) vì tag trong code không
180
+ // mang platform, và file này KHÔNG được đoán bố cục repo để suy ra. Mỗi entry giữ mọi nơi
181
+ // sc_id đó xuất hiện, nên báo cáo nêu đúng từng sổ thay vì tuyên bố một platform không chắc.
182
+ const ledger = new Map(); // sc_id → [{ at, filled }]
183
+
151
184
  // ── Đi cây thư mục ────────────────────────────────────────────────────────────
152
185
 
153
186
  function walk(dir, out = []) {
@@ -357,6 +390,11 @@ function lintBook(file) {
357
390
  } else {
358
391
  seen.set(id, n);
359
392
  }
393
+ // Gom cho T14 — chạy sau khi mọi dir đã quét xong.
394
+ const implIdx = header.indexOf('implemented_by');
395
+ const impl = implIdx === -1 ? '' : String(f[implIdx] || '').trim();
396
+ if (!ledger.has(id)) ledger.set(id, []);
397
+ ledger.get(id).push({ at, filled: !!impl && !isUnknown(impl) });
360
398
  }
361
399
  }
362
400
 
@@ -612,6 +650,163 @@ function lintOrphanBook(file, meta, traceRoot) {
612
650
  }
613
651
  }
614
652
 
653
+ // ── T14 — kho (code) vs sổ: làm rồi mà không ghi sổ ──────────────────────────
654
+ //
655
+ // Chiều ngược của T1–T13. Xem doc-comment đầu file để biết vì sao lưới cũ không bắt được.
656
+
657
+ const CODE_EXT = ['.java', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.dart', '.kt',
658
+ '.kts', '.py', '.vue', '.swift', '.go', '.cs', '.php', '.rb'];
659
+ const CODE_SKIP_DIR = new Set(['node_modules', '.git', 'build', 'dist', '.next', 'target',
660
+ 'out', '.dart_tool', 'coverage', '.gradle', '.idea', 'Pods',
661
+ 'vendor', '__pycache__', '.venv', '.agent', 'bin', 'obj']);
662
+
663
+ /**
664
+ * Bung dạng viết tắt của tag: `FT-01-UC1-SC2,SC7,OTHER-UC3-SC1` → các cặp {uc, unit}.
665
+ * `SC7` trần thừa hưởng UC của token đầy đủ gần nhất bên trái — đúng quy ước
666
+ * /generate-code dùng khi một file phủ nhiều scenario của cùng một UC.
667
+ */
668
+ function expandImplements(value) {
669
+ const out = [];
670
+ let lastUc = null;
671
+ for (let piece of String(value).split(/[,\s]+/)) {
672
+ piece = piece.trim().replace(/^\(+|\)+$/g, '');
673
+ if (!piece || piece.includes('@') || piece.includes('=')) continue;
674
+ let m = /^(.*?-UC\d+)-((?:SC|BR|AC)\d+)$/.exec(piece);
675
+ if (m) { lastUc = m[1]; out.push({ uc: m[1], unit: m[2] }); continue; }
676
+ m = /^((?:SC|BR|AC)\d+)$/.exec(piece);
677
+ if (m && lastUc) { out.push({ uc: lastUc, unit: m[1] }); continue; }
678
+ m = /^(.*?-UC\d+)$/.exec(piece);
679
+ if (m) { lastUc = m[1]; }
680
+ }
681
+ return out;
682
+ }
683
+
684
+ /**
685
+ * Thu mọi `@trace.implements` của một file, XỬ ĐƯỢC tag trải nhiều dòng.
686
+ *
687
+ * Vì sao cần (phát hiện 2026-08-27, ngay lượt dùng T14 đầu tiên trên dữ liệu thật):
688
+ * quy ước thực tế viết danh sách dài xuống nhiều dòng comment —
689
+ *
690
+ * // @trace.verifies=LESS-06-UC1-SC1, LESS-06-UC1-SC2, LESS-06-UC1-SC3,
691
+ * // LESS-06-UC1-SC4, LESS-06-UC1-SC5, LESS-06-UC1-SC9,
692
+ *
693
+ * Bản đầu của T14 đọc theo TỪNG DÒNG nên chỉ thấy 3 SC đầu và bỏ hết phần sau. Nó sai
694
+ * theo hướng an toàn (báo thiếu, không báo oan) nhưng vẫn là mù — và mù đúng ở những UC
695
+ * có nhiều scenario nhất, tức đúng chỗ cần nhìn nhất.
696
+ *
697
+ * Luật nối dòng: giá trị kết thúc bằng `,` ⇒ đọc tiếp các dòng sau **chỉ khi** dòng đó
698
+ * thuần là danh sách id (cho phép tiền tố comment). Gặp dòng khác ⇒ dừng. Chặt như vậy để
699
+ * một dấu phẩy cuối câu văn xuôi không kéo cả đoạn prose vào làm sc_id rác.
700
+ */
701
+ const ID_LIST_LINE = /^\s*(?:\/{2,}|\/\*+|\*|#)?\s*(?:[A-Z0-9][A-Z0-9-]*-UC\d+-(?:SC|BR|AC)\d+|(?:SC|BR|AC)\d+)\s*(?:,\s*(?:[A-Z0-9][A-Z0-9-]*-UC\d+-(?:SC|BR|AC)\d+|(?:SC|BR|AC)\d+)\s*)*,?\s*(?:\*\/)?\s*$/;
702
+
703
+ function collectImplements(txt) {
704
+ const out = [];
705
+ const lines = txt.split(/\r?\n/);
706
+ for (let i = 0; i < lines.length; i++) {
707
+ // matchAll, KHÔNG exec: một dòng comment có thể mang NHIỀU tag ngăn bởi `·`, và
708
+ // `exec` trên regex không-global chỉ trả về match ĐẦU TIÊN ⇒ mất tag thứ hai.
709
+ //
710
+ // Và group phải DỪNG trước `@trace.` kế tiếp (negative lookahead). Không có nó thì
711
+ // group tham lam nuốt luôn tag thứ hai, `matchAll` chỉ thấy MỘT match, và tag sau bị
712
+ // mất trong im lặng — bug từ bản đầu của T14: bản đó chỉ CẮT giá trị cho khỏi sinh
713
+ // sc_id rác, nó không bao giờ LẤY LẠI tag bị nuốt.
714
+ const hits = [...lines[i].matchAll(/@trace\.implements\s*[=:]?\s*((?:(?!@trace\.)[^\n\r*])*)/g)];
715
+ if (!hits.length) continue;
716
+ let j = i + 1;
717
+ hits.forEach((m, k) => {
718
+ // Lookahead ở trên đã chặn tag kế tiếp; `·` và khoảng trắng dư do expandImplements bỏ.
719
+ let buf = m[1];
720
+ // CHỈ tag cuối cùng của dòng mới có thể trải tiếp — các tag trước nó đã bị cắt ở
721
+ // `@trace.` nên không bao giờ kết thúc bằng dấu phẩy.
722
+ if (k === hits.length - 1) {
723
+ while (buf.trimEnd().endsWith(',') && j < lines.length && ID_LIST_LINE.test(lines[j])) {
724
+ buf += ' ' + lines[j].replace(/^\s*(?:\/{2,}|\/\*+|\*|#)?\s*/, '').replace(/\*\/\s*$/, '');
725
+ j++;
726
+ }
727
+ }
728
+ out.push(...expandImplements(buf));
729
+ });
730
+ i = j - 1;
731
+ }
732
+ return out;
733
+ }
734
+
735
+ function lintCodeVsLedger() {
736
+ if (!CODE_DIRS.length) {
737
+ infos.push(`T14 BỎ QUA — không có --code nên chưa đối chiếu code với sổ. ` +
738
+ `Ca "code đã có tag mà sổ ghi implemented_by trống" đang KHÔNG được kiểm. ` +
739
+ `Truyền --code {gốc mã nguồn, ngăn bởi dấu phẩy} để bật.`);
740
+ return;
741
+ }
742
+ const roots = CODE_DIRS.filter(d => fs.existsSync(path.resolve(d)));
743
+ const gone = CODE_DIRS.filter(d => !fs.existsSync(path.resolve(d)));
744
+ if (gone.length) {
745
+ infos.push(`T14 — ${gone.length} gốc code không tồn tại, đã bỏ qua: ${gone.join(', ')}`);
746
+ }
747
+ if (!roots.length) {
748
+ infos.push(`T14 BỎ QUA — không gốc code nào trong --code tồn tại.`);
749
+ return;
750
+ }
751
+
752
+ // sc_id → Set(file) mà code khai là implement nó
753
+ const tagged = new Map();
754
+ let filesScanned = 0;
755
+
756
+ const walkCode = dir => {
757
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
758
+ if (e.isDirectory()) {
759
+ if (CODE_SKIP_DIR.has(e.name)) continue;
760
+ walkCode(path.join(dir, e.name));
761
+ continue;
762
+ }
763
+ if (!CODE_EXT.includes(path.extname(e.name))) continue;
764
+ const p = path.join(dir, e.name);
765
+ let txt;
766
+ try { txt = fs.readFileSync(p, 'utf8'); } catch { continue; }
767
+ if (!txt.includes('@trace.implements')) continue;
768
+ filesScanned++;
769
+ for (const { uc, unit } of collectImplements(txt)) {
770
+ if (!unit.startsWith('SC')) continue; // BR/AC là tag hợp lệ, không phải scenario
771
+ const id = `${uc}-${unit}`;
772
+ if (!tagged.has(id)) tagged.set(id, new Set());
773
+ tagged.get(id).add(rel(p));
774
+ }
775
+ }
776
+ };
777
+ for (const d of roots) walkCode(path.resolve(d));
778
+
779
+ // Đối chiếu: code khai có, mà MỌI nơi trong sổ đều ghi trống.
780
+ const behind = [];
781
+ for (const [id, files] of tagged) {
782
+ const rows = ledger.get(id);
783
+ if (!rows) continue; // không có row nào → TRACE_ORPHAN, việc của Step 2b
784
+ if (rows.some(r => r.filled)) continue; // ít nhất một nơi đã ghi → không phán
785
+ behind.push({ id, files: [...files], rows: rows.map(r => r.at) });
786
+ }
787
+
788
+ infos.push(`T14 — đã quét ${filesScanned} file code mang @trace.implements ` +
789
+ `trong ${roots.length} gốc (${roots.join(', ')}).`);
790
+ if (!behind.length) return;
791
+
792
+ behind.sort((a, b) => a.id.localeCompare(b.id));
793
+ const SHOW = 15;
794
+ for (const b of behind.slice(0, SHOW)) {
795
+ warn('T14', b.rows.join(' · '),
796
+ `code khai implement "${b.id}" nhưng sổ ghi implemented_by trống`,
797
+ `Code: ${b.files.slice(0, 3).join(', ')}${b.files.length > 3 ? ` (+${b.files.length - 3})` : ''}\n` +
798
+ ` Hàng đã vào kho mà sổ chưa ghi ⇒ dòng này hiện UNTRACKED, dashboard đếm thiếu\n` +
799
+ ` và /generate-code sẽ coi là "chưa làm" rồi sinh lại.\n` +
800
+ ` Nguyên nhân thường gặp: /generate-code dừng ở cổng build nên §Write Trace State\n` +
801
+ ` chưa chạy. Sửa: /validate-traces --reconcile-code (điền từ tag, KHÔNG set status).`);
802
+ }
803
+ if (behind.length > SHOW) {
804
+ warn('T14', `(+${behind.length - SHOW} scenario nữa)`,
805
+ `tổng ${behind.length} scenario có code mà sổ chưa ghi — chỉ hiện ${SHOW} đầu`,
806
+ `Xem đủ danh sách: thêm --json. Dọn cả loạt: /validate-traces --reconcile-code`);
807
+ }
808
+ }
809
+
615
810
  // ── Main ──────────────────────────────────────────────────────────────────────
616
811
 
617
812
  let scanned = { books: 0, seams: 0, history: 0, files: 0 };
@@ -664,6 +859,10 @@ for (const dir of TRACE_DIRS) {
664
859
  }
665
860
  }
666
861
 
862
+ // T14 SAU vòng lặp mọi trace dir: ở chế độ umbrella một sc_id có thể nằm ở nhiều dir, nên
863
+ // phán khi chưa đọc hết sổ là phán trên dữ liệu thiếu.
864
+ lintCodeVsLedger();
865
+
667
866
  // ── Report ────────────────────────────────────────────────────────────────────
668
867
 
669
868
  if (AS_JSON) {
package/bin/self-check.js CHANGED
@@ -61,7 +61,6 @@
61
61
 
62
62
  const fs = require('fs');
63
63
  const path = require('path');
64
- const crypto = require('crypto'); // R16(b) — băm file snapshot để đối chiếu base_sha
65
64
 
66
65
  const ROOT = path.join(__dirname, '..');
67
66
  const WARN_ONLY = process.argv.includes('--warn-only');
@@ -1183,151 +1182,6 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
1183
1182
  }
1184
1183
  }
1185
1184
 
1186
- // ── R16 — snapshot upstream và framework không được trôi khỏi nhau trong im lặng ─
1187
- //
1188
- // Hai sự cố thật đã xảy ra trong lúc port bộ QC, và KHÔNG tầng nào bắt được:
1189
- //
1190
- // (1) 35 file skill QC bị xoá khỏi skills/. self-check ✅ · build ✅ 41/41 · .agent vẫn
1191
- // chạy được (mirror KHÔNG prune nên bản cũ còn nằm đó) — nhưng core/ thì rỗng, và
1192
- // bin/index.js prune sẽ CHỦ ĐỘNG xoá chúng ở project người dùng khi nâng cấp (đúng
1193
- // luật: có trong manifest + không còn ship + hash khớp). Ba tầng che nhau; hỏng chỉ
1194
- // lộ ở phía người dùng. → R16(d).
1195
- //
1196
- // (2) Một `upstream_sha` được GÕ BỪA vào frontmatter thay vì tính. Tự phát hiện, nhưng
1197
- // nếu lọt thì nó là BASE sai cho mọi lần merge ba chiều về sau — tức mọi lần đồng bộ
1198
- // 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
- // → R16(b).
1200
- //
1201
- // (a) là cái quan trọng nhất về lâu dài: file upstream KHÔNG có entry trong bản đồ =
1202
- // 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
- // thì không. Cùng khuôn với `checkpoint_levels` và `strict_use_check`: khai tường minh
1204
- // từng cái, rồi để máy canh — thay vì tin rằng ai đó sẽ nhớ.
1205
- //
1206
- // R16 tự bỏ qua nếu chưa có bin/qc-base-map.json (repo chưa port bộ QC) — không ép
1207
- // mọi consumer phải có bản đồ này.
1208
- {
1209
- const MAP_PATH = path.join(ROOT, 'bin/qc-base-map.json');
1210
- const SNAP_DIR = path.join(ROOT, 'upstream/qc-base');
1211
- const hasMap = fs.existsSync(MAP_PATH);
1212
- const hasSnap = fs.existsSync(SNAP_DIR);
1213
-
1214
- // ── (d) lệnh qc-* trỏ vào skill nào thì skill đó phải tồn tại ────────────────
1215
- // Chạy ĐỘC LẬP với bản đồ: đây là lớp lỗi (1) ở trên, và nó không cần snapshot.
1216
- const SKILLS_QC = path.join(ROOT, 'skills/qc');
1217
- if (fs.existsSync(SKILLS_QC)) {
1218
- const cmdDir = path.join(ROOT, 'commands');
1219
- for (const f of fs.readdirSync(cmdDir).filter(n => /^qc-.*\.tmpl$/.test(n))) {
1220
- const txt = fs.readFileSync(path.join(cmdDir, f), 'utf8');
1221
- // `{paths.qc_skills_dir}/<đường dẫn>` — lấy phần sau dấu / đầu tiên
1222
- const refs = new Set();
1223
- for (const m of txt.matchAll(/\{paths\.qc_skills_dir\}\/([A-Za-z0-9._\/-]+)/g)) {
1224
- refs.add(m[1].replace(/[.,)`]+$/, ''));
1225
- }
1226
- for (const ref of refs) {
1227
- const abs = path.join(SKILLS_QC, ref);
1228
- if (!fs.existsSync(abs)) {
1229
- err('R16', `${f} trỏ vào \`{paths.qc_skills_dir}/${ref}\` — KHÔNG TỒN TẠI trong skills/qc/`,
1230
- 'Lệnh vẫn chạy, vẫn ghi file, nhưng phân tích rỗng logic và KHÔNG cờ nào bật. ' +
1231
- 'Khôi phục skill, hoặc sửa lệnh nếu đã cố ý bỏ.');
1232
- }
1233
- }
1234
- }
1235
- }
1236
-
1237
- if (hasMap && hasSnap) {
1238
- let map;
1239
- try { map = JSON.parse(fs.readFileSync(MAP_PATH, 'utf8')); }
1240
- catch (e) { err('R16', `bin/qc-base-map.json không parse được: ${e.message}`); map = null; }
1241
-
1242
- if (map) {
1243
- const entries = map.map || [];
1244
- const byUp = new Map(entries.map(e => [e.upstream, e]));
1245
-
1246
- // liệt kê file snapshot thật
1247
- const walk = (dir, base = '') => {
1248
- let out = [];
1249
- for (const n of fs.readdirSync(dir)) {
1250
- const p = path.join(dir, n), rel = base ? `${base}/${n}` : n;
1251
- out = out.concat(fs.statSync(p).isDirectory() ? walk(p, rel) : [rel]);
1252
- }
1253
- return out;
1254
- };
1255
- const snap = walk(SNAP_DIR).sort();
1256
-
1257
- // ── (a) mọi file snapshot phải có entry ───────────────────────────────────
1258
- const missing = snap.filter(r => !byUp.has(r));
1259
- if (missing.length) {
1260
- err('R16', `${missing.length} file trong upstream/qc-base/ KHÔNG có entry trong bin/qc-base-map.json`,
1261
- `Bị bỏ quên trong im lặng. Thêm entry với state \`skipped\` + \`why\` nếu cố ý không lấy.\n` +
1262
- ` ${missing.slice(0, 5).join('\n ')}${missing.length > 5 ? `\n … và ${missing.length - 5} file nữa` : ''}`);
1263
- }
1264
- const orphan = entries.filter(e => !snap.includes(e.upstream));
1265
- if (orphan.length) {
1266
- err('R16', `${orphan.length} entry trỏ vào file KHÔNG còn trong snapshot: ${orphan.map(e => e.upstream).join(', ')}`,
1267
- 'Snapshot đã đổi mà bản đồ chưa theo — chạy lại bước dựng bản đồ.');
1268
- }
1269
-
1270
- // ── (b) entry `ported` phải có base_sha, và base_sha phải KHỚP file thật ──
1271
- const sha1 = b => crypto.createHash('sha1').update(b).digest('hex');
1272
- for (const e of entries.filter(x => x.state === 'ported')) {
1273
- if (!e.base_sha) {
1274
- err('R16', `entry \`${e.upstream}\` là \`ported\` nhưng THIẾU base_sha`,
1275
- 'Không có base thì merge ba chiều lần sau mất một chiều — mọi đồng bộ thành đoán.');
1276
- continue;
1277
- }
1278
- const abs = path.join(SNAP_DIR, e.upstream);
1279
- if (!fs.existsSync(abs)) continue; // đã báo ở orphan
1280
- const real = sha1(fs.readFileSync(abs));
1281
- if (real !== e.base_sha) {
1282
- err('R16', `base_sha của \`${e.upstream}\` KHÔNG khớp file snapshot`,
1283
- `khai=${e.base_sha.slice(0, 12)}… thật=${real.slice(0, 12)}… — sha bịa, hoặc snapshot đã bị sửa tay sau khi port.`);
1284
- }
1285
- // frontmatter của file đích phải khai đúng cùng một sha VÀ cùng một upstream_path
1286
- for (const t of (e.targets || [])) {
1287
- const tp = path.join(ROOT, t);
1288
- if (!fs.existsSync(tp)) continue; // đã báo ở (c)
1289
- const txt = fs.readFileSync(tp, 'utf8');
1290
- const m = /^upstream_sha: (.+)$/m.exec(txt);
1291
- if (m && m[1].trim() !== e.base_sha) {
1292
- err('R16', `${t} khai upstream_sha khác base_sha trong bản đồ`,
1293
- `file=${m[1].trim().slice(0, 12)}… map=${e.base_sha.slice(0, 12)}… — hai chỗ nói về cùng một sự thật mà lệch nhau.`);
1294
- }
1295
- // ── (e) upstream_path trong frontmatter phải trỏ vào file snapshot CÓ THẬT ──
1296
- //
1297
- // Vì sao cần nhánh riêng: (b) so sha, và sha vẫn khớp khi đường dẫn bị sửa sai —
1298
- // nên (b) mù với ca này. Ca thật (B9): một script đổi tên `DOC_GAPS.md → DOC_GAP.md`
1299
- // chạy quét cả frontmatter, sửa luôn `upstream_path` thành tên KHÔNG tồn tại ở
1300
- // upstream. self-check vẫn xanh; chỉ lộ ra khi kiểm tay. Mất `upstream_path` là mất
1301
- // nửa BASE của merge ba chiều — biết "sha nào" mà không biết "của file nào".
1302
- const mp = /^upstream_path: (.+)$/m.exec(txt);
1303
- if (mp) {
1304
- const declared = mp[1].trim();
1305
- if (declared !== e.upstream) {
1306
- err('R16', `${t} khai upstream_path "${declared}" ≠ bản đồ "${e.upstream}"`,
1307
- 'Hai chỗ cùng khai một sự thật mà lệch nhau — sửa cho khớp.');
1308
- }
1309
- if (!fs.existsSync(path.join(SNAP_DIR, declared))) {
1310
- err('R16', `${t} khai upstream_path "${declared}" — KHÔNG TỒN TẠI trong snapshot`,
1311
- 'Đường dẫn bị sửa sai (vd một lần đổi tên hàng loạt quét cả frontmatter), ' +
1312
- 'hoặc upstream đã đổi tên. sha có thể vẫn khớp nên nhánh (b) không bắt được.');
1313
- }
1314
- }
1315
- }
1316
- }
1317
-
1318
- // ── (c) mọi target của entry `ported` phải tồn tại ────────────────────────
1319
- for (const e of entries.filter(x => x.state === 'ported')) {
1320
- for (const t of (e.targets || [])) {
1321
- if (!fs.existsSync(path.join(ROOT, t))) {
1322
- err('R16', `entry \`${e.upstream}\` là \`ported\` nhưng target \`${t}\` không tồn tại`,
1323
- 'Bản đồ lỗi thời sau khi đổi tên/xoá file đích.');
1324
- }
1325
- }
1326
- }
1327
- }
1328
- }
1329
- }
1330
-
1331
1185
  // ── report ────────────────────────────────────────────────────────────────────
1332
1186
  const tagCount = schema.tags.length;
1333
1187
  const colCount = schema.tsv_columns.length;
@@ -695,7 +695,8 @@
695
695
  "n": 4,
696
696
  "name": "gen_ver",
697
697
  "written_by": [
698
- "generate-code"
698
+ "generate-code",
699
+ "validate-traces"
699
700
  ],
700
701
  "read_by": [
701
702
  "validate-traces",
@@ -706,7 +707,8 @@
706
707
  "n": 5,
707
708
  "name": "implemented_by",
708
709
  "written_by": [
709
- "generate-code"
710
+ "generate-code",
711
+ "validate-traces"
710
712
  ],
711
713
  "read_by": [
712
714
  "validate-traces",
@@ -910,7 +912,8 @@
910
912
  "name": "fe_phase",
911
913
  "written_by": [
912
914
  "generate-code",
913
- "generate-bdd"
915
+ "generate-bdd",
916
+ "validate-traces"
914
917
  ],
915
918
  "read_by": [
916
919
  "validate-traces"
@@ -1211,6 +1214,7 @@
1211
1214
  "DESIGNSPEC_DRIFT",
1212
1215
  "DESIGNSPEC_STALE_VS_BDD",
1213
1216
  "TRACE_ORPHAN",
1217
+ "LEDGER_BEHIND_CODE",
1214
1218
  "SEAM_UNWIRED",
1215
1219
  "STUB_UNRESOLVED",
1216
1220
  "SEAM_PENDING",
@@ -1851,6 +1855,11 @@
1851
1855
  ]
1852
1856
  },
1853
1857
  "forbidden_patterns": [
1858
+ {
1859
+ "pattern": "`integration`",
1860
+ "reason": "Giá trị cột `fe_phase` là `integrated` (CÓ hậu tố -ed). Dạng không -ed chỉ thuộc về CỜ `--phase=integration`. Ghi sai vào sổ thì `--lint-trace` T3 báo lỗi enum, VÀ `fe_integrated` ở /validate-traces Step 7 đếm `integrated` nên counter mãi bằng 0 trong im lặng — đúng hình dạng G1 (cờ im lặng). R6 ĐÃ DỰ ĐOÁN ca này (comment của nó: 'G19 là ca này') và chỉ đúng công cụ là forbidden_patterns — nhưng chưa ai khai. Nó tái diễn thật ở 0.9.0: bốn chỗ trong hai lệnh ghi giá trị cột là dạng không -ed, và CHÍNH validate-traces.tmpl tự mâu thuẫn: một dòng bảo ghi dạng đó, dòng khác đếm `integrated`.",
1861
+ "gap": "G19/G61"
1862
+ },
1854
1863
  {
1855
1864
  "pattern": "bdd/{UC-ID}.feature",
1856
1865
  "reason": "@trace.source thiếu segment {platform}. Layout thật: bdd/{platform}/{UC-ID}-{slug}.feature",
@@ -1 +1 @@
1
- 0.8.1
1
+ 0.9.1
@@ -736,6 +736,7 @@ Dựng mock từ `mock_source` đã phân giải ở Phase Detection — **shape
736
736
  - [ ] **Add-only cho file EXTEND:** đọc lại file trên disk; mọi member + `@trace.implements` cũ (gồm UC khác) còn nguyên; Guard sau-ghi đã chạy, không mất member nào
737
737
  - [ ] **Seam/Stub ledger:** không tạo giả lập khi hàng thật đã tồn tại (đã nối/lấp thẳng); mọi seam mới có `@trace.seam_pending` + dòng `kind=seam PENDING`; mọi method trắng có `@trace.stub`/`@trace.stub_owner`/`@trace.stub_for` + dòng `kind=stub PENDING` trong `_seams.tsv`; nếu UC này là chủ nợ (seam) → đã set `READY` + in cảnh báo
738
738
  - [ ] **Fill-before-create:** trước khi tạo method mới đã quét sổ + `@trace.stub` — có stub khớp thì **lấp tại chỗ** (giữ chữ ký, gỡ `@trace.stub*` → `@trace.implements`, sổ `RESOLVED`); KHÔNG còn method song song cùng trách nhiệm
739
+ - [ ] **Ghi sổ xong và đã KIỂM LẠI:** §Write Trace State đã chạy, và Guard sau-ghi cho sổ xác nhận mọi `sc_id` vừa implement có `gen_ver` + `implemented_by` ≠ `—`. Footer có dòng `📓 Sổ trace: đã ghi {n}/{m}`. *(Bước này ở cuối lệnh nên dễ rơi nhất — rơi là dashboard đếm thiếu vĩnh viễn, xem G60.)*
739
740
 
740
741
  ## Build Verify
741
742
  ```bash
@@ -749,8 +750,20 @@ Dựng mock từ `mock_source` đã phân giải ở Phase Detection — **shape
749
750
  > ❌ Build FAIL sau 3 lần thử — không ghi trace, không commit.
750
751
  > Lỗi cuối: {tóm tắt}
751
752
  > File đã sinh giữ nguyên trên working tree để bạn sửa tay hoặc chạy /debug.
753
+ >
754
+ > ⚠️ Code đã có tag @trace.implements nhưng SỔ CHƯA GHI. Nếu bạn sửa tay cho build
755
+ > pass rồi commit mà KHÔNG chạy lại lệnh này, sổ sẽ đứng ở "chưa có code" vĩnh
756
+ > viễn — không lệnh nào tự dọn. Hai đường ra:
757
+ > · chạy lại /generate-code {UC-ID} --force (ghi sổ đúng đường chính thức), hoặc
758
+ > · /validate-traces --reconcile-code (điền sổ từ tag, KHÔNG set status)
752
759
  > ```
753
760
  > Đặt Status badge = ❌ ở report cuối và dừng (bỏ qua Write Trace / Refresh Panel / Commit).
761
+ >
762
+ > **Vì sao đường thoát này phải chỉ cửa ra (G60):** trạng thái nó để lại — *code có tag, sổ
763
+ > trống* — là trạng thái **duy nhất** trong framework mà không bước nào sau đó sửa. Đo trên một
764
+ > project thật: 504 scenario ở đúng trạng thái này, làm dashboard đếm thiếu 28 điểm phủ code.
765
+ > Một ngõ cụt được tài liệu hoá mà không chỉ cửa ra thì người dùng sẽ tự chọn cửa sai (bôi xanh
766
+ > sổ bằng tay, kể cả những scenario chưa có test).
754
767
 
755
768
  ## Write Trace State
756
769
 
@@ -763,7 +776,8 @@ Cập nhật `{paths.trace_dir}/{domain}/{prd-slug}/{UC-ID}-{@trace.platform}.ts
763
776
  | `bdd_version` | `@trace.bdd_version` từ header `.feature` |
764
777
  | `tech_doc_revision` | `@trace.revision` từ tech-doc gộp `{TICKET-ID}-tech-design.md` (§4 backend đã điều khiển codegen của UC này), hoặc `—` nếu chưa có doc |
765
778
  | `fe_tech_doc_revision` | `@trace.revision` của cùng tech-doc gộp, ghi khi sinh FE có wire adapter theo §4.5.4 (`--phase=integration` **hoặc** `fe_full`); `—` cho BE, hoặc cho FE `--phase=ui` / chưa có §4.5.4 |
766
- | `fe_phase` | `ui` nếu `--phase=ui` \| `integration` nếu `--phase=integration` **hoặc** `fe_full` (đều đã wire real adapter) \| `—` cho BE |
779
+ | `fe_phase` | `ui` nếu `--phase=ui` \| **`integrated`** nếu `--phase=integration` **hoặc** `fe_full` (đều đã wire real adapter) \| `—` cho BE |
780
+ > ⚠️ Giá trị cột là **`integrated`** — có hậu tố **-ed**. Dạng không -ed chỉ thuộc về **cờ** `--phase=integration`, không bao giờ là giá trị của cột; vocabulary của cột chỉ có `ui` \| `integrated` (`trace-schema.json` → `vocabularies.fe_phase`). Ghi sai giá trị thì `--lint-trace` T3 báo lỗi, và `fe_integrated` ở `/validate-traces` Step 7 đếm `integrated` nên sẽ **mãi bằng 0 trong im lặng**.
767
781
  | `last_updated` | hôm nay `YYYY-MM-DD` |
768
782
 
769
783
  Giữ nguyên mọi cột khác (`sc_title`, `spec_ver`, `prd_version`, `prd_status`, `uc_status`, `test_count`, `test_classes`, `dev_selftest`, `dev_selftest_at`, `qc_status`, `qc_run_at`, `qc_owner`, `qc_blocked_by`) — **trừ ngoại lệ có kiểm soát ngay dưới đây**: khi logic vừa đổi thật (lấp stub, hoặc sửa method vì `DRIFT`), 4 cột nghiệm thu `dev_selftest`/`dev_selftest_at`/`qc_status`/`qc_run_at` **phải bị hạ** về "chưa biết". Giữ một `pass` đã hết hiệu lực là báo cáo sai, không phải tôn trọng quyền sở hữu cột.
@@ -794,6 +808,35 @@ Bất kể trường hợp nào, in khối này ở report cuối để dev khô
794
808
  số cũ mới là số sai. (Tỷ lệ phủ code/test không đổi — test_count giữ nguyên.)
795
809
  ```
796
810
 
811
+ ### Guard sau-ghi cho SỔ *(bắt buộc — không có nó thì bước này bỏ qua được trong im lặng)*
812
+
813
+ *Lệnh này **đã** có Guard sau-ghi cho **file code** (§File Scan: đọc lại, đối chiếu, mất member thì
814
+ khôi phục). Sổ thì chưa có — mà sổ mới là thứ dashboard và cổng PR đọc.*
815
+
816
+ Ngay sau khi ghi TSV, **đọc lại file trên disk** và kiểm đúng một điều: **mọi `sc_id` vừa implement
817
+ trong lần chạy này đều có `gen_ver` ≠ `—` VÀ `implemented_by` ≠ `—`.**
818
+
819
+ | Kết quả | Hành động |
820
+ |---|---|
821
+ | Đủ hết | tiếp Refresh Panel / Commit. In dòng tổng ở footer (dưới). |
822
+ | **Thiếu bất kỳ row nào** | **DỪNG. Đặt Status badge = ❌.** KHÔNG commit, KHÔNG báo "xong". In: `❌ Ghi sổ THẤT BẠI — {n}/{m} scenario chưa có gen_ver/implemented_by: {danh sách sc_id}. Code đã sinh nhưng sổ chưa ghi; chạy lại /generate-code {UC-ID} --force, hoặc /validate-traces --reconcile-code.` |
823
+ | Không tìm thấy file TSV | **DỪNG** cùng cách — sổ phải tồn tại từ `/generate-bdd`; không có nghĩa là chạy sai UC/platform, đừng tạo sổ mới ở đây. |
824
+
825
+ **In ở footer report, mọi lần chạy, kể cả khi đủ:**
826
+ ```
827
+ 📓 Sổ trace: đã ghi {n}/{m} scenario vào {UC-ID}-{platform}.tsv
828
+ ```
829
+
830
+ > **Vì sao một dòng đếm lại quan trọng (G60):** ghi sổ là **động tác cuối** của lệnh dài nhất trong
831
+ > bộ, nằm **sau** cổng build. Khi nó bị rơi, mọi thứ khác vẫn xanh: code có, build pass, commit
832
+ > xong, report báo ✅ — và **không có dòng nào trong transcript nói rằng sổ chưa được ghi**. Ba tuần
833
+ > sau dashboard hiện thiếu, không ai truy được lượt chạy nào. Đo trên một project thật: **504
834
+ > scenario** ở đúng trạng thái đó, và người phát hiện là một dev thấy dashboard nói "chưa có code"
835
+ > trong khi tự mình mở code ra thì tag đủ cả.
836
+ >
837
+ > Đây cũng là lý do guard này **phải chặn** (khác T14 ở `lint-trace`, cố ý chỉ WARN): ở đây không có
838
+ > nợ tồn nào để sợ — ta đang nói về **đúng lượt chạy vừa rồi**, và nó vừa thất bại thật.
839
+
797
840
  ## Refresh Panel Mirror
798
841
  # Làm mới panel mirror của Living Docs *(local)*
799
842
 
@@ -866,7 +909,7 @@ Output Artifacts · Next) cho report cuối, kèm khối bên dưới.
866
909
  Files: created={N}, extended={M}, filled={F} stub, skipped={K} | Build: SUCCESS
867
910
  Branch: feature/{TICKET_ID}-{slug}
868
911
  Phase : {UI (mock layer) | Integration (real API) | FE full (UI + real API) | BE full}
869
- fe_phase : {ui | integration (phase=integration | fe_full) | —}
912
+ fe_phase : {ui | integrated (--phase=integration | fe_full) | —}
870
913
  Figma : {Dev Mode MCP local (grounded) | ⚠️ chỉ link web + text spec (không có MCP local) | n/a cho BE} ← chỉ UI FE/App
871
914
 
872
915
  Next:
@@ -1,7 +1,7 @@
1
1
  # /propose-scenario — Đề xuất một BDD Scenario mới (cho Tester & QC)
2
2
 
3
3
  Dành cho **tester và QC** phát hiện edge case chưa được BDD hiện tại phủ (vd một gap missing-coverage
4
- `DOC_GAP` từ `/qc-analyze`). Draft một Gherkin scenario vào **khu proposal** để
4
+ `DOC_GAPS` từ `/qc-analyze`). Draft một Gherkin scenario vào **khu proposal** để
5
5
  PO/Dev review và promote.
6
6
 
7
7
  **KHÔNG sửa file `.feature` canonical.** BDD do PO/Dev sở hữu — lệnh này chỉ ghi