@educa-corp/sdd-framework 0.7.1 → 0.7.3

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/index.js CHANGED
@@ -5,6 +5,21 @@ const path = require('path');
5
5
 
6
6
  const ROOT = path.join(__dirname, '..');
7
7
 
8
+ // NGUỒN ASSET ĐỂ CÀI — luôn là `core/`, không bao giờ là dir ở gốc.
9
+ //
10
+ // `core/` là bản dựng sẵn và là thứ DUY NHẤT được ship (package.json → files:
11
+ // bin/ core/ scripts/ docs/). Các dir `modules/`, `hooks/`, `steps/`, `templates/`,
12
+ // `rules/`, `skills/` ở gốc là NGUỒN BUILD — chúng chỉ tồn tại trong dev checkout.
13
+ //
14
+ // Vì sao khai một hằng số cho việc này (hồi quy 0.7.0 / G59): đợt dọn tập publish bỏ
15
+ // `modules/` + `hooks/` khỏi `files`, nhưng BA chỗ trong file này vẫn đọc
16
+ // `path.join(ROOT, 'modules'|'hooks')`. Trong dev checkout chúng vẫn có nên test và
17
+ // `npm run build` đều xanh; trong tarball thì KHÔNG, và `--module` crash cứng bằng
18
+ // ENOENT giữa `--init` — chết TRƯỚC bước `--hooks`, nên một cờ hỏng làm chết cờ kia và
19
+ // project bị cài dở (thiếu .claude/settings.json).
20
+ // Test `bin/ chỉ đọc dir CÓ trong package.json files` canh việc này từ nay.
21
+ const CORE_DIR = path.join(ROOT, 'core');
22
+
8
23
  const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
9
24
  const VERSION = pkg.version;
10
25
 
@@ -14,6 +29,12 @@ const VERSION = pkg.version;
14
29
  // of this file is evaluated, and `const` does not hoist.
15
30
  const MANIFEST_NAME = '.install-manifest.json';
16
31
 
32
+ // Asset được YÊU CẦU tường minh (--module, --hooks, module của service) mà bản cài không
33
+ // có. Gom lại thay vì crash giữa `--init`: crash để lại project cài DỞ — đúng ca hồi quy
34
+ // 0.7.0, nơi ENOENT ở bước --module làm bước --hooks không bao giờ chạy. Nhưng cũng KHÔNG
35
+ // im lặng: exit 1 ở cuối, vì người dùng đã xin thứ họ không nhận được.
36
+ const assetErrors = [];
37
+
17
38
  const args = process.argv.slice(2);
18
39
  const isMigrateSpecs = args.includes('--migrate-specs');
19
40
  const isMigrateBddPf = args.includes('--migrate-bdd-platform');
@@ -301,11 +322,20 @@ if (isInit) {
301
322
  console.log(`⚠️ Unknown module: "${moduleName}"`);
302
323
  console.log(` Available: ${AVAILABLE_MODULES.join(', ')}`);
303
324
  } else {
304
- const srcModuleDir = path.join(ROOT, 'modules', moduleName);
325
+ const srcModuleDir = path.join(CORE_DIR, 'modules', moduleName);
305
326
  const destModuleDir = path.join(agentDir, 'modules', moduleName);
306
- fs.mkdirSync(destModuleDir, { recursive: true });
307
- copyDirRecursive(srcModuleDir, destModuleDir);
308
- console.log(`✅ Module "${moduleName}" installed to: .agent/modules/${moduleName}`);
327
+ // Guard TRƯỚC khi copy. Bản gọi thẳng copyDirRecursive và ENOENT làm chết cả
328
+ // `--init` giữa đường — bước `--hooks` ngay dưới không bao giờ chạy, project bị cài
329
+ // dở. Một cờ hỏng không được phép làm chết cờ khác.
330
+ if (!fs.existsSync(srcModuleDir)) {
331
+ console.log(`❌ Module "${moduleName}" không có trong bản cài: ${path.relative(ROOT, srcModuleDir)}`);
332
+ console.log(` Bản cài thiếu/hỏng — cài lại, hoặc xoá cache npx rồi chạy lại.`);
333
+ assetErrors.push(`module ${moduleName}`);
334
+ } else {
335
+ fs.mkdirSync(destModuleDir, { recursive: true });
336
+ copyDirRecursive(srcModuleDir, destModuleDir);
337
+ console.log(`✅ Module "${moduleName}" installed to: .agent/modules/${moduleName}`);
338
+ }
309
339
  }
