@educa-corp/sdd-framework 0.8.0 → 0.9.0

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.
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,119 @@ 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
+ function lintCodeVsLedger() {
685
+ if (!CODE_DIRS.length) {
686
+ infos.push(`T14 BỎ QUA — không có --code nên chưa đối chiếu code với sổ. ` +
687
+ `Ca "code đã có tag mà sổ ghi implemented_by trống" đang KHÔNG được kiểm. ` +
688
+ `Truyền --code {gốc mã nguồn, ngăn bởi dấu phẩy} để bật.`);
689
+ return;
690
+ }
691
+ const roots = CODE_DIRS.filter(d => fs.existsSync(path.resolve(d)));
692
+ const gone = CODE_DIRS.filter(d => !fs.existsSync(path.resolve(d)));
693
+ if (gone.length) {
694
+ infos.push(`T14 — ${gone.length} gốc code không tồn tại, đã bỏ qua: ${gone.join(', ')}`);
695
+ }
696
+ if (!roots.length) {
697
+ infos.push(`T14 BỎ QUA — không gốc code nào trong --code tồn tại.`);
698
+ return;
699
+ }
700
+
701
+ // sc_id → Set(file) mà code khai là implement nó
702
+ const tagged = new Map();
703
+ let filesScanned = 0;
704
+
705
+ const walkCode = dir => {
706
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
707
+ if (e.isDirectory()) {
708
+ if (CODE_SKIP_DIR.has(e.name)) continue;
709
+ walkCode(path.join(dir, e.name));
710
+ continue;
711
+ }
712
+ if (!CODE_EXT.includes(path.extname(e.name))) continue;
713
+ const p = path.join(dir, e.name);
714
+ let txt;
715
+ try { txt = fs.readFileSync(p, 'utf8'); } catch { continue; }
716
+ if (!txt.includes('@trace.implements')) continue;
717
+ filesScanned++;
718
+ // Một dòng comment có thể mang NHIỀU tag ngăn bởi `·` — cắt trước tag kế tiếp,
719
+ // không thì giá trị nuốt cả dòng và sinh sc_id rác.
720
+ for (const m of txt.matchAll(/@trace\.implements\s*[=:]?\s*([^\n\r*]*)/g)) {
721
+ let v = m[1];
722
+ const cut = v.indexOf('@trace.');
723
+ if (cut >= 0) v = v.slice(0, cut);
724
+ for (const { uc, unit } of expandImplements(v)) {
725
+ if (!unit.startsWith('SC')) continue; // BR/AC là tag hợp lệ, không phải scenario
726
+ const id = `${uc}-${unit}`;
727
+ if (!tagged.has(id)) tagged.set(id, new Set());
728
+ tagged.get(id).add(rel(p));
729
+ }
730
+ }
731
+ }
732
+ };
733
+ for (const d of roots) walkCode(path.resolve(d));
734
+
735
+ // Đối chiếu: code khai có, mà MỌI nơi trong sổ đều ghi trống.
736
+ const behind = [];
737
+ for (const [id, files] of tagged) {
738
+ const rows = ledger.get(id);
739
+ if (!rows) continue; // không có row nào → TRACE_ORPHAN, việc của Step 2b
740
+ if (rows.some(r => r.filled)) continue; // ít nhất một nơi đã ghi → không phán
741
+ behind.push({ id, files: [...files], rows: rows.map(r => r.at) });
742
+ }
743
+
744
+ infos.push(`T14 — đã quét ${filesScanned} file code mang @trace.implements ` +
745
+ `trong ${roots.length} gốc (${roots.join(', ')}).`);
746
+ if (!behind.length) return;
747
+
748
+ behind.sort((a, b) => a.id.localeCompare(b.id));
749
+ const SHOW = 15;
750
+ for (const b of behind.slice(0, SHOW)) {
751
+ warn('T14', b.rows.join(' · '),
752
+ `code khai implement "${b.id}" nhưng sổ ghi implemented_by trống`,
753
+ `Code: ${b.files.slice(0, 3).join(', ')}${b.files.length > 3 ? ` (+${b.files.length - 3})` : ''}\n` +
754
+ ` Hàng đã vào kho mà sổ chưa ghi ⇒ dòng này hiện UNTRACKED, dashboard đếm thiếu\n` +
755
+ ` và /generate-code sẽ coi là "chưa làm" rồi sinh lại.\n` +
756
+ ` Nguyên nhân thường gặp: /generate-code dừng ở cổng build nên §Write Trace State\n` +
757
+ ` chưa chạy. Sửa: /validate-traces --reconcile-code (điền từ tag, KHÔNG set status).`);
758
+ }
759
+ if (behind.length > SHOW) {
760
+ warn('T14', `(+${behind.length - SHOW} scenario nữa)`,
761
+ `tổng ${behind.length} scenario có code mà sổ chưa ghi — chỉ hiện ${SHOW} đầu`,
762
+ `Xem đủ danh sách: thêm --json. Dọn cả loạt: /validate-traces --reconcile-code`);
763
+ }
764
+ }
765
+
615
766
  // ── Main ──────────────────────────────────────────────────────────────────────
