@educa-corp/sdd-framework 0.9.0 → 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.
package/bin/lint-trace.js CHANGED
@@ -681,6 +681,57 @@ function expandImplements(value) {
681
681
  return out;
682
682
  }
683
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
+
684
735
  function lintCodeVsLedger() {
685
736
  if (!CODE_DIRS.length) {
686
737
  infos.push(`T14 BỎ QUA — không có --code nên chưa đối chiếu code với sổ. ` +
@@ -715,18 +766,11 @@ function lintCodeVsLedger() {
715
766
  try { txt = fs.readFileSync(p, 'utf8'); } catch { continue; }
716
767
  if (!txt.includes('@trace.implements')) continue;
717
768
  filesScanned++;
718
- // Một dòng comment 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 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
- }
769
+ for (const { uc, unit } of collectImplements(txt)) {
770
+ if (!unit.startsWith('SC')) continue; // BR/AC 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));
730
774
  }
731
775
  }
732
776
  };
@@ -1855,6 +1855,11 @@
1855
1855
  ]
1856
1856
  },
1857
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
+ },
1858
1863
  {
1859
1864
  "pattern": "bdd/{UC-ID}.feature",
1860
1865
  "reason": "@trace.source thiếu segment {platform}. Layout thật: bdd/{platform}/{UC-ID}-{slug}.feature",
@@ -1 +1 @@
1
- 0.9.0
1
+ 0.9.1
@@ -776,7 +776,8 @@ Cập nhật `{paths.trace_dir}/{domain}/{prd-slug}/{UC-ID}-{@trace.platform}.ts
776
776
  | `bdd_version` | `@trace.bdd_version` từ header `.feature` |
777
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 |
778
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 |
779
- | `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**.
780
781
  | `last_updated` | hôm nay `YYYY-MM-DD` |
781
782
 
782
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.
@@ -908,7 +909,7 @@ Output Artifacts · Next) cho report cuối, kèm khối bên dưới.
908
909
  Files: created={N}, extended={M}, filled={F} stub, skipped={K} | Build: SUCCESS
909
910
  Branch: feature/{TICKET_ID}-{slug}
910
911
  Phase : {UI (mock layer) | Integration (real API) | FE full (UI + real API) | BE full}
911
- fe_phase : {ui | integration (phase=integration | fe_full) | —}
912
+ fe_phase : {ui | integrated (--phase=integration | fe_full) | —}
912
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
913
914
 
914
915
  Next:
@@ -190,6 +190,30 @@ Nhiều cờ scope cùng lúc → **DỪNG**, báo lỗi: chúng loại trừ nh
190
190
 
191
191
  `--prd`/`--uc` không phân giải được về một package/UC có thật → **DỪNG** và liệt kê ứng viên. **KHÔNG** âm thầm rơi về `all` (chạy toàn bộ khi người ta xin một phần là đốt 30 phút không ai muốn) và **KHÔNG** âm thầm audit rỗng (báo cáo "sạch" trên 0 row).
192
192
 
193
+ **Token TRẦN (không mở đầu bằng `--`)** — vd `/validate-traces --reconcile-code LESS-06`:
194
+
195
+ | Token trần trông như | Xử |
196
+ |---|---|
197
+ | **UC-ID** (`{TICKET}-UC{n}`) | coi là `--uc {token}` |
198
+ | **TICKET-ID** phân giải được về đúng **một** feature-package | coi là `--prd {token}` |
199
+ | không phân giải được, hoặc khớp **nhiều** package | **DỪNG**, liệt kê ứng viên |
200
+ | có **cả** token trần **và** một cờ scope | **DỪNG** — hai nguồn phạm vi, không tự chọn hộ |
201
+
202
+ Hai hàng đầu **phải in một dòng** nói rõ đã tự suy, để người dùng thấy mình vừa được hiểu thế nào:
203
+ ```
204
+ ⓘ Hiểu `{token}` là `--prd {token}` ({domain}/{prd-slug}). Gõ cờ tường minh nếu ý khác.
205
+ ```
206
+
207
+ > **Vì sao không để token trần rơi về `all` (phát hiện 2026-08-27, lượt dùng `--reconcile-code` đầu tiên).**
208
+ > Gate Bước 1 loại mọi token `--` khỏi phần resolve target, và lệnh này **không có target file** — nên
209
+ > token trần trước đây **bị bỏ hoàn toàn** ⇒ `scope` = `all`. Người dùng gõ `--reconcile-code LESS-06`
210
+ > tin rằng đang chạy hẹp một PRD, trong khi lệnh sẽ **GHI vào sổ của cả 20+ PRD**, gồm cả phần của PO
211
+ > khác. Đây đúng điều chính step này cấm hai dòng ở trên — *"KHÔNG âm thầm rơi về `all`"* — chỉ khác là
212
+ > lỗ vào bằng cửa *"không có cờ nào"* thay vì cửa *"cờ không phân giải được"*.
213
+ >
214
+ > Với một lệnh **chỉ đọc** thì đó là đốt thời gian. Với `--reconcile-code` (có GHI) thì đó là ghi ra
215
+ > ngoài phạm vi mà người dùng nghĩ mình đã yêu cầu — hạng nặng hơn hẳn.
216
+
193
217
  Lưu `scope` — mọi step sau dùng nó:
194
218
 
195
219
  | Step | Hẹp thế nào |
@@ -621,7 +645,7 @@ Không đụng `dev_selftest`/`qc_status` (luật G28 không áp: **không có l
621
645
  |---|---|
622
646
  | `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
647
  | `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ỉ mock · `—` cho `system`. **Không suy được** → để `—`, đừng đoán |
648
+ | `fe_phase` | **`integrated`** nếu file code có wire adapter thật theo §4.5.4 · `ui` nếu còn `@trace.mock_for`/mock adapter · `—` cho `system`. **Không suy được** → để `—`, đừng đoán. *(Vocabulary chỉ có `ui` \| `integrated` — nhớ hậu tố **-ed**. Dạng không -ed thuộc về cờ `--phase=integration`, không phải cột này.)* |
625
649
  | `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
650
 
627
651
  > ### ⚠️ Luật cứng: KHÔNG set `status`
@@ -654,7 +678,7 @@ Không đụng `dev_selftest`/`qc_status` (luật G28 không áp: **không có l
654
678
  {UC-ID}-{platform}.tsv:{dòng} {sc_id}
655
679
  implemented_by ← {ClassName.method} (từ {file}:{dòng})
656
680
  gen_ver ← {spec_ver}
657
- fe_phase ← {ui | integration | —}
681
+ fe_phase ← {ui | integrated | —}
658
682
  test_count ← {n} · test_classes ← {…} (từ @trace.verifies ở {file}) [hoặc: bỏ trống — không có @trace.verifies]
659
683
  ```
660
684
  Ghi bù im lặng là ghi bù không kiểm chứng được.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@educa-corp/sdd-framework",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Spec Driven Development workflow framework for Claude Code",
5
5
  "bin": {
6
6
  "sdd-framework": "./bin/index.js"