310
340
  }
311
341
 
@@ -440,12 +470,18 @@ if (isInit) {
440
470
  if (!AVAILABLE_MODULES.includes(svc.module)) {
441
471
  console.log(` ⚠️ Unknown module "${svc.module}" — skipped`);
442
472
  } else {
443
- const srcMod = path.join(ROOT, 'modules', svc.module);
473
+ const srcMod = path.join(CORE_DIR, 'modules', svc.module);
444
474
  const destMod = path.join(svcAgentDir, 'modules', svc.module);
445
475
  if (fs.existsSync(srcMod)) {
446
476
  fs.mkdirSync(destMod, { recursive: true });
447
477
  copyDirRecursive(srcMod, destMod);
448
478
  console.log(` ✅ ${svc.name}/.agent/modules/${svc.module}/`);
479
+ } else {
480
+ // Bản cũ bỏ qua ÂM THẦM ở đây: người dùng khai module cho service, không nhận
481
+ // được gì, và không dòng nào báo. Cùng nguồn lỗi với ca --module ở trên nhưng
482
+ // hỏng theo hướng im lặng — tệ hơn.
483
+ console.log(` ❌ ${svc.name}: module "${svc.module}" không có trong bản cài`);
484
+ assetErrors.push(`${svc.name}/module ${svc.module}`);
449
485
  }
450
486
  }
451
487
  }
@@ -577,6 +613,12 @@ if (isInit) {
577
613
  console.log('To upgrade later: /update-framework (trong Claude Code)');
578
614
  console.log(' (or: npx @educa-corp/sdd-framework@latest --init)');
579
615
  console.log('');
616
+ if (assetErrors.length) {
617
+ console.log('');
618
+ console.log(`❌ ${assetErrors.length} asset được yêu cầu nhưng bản cài không có: ${assetErrors.join(' · ')}`);
619
+ console.log(' Phần còn lại của --init đã hoàn tất — nhưng exit 1 vì bạn đã xin thứ không nhận được.');
620
+ process.exit(1);
621
+ }
580
622
  process.exit(0);
581
623
  }
582
624
 