616
767
 
617
768
  let scanned = { books: 0, seams: 0, history: 0, files: 0 };
@@ -664,6 +815,10 @@ for (const dir of TRACE_DIRS) {
664
815
  }
665
816
  }
666
817
 
818
+ // 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
819
+ // phán khi chưa đọc hết sổ là phán trên dữ liệu thiếu.
820
+ lintCodeVsLedger();
821
+
667
822
  // ── Report ────────────────────────────────────────────────────────────────────
668
823
 
669
824
  if (AS_JSON) {
@@ -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",
@@ -1 +1 @@
1
- 0.8.0
1
+ 0.9.0
@@ -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
 
@@ -794,6 +807,35 @@ Bất kể trường hợp nào, in khối này ở report cuối để dev khô
794
807
  số cũ mới là số sai. (Tỷ lệ phủ code/test không đổi — test_count giữ nguyên.)
795
808
  ```
796
809
 
810
+ ### 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)*
811
+
812
+ *Lệnh này **đã** có Guard sau-ghi cho **file code** (§File Scan: đọc lại, đối chiếu, mất member thì
813
+ khôi phục). Sổ thì chưa có — mà sổ mới là thứ dashboard và cổng PR đọc.*
814
+
815
+ 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
816
+ trong lần chạy này đều có `gen_ver` ≠ `—` VÀ `implemented_by` ≠ `—`.**
817
+
818
+ | Kết quả | Hành động |
819
+ |---|---|
820
+ | Đủ hết | tiếp Refresh Panel / Commit. In dòng tổng ở footer (dưới). |
821
+ | **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.` |
822
+ | 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. |
823
+
824
+ **In ở footer report, mọi lần chạy, kể cả khi đủ:**
825
+ ```
826
+ 📓 Sổ trace: đã ghi {n}/{m} scenario vào {UC-ID}-{platform}.tsv
827
+ ```
828
+
829
+ > **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
830
+ > 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
831
+ > 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
832
+ > 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
833
+ > 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"
834
+ > trong khi tự mình mở code ra thì tag đủ cả.
835
+ >
836
+ > Đâ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ó
837
+ > 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.
838
+
797
839
  ## Refresh Panel Mirror
798
840
  # Làm mới panel mirror của Living Docs *(local)*
799
841
 
@@ -256,11 +256,26 @@ Kiểm tra mảng `services` có tồn tại trong `project-context.yaml` không
256
256
  Chạy checker xác định trên mọi trace dir đã phân giải ở Step 0:
257
257
 
258
258
  ```bash
259
- npx @educa-corp/sdd-framework --lint-trace --trace {all_trace_dirs, ngăn cách bởi dấu phẩy} --specs {paths.specs_dir}
259
+ npx @educa-corp/sdd-framework --lint-trace --trace {all_trace_dirs, ngăn cách bởi dấu phẩy} --specs {paths.specs_dir} --code {code_roots, ngăn cách bởi dấu phẩy}
260
260
  ```
261
261
 
262
+ **Dựng `code_roots` — bắt buộc truyền, đây là chìa khoá kho của T14:**
263
+
264
+ | Chế độ | `code_roots` |
265
+ |---|---|
266
+ | **Umbrella** (có `services:`) | tập **duy nhất** mọi giá trị `path` xuất hiện trong `services:` — gồm cả `path` lồng trong `by_prd_slug`. Bỏ trùng (nhiều domain trỏ chung một submodule là chuyện thường). |
267
+ | **Single-service** | `{paths.src_dir}` |
268
+ | Không phân giải được cái nào | **truyền rỗng** — T14 tự bỏ qua và tự in một dòng nói rõ đã bỏ qua. **Đừng đoán** một thư mục nào đó: quét sai chỗ còn tệ hơn không quét. |
269
+
262
270
  *`bin/` sống trong package npm, không được cài vào project — nên `npx` là đường duy nhất. Không có mạng / npx fail → **bỏ qua step này**, in `⚠️ Chưa lint được sổ trace (npx không khả dụng) — kết quả dưới đây chưa được kiểm cấu trúc` vào report, rồi tiếp Step 1. Đừng để nó chặn cả lệnh.*
263
271
 
272
+ > **Vì sao `--code` (G60).** Trước nó, checker xác định duy nhất mở file thật của dự án chỉ nhận
273
+ > `--trace` và `--specs` — nó **không thể thấy code**, nên ca *"code đã có tag mà sổ ghi
274
+ > `implemented_by` trống"* không có một mắt xác định nào canh. T14 là chiều ngược đó, ở mức
275
+ > **WARN**: nó **không được** chặn lệnh này, vì nợ tồn ở project đang chạy sẽ đỏ khắp nơi ở lần
276
+ > đầu và người ta sẽ tắt cổng — mất luôn T1–T13 thật.
277
+ > Cờ `LEDGER_BEHIND_CODE` ở Step 2b là bản LLM của cùng phép kiểm; `--reconcile-code` là đường ra.
278
+
264
279
  **Exit 0 → tiếp Step 1.**
265
280
 
266
281
  **Exit 1 → DỪNG NGAY.** Đừng nạp, đừng tính `status`, đừng ghi lại gì:
@@ -334,9 +349,23 @@ Với mỗi row có `service` ∈ (`unrouted`, `unresolved`), tra lại `service
334
349
  > `unrouted` → path. Không sửa tay, không sinh lại BDD. Không có bước này thì `unrouted` **đọng lại
335
350
  > vĩnh viễn** — đúng bệnh `TBD` mà G1/G28 đã chỉ ra.
336
351
 
337
- ### Step 2b — Reverse audit (bắt tag mồ côi)
352
+ ### Step 2b — Reverse audit (hai câu hỏi trên cùng một lượt quét)
353
+
354
+ *Step 2 đi chiều **spec → code** (mỗi row TSV, SC đó implement tới đâu). Step này đi **chiều ngược** — bắt lớp lỗi mà Step 2 cấu trúc không thể thấy.*
338
355
 
339
- *Step 2 đi chiều **spec code** (mỗi row TSV, SC đó implement tới đâu). Step này đi **chiều ngược** — bắt lớp lỗi mà Step 2 cấu trúc không thể thấy: code trỏ vào một scenario **không còn tồn tại**. Xảy ra khi gen lại BDD làm một SC biến mất (gộp / đổi số / xoá) trong khi code implement nó vẫn nằm đó, vẫn được caller gọi.*
356
+ **Một lượt quét, HAI câu hỏi.** Đừng dừng câu đầu:
357
+
358
+ | | Câu hỏi | Bắt được gì |
359
+ |---|---|---|
360
+ | **A** | *"Tag này trỏ vào SC có tồn tại không?"* | code trỏ vào scenario **đã bị xoá** → `ORPHANED` / `TRACE_ORPHAN` |
361
+ | **B** | *"Sổ có biết SC này đã có code chưa?"* | code đã làm mà **sổ chưa ghi** → `LEDGER_BEHIND_CODE` |
362
+
363
+ > **Vì sao câu B phải nằm ở đây, và vì sao nó từng thiếu (G60).** Bản trước chỉ có câu A. Câu B dùng
364
+ > **đúng dữ liệu đã nạp trong bộ nhớ** — không phát sinh thêm một lần I/O nào — nên bỏ nó là bỏ
365
+ > không vì lý do gì. Và không có nó thì lớp lỗi *"làm rồi mà không ghi sổ"* **không có một mắt nào
366
+ > canh**: `self-check` R1 hỏi *"ai được phép ghi"* (`implemented_by` có producer ⇒ xanh), còn
367
+ > `lint-trace` trước T14 thì không thấy được code. Đo trên một project thật: **504 scenario** có code
368
+ > mà sổ ghi trống, làm dashboard đếm thiếu **28 điểm** phủ code.
340
369
 
341
370
  **Quét (gộp vào cùng lượt quét code của Step 5b — không thêm pass mới):** dưới `{code_base_package}` (CLAUDE.md §2) + `{paths.src_dir}`, thu mọi `@trace.implements={UC-ID}-SC{N}`; trong thư mục test thu mọi `@trace.verifies={UC-ID}-SC{N}`.
342
371
 
@@ -352,6 +381,24 @@ Với mỗi tag, hỏi: `SC{N}` đó có tồn tại trong `.feature` của đú
352
381
 
353
382
  Không tìm thấy tag mồ côi nào → bỏ qua im lặng.
354
383
 
384
+ #### Câu B — sổ có theo kịp code chưa?
385
+
386
+ Với mỗi `@trace.implements={UC-ID}-SC{N}` mà `SC{N}` **có** trong `.feature` (tức đã qua câu A):
387
+
388
+ | Điều kiện | Cờ | Ý nghĩa |
389
+ |---|---|---|
390
+ | Row TSV tồn tại **và** `implemented_by != —` | *(sạch)* | Sổ khớp kho |
391
+ | Row TSV tồn tại **nhưng** `implemented_by == —` | `LEDGER_BEHIND_CODE` 🟠 | **Làm rồi mà không ghi sổ.** Row đang hiện `UNTRACKED`, dashboard đếm thiếu, và `/generate-code` sẽ coi là "chưa làm" rồi sinh lại |
392
+ | Không có row TSV nào | — | Step 2 đã thêm row `UNTRACKED` từ `.feature`; lượt sau sẽ vào hàng trên |
393
+
394
+ **Ghi lại cho mỗi cờ:** `sc_id` · `platform` (từ tên file sổ) · danh sách file code mang tag · và **`has_verifies`** = có `@trace.verifies` cho đúng SC đó hay không. Field cuối là thứ quyết định `--reconcile-code` điền row đó thành *có test* hay *chưa test* — thu ngay ở đây vì lượt quét này đã đọc cả hai loại tag.
395
+
396
+ **Nguyên nhân thường gặp, nêu luôn trong report** để người đọc không phải đoán: `/generate-code` dừng ở cổng build nên §Write Trace State chưa chạy; hoặc dev sửa tay cho build pass rồi commit mà không chạy lại lệnh.
397
+
398
+ **Cờ này 🟠 KHÔNG chặn PR** — xem lý do ở Step 7 (`ledger_behind_code_count`). Đường ra là `--reconcile-code` (Step 5f).
399
+
400
+ Không tìm thấy row nào lệch → bỏ qua im lặng.
401
+
355
402
  ### Step 3 — Tính `status` theo từng scenario
356
403
 
357
404
  Áp dụng quy tắc theo thứ tự ưu tiên (first-match-wins):
@@ -562,6 +609,66 @@ Parse `$ARGUMENTS`:
562
609
 
563
610
  Không đụng `dev_selftest`/`qc_status` (luật G28 không áp: **không có logic nào đổi** — đó chính là tiền đề của realign).
564
611
 
612
+ ### Step 5f — Reconcile code mode *(chỉ chạy khi có `--reconcile-code`)*
613
+
614
+ *Đường ra cho cờ `LEDGER_BEHIND_CODE` (Step 2b câu B). Không có nó thì cờ đó **không bao giờ sạch được**: `implemented_by` chỉ `/generate-code` ghi, mà nó thấy row đã có code là skip — nên lối ra duy nhất là ép sinh lại code cho hàng loạt scenario đã làm xong. Đúng vòng lặp đóng mà `--realign` được viết ra để mở, chỉ ở một cột khác.*
615
+
616
+ `--reconcile-code` (không tham số → mọi row `LEDGER_BEHIND_CODE` **trong scope** của Step 0-A; dùng `--domain`/`--prd`/`--uc` để hẹp lại).
617
+
618
+ **Điền từ bằng chứng — 5 cột, không hơn:**
619
+
620
+ | Cột | Lấy từ đâu |
621
+ |---|---|
622
+ | `implemented_by` | tên class/hàm mang `@trace.implements` cho SC đó (nhiều file → nối bằng `,`, đúng khuôn `/generate-code` đang ghi) |
623
+ | `gen_ver` | copy `spec_ver` của **chính row đó** *(không phải version hiện tại của `.feature` — code sinh ra từ bản nào thì `spec_ver` lúc này là bản đó; Step 2 vừa đồng bộ nó)* |
624
+ | `fe_phase` | `integration` nếu file code có wire adapter thật theo §4.5.4 · `ui` nếu chỉ có mock · `—` cho `system`. **Không suy được** → để `—`, đừng đoán |
625
+ | `test_count` · `test_classes` | **CHỈ khi `has_verifies` = true** (Step 2b đã thu). Số test + tên class lấy từ file mang `@trace.verifies`. `has_verifies` = false → **để trống**, tuyệt đối không điền 0-giả hay tên class phỏng đoán |
626
+
627
+ > ### ⚠️ Luật cứng: KHÔNG set `status`
628
+ >
629
+ > Điền xong 5 cột rồi **để Step 3 tự tính** `status`. Đây không phải chi tiết kỹ thuật — nó là điều
630
+ > quyết định lệnh này hữu ích hay có hại.
631
+ >
632
+ > Có **hai loại bằng chứng, hai người ghi**: `@trace.implements` = *"đã làm"* (do `/generate-code`) ·
633
+ > `@trace.verifies` = *"đã kiểm"* (do `/dev-gen-test`). **Không được suy cái này ra cái kia.**
634
+ >
635
+ > Đo trên một project thật: trong 910 row lệch, **446 có cả hai** ⇒ Step 3 tính ra `OK`; **464 chỉ
636
+ > có cái thứ nhất** ⇒ Step 3 tính ra `GAP`, tức **lộ ra 464 chỗ thiếu test** đang bị chữ `UNTRACKED`
637
+ > che. Nếu lệnh này tự viết `status = OK` thì 464 scenario chưa ai kiểm lên dashboard thành xong hết,
638
+ > và **cổng chặn PR — thứ tồn tại để bắt đúng loại lỗi này — mở cửa cho hàng chưa kiểm.**
639
+ >
640
+ > `/generate-code` đã phát biểu cùng luật này cho chính nó: *"`status` được tính bởi
641
+ > `/validate-traces` — không set ở đây."* Lệnh ghi bù cũng không được là ngoại lệ.
642
+
643
+ **Ba rào an toàn — bắt buộc, cùng khuôn Step 5e:**
644
+
645
+ 1. **Chỉ chạm row đang `LEDGER_BEHIND_CODE`.** Từ chối mọi row khác. Row `DRIFT`/`ORPHANED`/`GAP`/`OK` đã có `implemented_by` — ghi lên đó là ghi đè việc của `/generate-code`, không phải ghi bù. In:
646
+ ```
647
+ ❌ Từ chối reconcile {sc_id}: row này đang {status}, không phải LEDGER_BEHIND_CODE.
648
+ --reconcile-code chỉ điền vào row TRỐNG mà code đã có tag. Row này có chuyện khác.
649
+ ```
650
+ 2. **Chỉ ghi 5 cột trên.** **Guard sau-ghi:** đọc lại TSV, diff với bản trước; có bất kỳ ô nào **khác** ngoài 5 cột đã định (kể cả `status`, `last_updated` của row không thuộc phạm vi) → **khôi phục file** và dừng.
651
+ 3. **In chính xác từng file + từng row đã sửa**, kèm cột nào điền giá trị gì và **bằng chứng nào**:
652
+ ```
653
+ 📓 Đã ghi bù {n} row:
654
+ {UC-ID}-{platform}.tsv:{dòng} {sc_id}
655
+ implemented_by ← {ClassName.method} (từ {file}:{dòng})
656
+ gen_ver ← {spec_ver}
657
+ fe_phase ← {ui | integration | —}
658
+ test_count ← {n} · test_classes ← {…} (từ @trace.verifies ở {file}) [hoặc: bỏ trống — không có @trace.verifies]
659
+ ```
660
+ Ghi bù im lặng là ghi bù không kiểm chứng được.
661
+
662
+ **Không đụng** `dev_selftest`/`qc_status`/`qc_owner`/`qc_blocked_by`: luật G28 không áp vì **không có logic nào đổi** — code đã nằm đó từ trước, ta chỉ ghi lại sự thật vào sổ. Hạ tín hiệu nghiệm thu ở đây là trừng phạt một lượt dọn dẹp.
663
+
664
+ **Sau khi ghi, chạy tiếp Step 3 → Step 6 → Step 7 như thường** để `status` được tính lại và aggregate phản ánh số mới. In cảnh báo ở report:
665
+ ```
666
+ ℓ Coverage sẽ NHẢY sau lượt này — code {cũ}% → {mới}%, test {cũ}% → {mới}%.
667
+ Đây KHÔNG phải việc mới làm được: là việc đã làm rồi mà sổ chưa ghi. Và {n} row
668
+ chuyển sang GAP là {n} chỗ thiếu test vừa lộ ra — phần "xấu đi" trên giấy chính
669
+ là phần trước đó đang bị che.
670
+ ```
671
+
565
672
  ### Step 5c — BDD version drift check
566
673
 
567
674
  *Đối xứng với Step 4 (PRD drift). Trước đây tầng BDD là tầng DUY NHẤT không có cờ drift — dù `/generate-code` vẫn ghi `@trace.bdd_version` vào code và JSON report vẫn lưu nó. Dữ liệu có, chỉ thiếu phép so.*
@@ -692,6 +799,18 @@ fe_integrated = rows where fe_phase == integrated # FE đã wire adapter th
692
799
  # công việc CHƯA XONG dù status có thể đã là OK (có code + có test trên mock).
693
800
  orphaned_count = rows where status == ORPHANED # code còn, scenario đã bị xoá khỏi .feature (Step 2b/Rule 0)
694
801
  trace_orphan_count = số tag @trace.implements/@trace.verifies trỏ vào SC không tồn tại VÀ không có row TSV (Step 2b)
802
+ ledger_behind_code_count = số row có @trace.implements trong code mà cột implemented_by còn trống
803
+ (Step 2b câu B). 🟠 — "làm rồi mà không ghi sổ". Sạch bằng --reconcile-code.
804
+ # KHÔNG vào gate.blocking, và đây là quyết định có chủ ý — đừng "sửa":
805
+ # (1) gate.blocking nghĩa hẹp là CODE ĐANG HỎNG. Ở đây code hoàn toàn đúng
806
+ # và đang chạy; chỉ có sổ nói sai về nó. Cùng ranh giới mà R9(e) giữ.
807
+ # (2) Nợ tồn khi mới bật: mọi project đang chạy đều đã có sẵn hàng trăm row
808
+ # loại này (đo thật: 504 scenario / 910 row ở một project) ⇒ cờ chặn mới
809
+ # sẽ đỏ khắp nơi ở lần đầu ⇒ người ta tắt cổng ⇒ mất luôn 4 cờ 🔴 thật.
810
+ # (3) Nhưng nó vẫn PHẢI thấy được: in mỗi lần chạy + counter ở summary, và
811
+ # bản xác định của cùng phép kiểm là lint-trace T14 (cũng WARN).
812
+ # Team nào đã dọn sạch nợ tồn thì tự thêm counter này vào gate.blocking kèm
813
+ # `why` — self-check R13(e) canh việc đó.
695
814
  prd_untracked_edit_count = số file PRD có nội dung đổi kể từ `sha_at_audit` mà `Version` KHÔNG đổi
696
815
  (Step 3.9). 🔴 — có người sửa ngoài đường chính thức, nên MỌI phán đoán version
697
816
  của Step 4/5 trên file đó đang dựa vào một nhãn không còn đúng.
@@ -797,6 +916,7 @@ Schema:
797
916
  "fe_integrated": 0,
798
917
  "orphaned_count": 0,
799
918
  "trace_orphan_count": 0,
919
+ "ledger_behind_code_count": 0,
800
920
  "prd_untracked_edit_count": 0,
801
921
  "prd_drift_count": 0,
802
922
  "prd_stale_ref_count": 0,
@@ -1007,6 +1127,16 @@ Schema:
1007
1127
  "fix": "sửa sc_id cho đúng SC hiện có, HOẶC xoá code/test nếu không còn cần"
1008
1128
  }
1009
1129
  ],
1130
+ "ledger_behind_code": [
1131
+ {
1132
+ "sc_id": "<SC-ID>",
1133
+ "platform": "web | app | system | webview | …",
1134
+ "files": ["<file code mang @trace.implements cho SC này>"],
1135
+ "has_verifies": false,
1136
+ "would_become": "OK | GAP",
1137
+ "fix": "/validate-traces --reconcile-code (điền implemented_by/gen_ver/fe_phase từ tag; test_count CHỈ khi có @trace.verifies; KHÔNG set status)"
1138
+ }
1139
+ ],
1010
1140
  "techdoc_drift": [
1011
1141
  {
1012
1142
  "uc_id": "<UC-ID>",
@@ -1242,6 +1372,23 @@ Trace orphan (tag trỏ vào SC không tồn tại, KHÔNG có row .tsv nào):
1242
1372
  (Không lệnh nào khác bắt được cái này — row .tsv đã bị xoá bởi version cũ,
1243
1373
  hoặc tag ghi sai id ngay từ đầu.)
1244
1374
 
1375
+ {khối dưới CHỈ in khi ledger_behind_code_count > 0 — else bỏ cả khối}
1376
+ 🟠 LEDGER_BEHIND_CODE — làm rồi mà sổ chưa ghi ({n} scenario):
1377
+ {UC}-UC1-SC3 (web) code: {File.method} → sẽ thành OK
1378
+ {UC}-UC1-SC4 (web) code: {File.method} chưa có test → sẽ thành GAP
1379
+ … {n} scenario, gom theo PRD: {LESS-06} 76 · {LESS-09} 65 · …
1380
+ Code đã có tag @trace.implements, nhưng cột implemented_by còn trống ⇒ các row này
1381
+ đang hiện UNTRACKED. Dashboard đếm THIẾU, và /generate-code coi là "chưa làm" nên
1382
+ sẽ sinh lại thứ đã có.
1383
+ Nguyên nhân thường gặp: /generate-code dừng ở cổng build nên §Write Trace State
1384
+ chưa chạy (build fail 3 lần → "KHÔNG ghi trace" là đường thoát hợp lệ), rồi dev
1385
+ sửa tay cho build pass và commit mà không chạy lại lệnh.
1386
+ → /validate-traces --reconcile-code (cả scope hiện tại)
1387
+ /validate-traces --reconcile-code --domain {d} (dọn từng domain cho dễ soi)
1388
+ ⚠️ {n} trong số này CHƯA có @trace.verifies → chúng sẽ thành GAP, không phải OK.
1389
+ Đó là số ĐÚNG: chỗ thiếu test vừa lộ ra, không phải chỗ mới hỏng.
1390
+ 🟠 KHÔNG chặn PR — code đang chạy đúng, chỉ sổ nói sai về nó.
1391
+
1245
1392
  🔴 PRD_UNTRACKED_EDIT — nội dung PRD đổi mà nhãn Version KHÔNG đổi ({n} file):
1246
1393
  specs/payment/create-invoice/PAY01-create-invoice.md Version 1.3 (không đổi từ lần audit)
1247
1394
  Bằng chứng : git status — sửa CHƯA commit
@@ -1311,6 +1458,10 @@ Routing chưa chốt ({service_unrouted_count} scenario) — 🟠 KHÔNG chặn
1311
1458
  và đóng dấu incorporated + archived/ — không phải làm tay bước nào).
1312
1459
 
1313
1460
  Recommendations:
1461
+ - /validate-traces --reconcile-code cho LEDGER_BEHIND_CODE 🟠 ← CHẠY CÁI NÀY TRƯỚC
1462
+ ↳ Nó chỉ ghi sổ theo tag đã có trong code, không sinh code. Chạy trước thì
1463
+ UNTRACKED/GAP bên dưới mới là số thật; chạy /generate-code trước sẽ sinh lại
1464
+ thứ đã tồn tại và có thể ghi đè code đang chạy.
1314
1465
  - /generate-code {UC-ID} cho scenario DRIFT và UNTRACKED
1315
1466
  - /dev-gen-test {UC-ID} cho GAP (thiếu test)
1316
1467
  - /generate-bdd {prd-file} cho PRD_DRIFT 🟠 (changelog CÓ nêu UC này)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@educa-corp/sdd-framework",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Spec Driven Development workflow framework for Claude Code",
5
5
  "bin": {
6
6
  "sdd-framework": "./bin/index.js"
@@ -21,7 +21,8 @@
21
21
  "bin/",
22
22
  "core/",
23
23
  "scripts/",
24
- "docs/"
24
+ "docs/",
25
+ "!docs/update-framework-daily-logs.md"
25
26
  ],
26
27
  "keywords": [
27
28
  "claude-code",