@@ -963,7 +1005,7 @@ function installDataGuardHook() {
963
1005
 
964
1006
  // 1. Copy hook script
965
1007
  fs.mkdirSync(hooksDir, { recursive: true });
966
- const srcHook = path.join(ROOT, 'hooks', 'data-guard.js');
1008
+ const srcHook = path.join(CORE_DIR, 'hooks', 'data-guard.js');
967
1009
  const destHook = path.join(hooksDir, 'data-guard.js');
968
1010
  fs.copyFileSync(srcHook, destHook);
969
1011
  console.log(` ✅ Hook copied to: .claude/hooks/data-guard.js`);
package/bin/lint-trace.js CHANGED
@@ -155,6 +155,63 @@ function readLines(file) {
155
155
  return fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n').split('\n');
156
156
  }
157
157
 
158
+ // ── T13 — hai điều kiện bảo vệ sổ có KIỂM ĐƯỢC không ─────────────────────────
159
+ //
160
+ // T10 (luật merge) và T11 (bị gitignore) đều hỏi `git`. Trước T13, khi không hỏi được
161
+ // thì cả hai `catch { return; }` — IM LẶNG. Và im lặng ở đây tệ hơn nó nghe: gate-trace
162
+ // G0 lọc warns theo rule id, không thấy gì thì in ra
163
+ // "✅ G0 sổ được bảo vệ — nằm trong git · git biết cách gộp (2/2 điều kiện)"
164
+ // tức KHẲNG ĐỊNH sổ được bảo vệ trên bằng chứng chưa bao giờ được thu. Cùng hình dạng
165
+ // G55 (`pass` không có quyền khẳng định).
166
+ //
167
+ // Ca thường gặp nhất KHÔNG phải hiếm: image CI tối giản. `node:20-alpine` và
168
+ // `node:20-slim` đều KHÔNG có git — tức chính môi trường mà cổng này tồn tại để chạy.
169
+ //
170
+ // Ba trạng thái, phân biệt có chủ ý:
171
+ // ok → hỏi được, T10/T11 chạy bình thường
172
+ // no-binary → không có `git` ⇒ KHÔNG BIẾT sổ có được bảo vệ hay không
173
+ // not-a-repo → có git nhưng sổ không nằm trong repo nào ⇒ nó KHÔNG được commit
174
+ // Hai trạng thái sau đều phát T13. Gate nâng T13 thành chặn (schema →
175
+ // gate.config_preconditions.promote_lint_rules) — pre-push NHẮC, CI CHẶN, y như T10/T11.
176
+ // Thụt lề cho dòng hint nhiều dòng — trùng khuôn của err()/warn().
177
+ const NL = String.fromCharCode(10) + " ";
178
+ let _gitState = null;
179
+ function gitState(sampleFile) {
180
+ if (_gitState) return _gitState;
181
+ const cp = require('child_process');
182
+ const q = (args, opts) => cp.spawnSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'ignore', 'ignore'], ...opts });
183
+ const v = q(['--version']);
184
+ if (v.error || v.status !== 0) return (_gitState = 'no-binary');
185
+ const dir = sampleFile ? path.dirname(sampleFile) : '.';
186
+ const r = q(['rev-parse', '--git-dir'], { cwd: fs.existsSync(dir) ? dir : '.' });
187
+ if (r.error || r.status !== 0) return (_gitState = 'not-a-repo');
188
+ return (_gitState = 'ok');
189
+ }
190
+
191
+ function lintGitEvaluable(sampleFile) {
192
+ const st = gitState(sampleFile);
193
+ if (st === 'ok') return true;
194
+ const where = rel(sampleFile || TRACE_DIRS[0]);
195
+ if (st === 'no-binary') {
196
+ warn('T13', where,
197
+ 'KHÔNG kiểm được điều kiện bảo vệ sổ — môi trường này không có binary `git`',
198
+ ['T10 (luật merge) và T11 (bị gitignore) đều phải hỏi git. Không hỏi được thì hai điều',
199
+ 'kiện đó KHÔNG được đánh giá — và "không biết" KHÔNG PHẢI "sạch".',
200
+ 'Ca thường gặp nhất: image CI tối giản. node:*-alpine và node:*-slim đều KHÔNG có git.',
201
+ 'Sửa: image: node:20 (bản Debian — có git sẵn)',
202
+ ' hoặc apk add --no-cache git / apt-get install -y git'].join(NL));
203
+ return false;
204
+ }
205
+ warn('T13', where,
206
+ 'sổ trace KHÔNG nằm trong một repo git nào — nó không được version control',
207
+ ['Sổ trace phải commit: nó không regenerate được, và trace-history.jsonl là dữ liệu',
208
+ 'tích luỹ — mất là mất vĩnh viễn.',
209
+ 'Nếu đây là bản copy tạm để kiểm thì bỏ qua được. Trong CI thì KHÔNG: nghĩa là repo',
210
+ 'chưa được clone đúng, hoặc sổ nằm trong submodule chưa clone',
211
+ '(GitLab: GIT_SUBMODULE_STRATEGY=recursive · GitHub: submodules: recursive).'].join(NL));
212
+ return false;
213
+ }
214
+
158
215
  // ── T7 — marker conflict git ──────────────────────────────────────────────────
159
216
  //
160
217
  // Chạy TRƯỚC mọi rule khác và trên MỌI file: một marker lọt vào TSV làm T1/T2 báo
@@ -587,8 +644,12 @@ for (const dir of TRACE_DIRS) {
587
644
  }
588
645
  }
589
646
 
590
- if (sampleForAttr) lintMergeAttr(sampleForAttr);
591
- lintGitTracked(abs);
647
+ // T13 TRƯỚC T10/T11: nếu không hỏi được git thì hai rule kia không có gì để phán, và
648
+ // im lặng ở đó làm gate G0 khẳng định "sổ được bảo vệ" trên bằng chứng không tồn tại.
649
+ if (lintGitEvaluable(sampleForAttr || abs)) {
650
+ if (sampleForAttr) lintMergeAttr(sampleForAttr);
651
+ lintGitTracked(abs);
652
+ }
592
653
  }
593
654
 
594
655
  // ── Report ────────────────────────────────────────────────────────────────────
@@ -596,7 +657,7 @@ for (const dir of TRACE_DIRS) {
596
657
  if (AS_JSON) {
597
658
  console.log(JSON.stringify({
598
659
  ok: errors.length === 0,
599
- scanned, trace_dirs: foundDirs, errors, warns, infos,
660
+ scanned, trace_dirs: foundDirs, git_state: _gitState, errors, warns, infos,
600
661
  }, null, 2));
601
662
  process.exit(errors.length && !WARN_ONLY ? 1 : 0);
602
663
  }
package/bin/self-check.js CHANGED
@@ -695,13 +695,16 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
695
695
  {
696
696
  const inv = (schema.gate || {}).audit_invalidating_tags;
697
697
  if (inv) {
698
- const rel = inv.ci_template;
699
- if (!rel) {
700
- err('R10', '`gate.audit_invalidating_tags` thiếu `ci_template` không biết canh file nào');
701
- } else {
698
+ // MỖI template CI phải grep đủ 7 tag — không chỉ bản GitHub. Một bản dịch (GitLab…)
699
+ // không được canh mà tụt lại sau schema thì grep không khớp gì ⇒ job LUÔN XANH.
700
+ const rels = inv.ci_templates || (inv.ci_template ? [inv.ci_template] : []);
701
+ if (!rels.length) {
702
+ err('R10', '`gate.audit_invalidating_tags` thiếu `ci_templates` — không biết canh file nào');
703
+ }
704
+ for (const rel of rels) {
702
705
  const abs = path.join(ROOT, rel);
703
706
  if (!fs.existsSync(abs)) {
704
- err('R10', `\`audit_invalidating_tags.ci_template\` trỏ tới ${rel} — file không tồn tại`,
707
+ err('R10', `\`audit_invalidating_tags.ci_templates\` trỏ tới ${rel} — file không tồn tại`,
705
708
  'Template đổi tên/bị xoá? Không có nó thì lỗ "sửa code mà không audit lại" hở lại.');
706
709
  } else {
707
710
  const yml = fs.readFileSync(abs, 'utf8');
@@ -1259,6 +1259,10 @@
1259
1259
  "Cùng cách phân vai G39 đã chọn cho pre-push vs CI."
1260
1260
  ],
1261
1261
  "promote_lint_rules": [
1262
+ {
1263
+ "rule": "T13",
1264
+ "why": "KHÔNG KIỂM ĐƯỢC hai điều kiện kia — thiếu binary `git`, hoặc sổ không nằm trong repo git nào. Đây là loại THỨ TƯ, khác cả ba loại còn lại: không phải 'đang có cái sai' (🔴), không phải 'nợ' (🟠), cũng không phải 'bảo vệ đang tắt' (T10/T11) — mà là 'KHÔNG BIẾT bảo vệ có bật hay không'. Trước T13, ca này làm G0 in ra `sổ được bảo vệ — 2/2 điều kiện`: một lời KHẲNG ĐỊNH trên bằng chứng chưa bao giờ được thu, tệ hơn im lặng (cùng hình dạng G55). Ca thường gặp nhất là CI dùng image tối giản — node:*-alpine và node:*-slim đều KHÔNG có git — tức chính môi trường mà cổng này tồn tại để chạy."
1265
+ },
1262
1266
  {
1263
1267
  "rule": "T11",
1264
1268
  "why": "sổ trace đang bị gitignore — nó KHÔNG được lưu vào git, mất là mất toàn bộ (G29)"
@@ -1369,7 +1373,17 @@
1369
1373
  "self-check R10 canh templates/ci/trace-gate.yml có nhắc đủ 7 tag này. Tag đổi tên mà",
1370
1374
  "file CI không biết ⇒ grep không khớp gì ⇒ job LUÔN XANH ⇒ cổng mù im lặng."
1371
1375
  ],
1372
- "ci_template": "templates/ci/trace-gate.yml",
1376
+ "$ci_templates_comment": [
1377
+ "MỖI file CI phải grep đủ 7 tag. Trước v0.7.2 chỉ có bản GitHub Actions được canh —",
1378
+ "nên khi một team dịch sang GitLab, bản dịch KHÔNG được rule nào canh, và nếu nó tụt",
1379
+ "lại sau schema thì grep không khớp gì ⇒ job LUÔN XANH ⇒ cổng mù trong im lặng. Đúng",
1380
+ "lỗi R10 tồn tại để chặn, chỉ ở một file khác loại. Thêm template CI mới thì THÊM VÀO",
1381
+ "danh sách này ngay — R10 canh từng file."
1382
+ ],
1383
+ "ci_templates": [
1384
+ "templates/ci/trace-gate.yml",
1385
+ "templates/ci/trace-gate.gitlab-ci.yml"
1386
+ ],
1373
1387
  "tags": [
1374
1388
  {
1375
1389
  "tag": "@trace.implements",
@@ -1 +1 @@
1
- 0.7.1
1
+ 0.7.3
@@ -0,0 +1,169 @@
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # SDD Framework — Trace Gate (GitLab CI)
3
+ #
4
+ # COPY nội dung này vào `.gitlab-ci.yml` ở gốc repo. Nó KHÔNG tự chạy từ
5
+ # `.agent/templates/` — mọi thứ trong `.agent/` là bản sinh ra, bị ghi đè mỗi lần
6
+ # `/update-framework`.
7
+ #
8
+ # ⚠️ Repo chưa có `.gitlab-ci.yml`? Thêm file này = mọi push/MR từ nay sẽ kích hoạt
9
+ # pipeline. Đó là mục đích — nhưng hãy biết trước.
10
+ #
11
+ # GIỚI HẠN — đọc trước khi tin:
12
+ # Job này chứng minh "report khớp SỔ, và sổ không có cờ 🔴".
13
+ # Nó KHÔNG chứng minh "sổ khớp CODE" — việc đó cần quét tag trong source, đọc
14
+ # .feature, so version, tức cần `/validate-traces` (một lệnh LLM, không chạy được
15
+ # trong CI thường). Nên nó bắt ca phổ biến "quên chạy lại /validate-traces",
16
+ # nhưng KHÔNG bắt ca "sửa code mà không đụng sổ".
17
+ #
18
+ # ─────────────────────────────────────────────────────────────────────────────
19
+ # BỐN BẪY KHI DỊCH TỪ GITHUB ACTIONS — file này đã vá cả bốn.
20
+ #
21
+ # Bản GitHub (`trace-gate.yml`) dựa vào ba thứ `ubuntu-latest` cho sẵn mà GitLab
22
+ # KHÔNG có. Hai bẫy đầu ỒN (job đỏ, thấy ngay). Hai bẫy sau IM LẶNG — job XANH
23
+ # trên một phép kiểm chưa bao giờ chạy, tức đúng thứ cổng này tồn tại để chống.
24
+ #
25
+ # 1. ỒN thiếu `tags:` → job stuck "no active runners that can run this job",
26
+ # dù runner Online. Group/shared runner thường có
27
+ # `run_untagged = false`, nên job KHÔNG tag sẽ không
28
+ # bao giờ được nhận. Điền đúng tag của runner bạn.
29
+ # 2. ỒN thiếu Node → `npx: not found`. GitHub có bước
30
+ # `actions/setup-node@v4`; GitLab không có tương
31
+ # đương, và bước đó là chỗ cấp `npx`.
32
+ # 3. IM LẶNG thiếu `GIT_DEPTH: 0` → GitLab clone SHALLOW (mặc định 20). `git diff
33
+ # BASE...HEAD` không có merge-base ⇒ ra RỖNG ⇒ job
34
+ # `require-fresh-audit` in "MR không chạm tag nào"
35
+ # và PASS, trong khi bạn vừa đổi tag. XANH GIẢ.
36
+ # 4. IM LẶNG image không có git → `node:20-alpine` và `node:20-slim` đều KHÔNG có
37
+ # git. Mà lint-trace cần git cho T10 (luật merge) và
38
+ # T11 (bị gitignore) — hai điều kiện `gate-trace` G0
39
+ # NÂNG thành chặn. Không hỏi được git thì (trước T13)
40
+ # G0 in "✅ sổ được bảo vệ — 2/2 điều kiện": một lời
41
+ # KHẲNG ĐỊNH trên bằng chứng chưa bao giờ được thu.
42
+ # Từ v0.7.2 lint phát **T13** và gate FAIL đúng ca đó
43
+ # — nhưng cách sửa vẫn là dùng image CÓ git.
44
+ #
45
+ # `before_script` dưới đây in `node -v` và `git --version` để bẫy 2 và 4 lộ ra ở
46
+ # dòng log đầu tiên, thay vì lộ ra dưới dạng một dấu xanh sai.
47
+ # ─────────────────────────────────────────────────────────────────────────────
48
+
49
+ stages:
50
+ - trace
51
+
52
+ variables:
53
+ # ── BẪY 3 ── BẮT BUỘC. Không có dòng này thì `require-fresh-audit` XANH GIẢ.
54
+ GIT_DEPTH: 0
55
+
56
+ # PIN VERSION, đừng `@latest`. Cổng này quyết định PR nào được merge — một bản
57
+ # release framework đổi hành vi cổng giữa sprint là thứ bạn muốn tự chọn thời điểm.
58
+ SDD_VERSION: "0.7.2"
59
+
60
+ # Sổ/spec nằm trong submodule (umbrella + spec_source)? Bỏ comment dòng dưới.
61
+ # Không có nó, lint thấy 0 sổ → gate FAIL ở G2 ("chưa audit ≠ sạch"). Ồn, không im.
62
+ # GIT_SUBMODULE_STRATEGY: recursive
63
+
64
+ .sdd:
65
+ # ── BẪY 2 + 4 ── `node:20` là bản Debian: có SẴN cả node lẫn git.
66
+ # KHÔNG dùng `node:20-alpine` / `node:20-slim` — chúng thiếu git (xem bẫy 4).
67
+ # Buộc phải dùng alpine? Thêm: `- apk add --no-cache git`
68
+ image: node:20
69
+
70
+ # ── BẪY 1 ── ĐỔI thành đúng tag của runner bạn. Kiểm ở Settings → CI/CD → Runners.
71
+ # Runner Online mà job vẫn stuck ⇒ hoặc lệch tag, hoặc runner không bật
72
+ # "Run untagged jobs", hoặc runner chỉ nhận protected ref (MR pipeline chạy trên
73
+ # `refs/merge-requests/N/head` — KHÔNG phải protected ref, kể cả khi đích là main).
74
+ tags:
75
+ - CHANGE_ME
76
+
77
+ before_script:
78
+ # Hai dòng này tồn tại để bẫy 2 và 4 lộ ra NGAY, chứ không lộ ra dưới dạng dấu xanh sai.
79
+ - node -v
80
+ - git --version
81
+
82
+ # ── 1+2. Cấu trúc sổ + cổng chặn cờ 🔴 ───────────────────────────────────────
83
+ # Sổ trace 24 cột do LLM ghi tay: một dấu tab thiếu dồn mọi ô sang trái, ô `status`
84
+ # nhận một ngày tháng, và không cờ nào bật. lint bắt việc đó (T1/T2/T3), bắt marker
85
+ # conflict git lọt vào sổ (T7), sổ thiếu luật merge (T10), sổ bị gitignore (T11),
86
+ # row vừa DRIFT vừa mang `pass` (T12), và ca không hỏi được git (T13).
87
+ #
88
+ # `--gate-trace` tự chạy lại lint ở tầng G1, nên bước lint riêng ở trên là để có log
89
+ # dễ đọc khi đỏ. Muốn gọn thì bỏ nó và chỉ giữ `--gate-trace`.
90
+ trace-gate:
91
+ extends: .sdd
92
+ stage: trace
93
+ script:
94
+ - npx -y @educa-corp/sdd-framework@${SDD_VERSION} --lint-trace
95
+ - npx -y @educa-corp/sdd-framework@${SDD_VERSION} --gate-trace
96
+ artifacts:
97
+ when: always
98
+ paths:
99
+ - .trace/trace-report.json
100
+ expire_in: 1 week
101
+ rules:
102
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
103
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
104
+
105
+ # ── 3. Ép audit phải TƯƠI khi thứ report đang KHẲNG ĐỊNH bị đổi ──────────────
106
+ # ĐO BẰNG TAG, KHÔNG BẰNG `src/**`: framework có luật boundary-only tagging, nên
107
+ # "MR có sửa src/ không?" chặn oan cả MR chỉ thêm một field vào DTO — một file không
108
+ # mang lời khẳng định trace nào. Cái gì báo oan thì bị tắt, rồi mất luôn phần cần chặn.
109
+ # Câu hỏi đúng: "MR có chạm dòng nào mang tag mà report đang khẳng định không?"
110
+ # sửa log trong Controller → cho qua
111
+ # thêm field vào DTO → cho qua
112
+ # thêm method + @implements → CHẶN
113
+ # lấp một @trace.stub → CHẶN
114
+ #
115
+ # 7 tag dưới đây khai ở `bin/trace-schema.json` → `gate.audit_invalidating_tags`;
116
+ # `self-check` R10 canh danh sách này khớp schema. Tag đổi tên mà file CI không biết
117
+ # ⇒ grep không khớp gì ⇒ job LUÔN XANH ⇒ cổng mù trong im lặng.
118
+ #
119
+ # GIỚI HẠN ĐÃ BIẾT: đổi tên class (AuthService → AuthenticationService) không chạm
120
+ # dòng tag ⇒ job này bỏ lọt, dù cột `implemented_by` giờ trỏ vào tên không còn. Chấp
121
+ # nhận có chủ ý: ca đó ít gặp và KHÔNG im lặng (lần `/validate-traces` sau báo ngay),
122
+ # còn báo oan thì xảy ra mỗi ngày và hậu quả là CỔNG BỊ TẮT.
123
+ require-fresh-audit:
124
+ extends: .sdd
125
+ stage: trace
126
+ rules:
127
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
128
+ script:
129
+ - |
130
+ # ── BẪY 3, phần thứ hai ── GitHub có `github.base_ref` sẵn trong checkout.
131
+ # GitLab phải FETCH branch đích TRƯỚC — nếu không thì "$BASE" không tồn tại,
132
+ # `git diff` ra rỗng, và job PASS sai. Refspec tường minh để chắc chắn
133
+ # `refs/remotes/origin/<branch>` được cập nhật.
134
+ BR="$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
135
+ git fetch --no-tags origin "+refs/heads/${BR}:refs/remotes/origin/${BR}"
136
+ BASE="origin/${BR}"
137
+
138
+ # Fail-fast: chứng minh merge-base tồn tại. Không có nó, mọi kết luận bên dưới
139
+ # dựa trên một diff rỗng — và "rỗng" ở đây nghĩa là "không biết", không phải "sạch".
140
+ if ! git merge-base "$BASE" HEAD >/dev/null 2>&1; then
141
+ echo "🔴 Không tìm được merge-base giữa $BASE và HEAD."
142
+ echo " Gần như chắc chắn là clone SHALLOW — kiểm biến GIT_DEPTH: 0 ở đầu file."
143
+ echo " Không có merge-base thì git diff ra rỗng và job này sẽ PASS SAI."
144
+ exit 1
145
+ fi
146
+
147
+ TAGS='@trace\.(implements|verifies|seam_port|seam_pending|stub|stub_owner|stub_for)'
148
+ touched=$(git diff -U0 "$BASE"...HEAD | grep -E "^[+-].*${TAGS}" | head -5 || true)
149
+ audit=$(git diff --name-only "$BASE"...HEAD | grep -E 'trace-report\.json$' | head -1 || true)
150
+
151
+ if [ -n "$touched" ] && [ -z "$audit" ]; then
152
+ echo "🔴 MR đổi tag trace nhưng không kèm trace-report.json được sinh lại."
153
+ echo ""
154
+ echo "Những dòng này đã đổi:"
155
+ echo "$touched" | sed 's/^/ /'
156
+ echo ""
157
+ echo "Trace gate chỉ chứng minh 'report khớp SỔ'. Tag vừa đổi mà chưa audit lại"
158
+ echo "thì mọi cờ 🔴 trong report nói về trạng thái TRƯỚC khi bạn sửa."
159
+ echo ""
160
+ echo "Chạy trong Claude Code: /validate-traces"
161
+ echo "Rồi commit: .trace/trace-report.json + *.tsv"
162
+ exit 1
163
+ fi
164
+
165
+ if [ -n "$touched" ]; then
166
+ echo "✅ Tag trace có đổi, và audit đã được sinh lại cùng MR."
167
+ else
168
+ echo "✅ MR không chạm tag nào mà report đang khẳng định — audit vẫn còn đúng."
169
+ fi
@@ -98,7 +98,7 @@ sprint thứ ba không ai làm.** Framework có hai lệnh CLI trả exit code
98
98
 
99
99
  | Lệnh | Chặn gì | Đặt ở đâu |
100
100
  |---|---|---|
101
- | `--lint-trace` | **Cấu trúc sổ** (12 rule): header lệch · row sai số ô · enum sai · `sc_id` trùng · marker conflict git · `.jsonl` hỏng. Cộng **T12 — nhất quán GIỮA các ô**: row vừa `status ∈ {DRIFT, ORPHANED}` vừa mang `dev_selftest`/`qc_status = pass`. Cộng hai điều kiện **cấu hình** ở mức ⚠️: thiếu luật merge · sổ bị gitignore | pre-push **và** CI |
101
+ | `--lint-trace` | **Cấu trúc sổ** (13 rule): header lệch · row sai số ô · enum sai · `sc_id` trùng · marker conflict git · `.jsonl` hỏng. Cộng **T12 — nhất quán GIỮA các ô**: row vừa `status ∈ {DRIFT, ORPHANED}` vừa mang `dev_selftest`/`qc_status = pass`. Cộng hai điều kiện **cấu hình** ở mức ⚠️: thiếu luật merge · sổ bị gitignore | pre-push **và** CI |
102
102
  | `--gate-trace` | **Cấu hình** (nâng hai ⚠️ trên thành chặn) + **cờ 🔴**: `ORPHANED` · `TRACE_ORPHAN` · `SEAM_UNWIRED` · `STUB_UNRESOLVED` | CI (cần report tươi) |
103
103
 
104
104
  ```bash
@@ -106,12 +106,13 @@ npx @educa-corp/sdd-framework --lint-trace # exit 1 nếu sổ hỏng
106
106
  npx @educa-corp/sdd-framework --gate-trace # exit 1 nếu có cờ 🔴
107
107
  ```
108
108
 
109
- Mẫu sẵn dùng: `.agent/templates/ci/trace-gate.yml` (GitHub Actions) · `.agent/templates/hooks/pre-push`.
109
+ Mẫu sẵn dùng: `.agent/templates/ci/trace-gate.yml` (GitHub Actions) · `.agent/templates/ci/trace-gate.gitlab-ci.yml` (GitLab CI) · `.agent/templates/hooks/pre-push`.
110
110
  **Copy ra khỏi `.agent/`** trước khi dùng — mọi thứ trong đó bị ghi đè mỗi lần `/update-framework`.
111
111
 
112
112
  ### Bốn tầng của `--gate-trace`, và vì sao thứ tự đó
113
113
 
114
114
  0. **Sổ được bảo vệ?** — sổ có **nằm trong git**? git có **biết cách gộp** sổ? Kiểm cấu trúc của một quyển sổ sắp mất thì vô nghĩa. Hai điều kiện này `lint-trace` đã phát hiện dưới dạng ⚠️; gate **nâng** chúng thành lỗi chặn — *pre-push nhắc, CI chặn*.
115
+ **Và một điều kiện thứ ba: có kiểm được không?** (`T13`) — thiếu binary `git`, hoặc sổ ngoài mọi repo. Trước v0.7.2, ca này làm G0 in `✅ sổ được bảo vệ — 2/2 điều kiện` trên bằng chứng chưa bao giờ thu, vì G0 lọc warns theo rule id và không thấy gì thì kết luận *"sạch"* thay vì *"không biết"*. Ca thường gặp nhất là **image CI tối giản** — `node:*-alpine` và `node:*-slim` đều không có git, tức chính môi trường mà cổng này tồn tại để chạy.
115
116
  1. **Sổ đúng hình dạng?** (gọi `--lint-trace`) — phán trạng thái trên sổ lệch cột là phán trên dữ liệu rác.
116
117
  2. **Report còn tươi VÀ phủ toàn bộ?** — đối chiếu `trace-report.json` với chính sổ TSV nó khai
117
118
  là đang mô tả. Không có tầng này thì cổng là **sân khấu**: chạy `/validate-traces` một lần,
@@ -193,6 +193,8 @@ Shared code dò qua **import chain** từ boundary → tránh tag explosion.
193
193
  >
194
194
  > ⓘ = **không phải lỗi.** Hai cờ `*_STALE_REF` tồn tại vì version PRD/tech-doc là **MỘT số cho cả tài liệu nhiều UC** — thêm một UC làm mọi UC cũ lệch số dù không đổi một chữ. Không lọc thì cả loạt UC ăn cờ đỏ oan, và làm theo hướng dẫn cũng không tắt được (`/generate-code` skip row đang `OK`). Sạch bằng `--realign-*`: chỉ sửa dòng `@trace.*`, **không đụng logic**, và **từ chối chạy** nếu UC đó đang thật sự `DRIFT`/`ORPHANED`.
195
195
  >
196
+ > **Không kiểm được ≠ sạch.** `lint-trace` **T13** phát khi không hỏi được `git` (image CI thiếu git, hoặc sổ ngoài repo) — và `gate-trace` G0 **fail**, thay vì khẳng định `sổ được bảo vệ — 2/2 điều kiện` trên bằng chứng chưa bao giờ thu. Cùng nguyên tắc `spec_edit_detection.degraded`: *không bịa cờ, và không im lặng*.
197
+
196
198
  > **Mọi cờ đều PHẢI có counter `{flag}_count`** trong Step 7 + `summary` của `trace-report.json` — `bin/self-check.js` R7 ép, không có ngoại lệ. Thiếu counter = cờ vô hình với dashboard.
197
199
 
198
200
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@educa-corp/sdd-framework",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Spec Driven Development workflow framework for Claude Code",
5
5
  "bin": {
6
6
  "sdd-framework": "./bin/index.js"