@educa-corp/sdd-framework 0.9.0 → 0.9.2

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 +56 -12
  2. package/bin/qc-base-map.json +595 -0
  3. package/bin/self-check.js +146 -0
  4. package/bin/trace-schema.json +5 -0
  5. package/core/FRAMEWORK_VERSION +1 -1
  6. package/core/commands/generate-code.md +3 -2
  7. package/core/commands/propose-scenario.md +1 -1
  8. package/core/commands/qc-analyze.md +260 -22
  9. package/core/commands/qc-design-test.md +1 -1
  10. package/core/commands/qc-plan.md +7 -4
  11. package/core/commands/qc-run-test.md +1 -1
  12. package/core/commands/refine-prd.md +47 -20
  13. package/core/commands/report-bug.md +1 -1
  14. package/core/commands/review-context.md +27 -1
  15. package/core/commands/validate-traces.md +26 -2
  16. package/core/skills/qc/qa-analyst/DOC_GAP.template.md +117 -0
  17. package/core/skills/qc/qa-analyst/acceptance-criteria.md +4 -2
  18. package/core/skills/qc/qa-analyst/business-rules.md +38 -4
  19. package/core/skills/qc/qa-analyst/data-flow.md +5 -3
  20. package/core/skills/qc/qa-analyst/exhaustive-gap-scanner.md +174 -0
  21. package/core/skills/qc/qa-analyst/spec-breakdown.md +9 -7
  22. package/core/skills/qc/qa-analyst/spec-issue-reporter.md +100 -0
  23. package/core/skills/qc/qa-designer/e2e/journey.md +2 -2
  24. package/core/skills/qc/qa-designer/exploratory/charter.md +1 -1
  25. package/core/skills/qc/qa-designer/exploratory/explore-to-functional.md +1 -1
  26. package/core/skills/qc/qa-designer/functional/api.md +2 -2
  27. package/core/skills/qc/qa-designer/functional/gui-feature.md +2 -2
  28. package/core/skills/qc/qa-designer/functional/gui-screen.md +2 -2
  29. package/core/skills/qc/qa-designer/integration/api.md +2 -2
  30. package/core/skills/qc/qa-designer/integration/db.md +2 -2
  31. package/core/skills/qc/qa-designer/integration/gui.md +2 -2
  32. package/core/skills/qc/qa-designer/integration/kafka.md +2 -2
  33. package/core/skills/qc/qa-designer/non-functional.md +2 -2
  34. package/core/skills/qc/qa-planner/risk-model.md +106 -0
  35. package/core/skills/qc/qa-planner/test-plan.md +13 -10
  36. package/core/skills/qc/qa-reviewer/script/e2e.md +1 -1
  37. package/core/skills/qc/qa-reviewer/script/exploratory.md +1 -1
  38. package/core/skills/qc/qa-reviewer/script/functional.md +1 -1
  39. package/core/skills/qc/qa-reviewer/script/integration.md +1 -1
  40. package/core/skills/qc/qa-reviewer/script/non-functional.md +1 -1
  41. package/core/skills/qc/qa-reviewer/test-case/e2e.md +1 -1
  42. package/core/skills/qc/qa-reviewer/test-case/exploratory.md +1 -1
  43. package/core/skills/qc/qa-reviewer/test-case/functional.md +1 -1
  44. package/core/skills/qc/qa-reviewer/test-case/integration.md +2 -2
  45. package/core/skills/qc/qa-reviewer/test-case/non-functional.md +1 -1
  46. package/core/skills/qc/qa-runner/e2e.md +1 -1
  47. package/core/skills/qc/qa-runner/exploratory/session.md +1 -1
  48. package/core/skills/qc/qa-runner/functional/api.md +1 -1
  49. package/core/skills/qc/qa-runner/functional/gui-feature.md +1 -1
  50. package/core/skills/qc/qa-runner/functional/gui-screen.md +1 -1
  51. package/core/skills/qc/qa-runner/integration.md +1 -1
  52. package/core/skills/qc/qa-runner/non-functional.md +1 -1
  53. package/core/skills/qc/qa-runner/report/report.md +1 -1
  54. package/core/steps/gap-verify.md +231 -0
  55. package/core/steps/review-fanout.md +27 -1
  56. package/core/templates/project-context.yaml +2 -2
  57. package/docs/02-concepts/pipeline-steps/08-qc-automation.md +2 -2
  58. package/docs/04-reference/commands.md +1 -1
  59. package/docs/explain/03-refine-prd.md +8 -6
  60. package/docs/explain/15-qc-analyze.md +10 -7
  61. package/docs/explain/16-qc-plan.md +2 -2
  62. package/docs/plans/qc-implementation-log.md +1446 -0
  63. package/docs/plans/qc-merge-plan.md +502 -0
  64. package/docs/plans/qc-sync-command.md +358 -0
  65. package/package.json +1 -1
  66. package/core/skills/qc/qa-analyst/DOC_GAPS.template.md +0 -63
package/bin/self-check.js CHANGED
@@ -61,6 +61,7 @@
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
64
65
 
65
66
  const ROOT = path.join(__dirname, '..');
66
67
  const WARN_ONLY = process.argv.includes('--warn-only');
@@ -1182,6 +1183,151 @@ for (const [vocab, def] of Object.entries(schema.vocabularies)) {
1182
1183
  }
1183
1184
  }
1184
1185
 
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
+
1185
1331
  // ── report ────────────────────────────────────────────────────────────────────
1186
1332
  const tagCount = schema.tags.length;
1187
1333
  const colCount = schema.tsv_columns.length;
@@ -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.2
@@ -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:
@@ -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_GAPS` từ `/qc-analyze`). Draft một Gherkin scenario vào **khu proposal** để
4
+ `DOC_GAP` 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
@@ -9,6 +9,9 @@ ported_from: ai-automation-qc-base
9
9
  > Stage 1 của QC automation pipeline native (qc-analyze → qc-plan → qc-design-test → qc-review → qc-run-test → qc-report). Port từ qa-analyst của team QC. Markdown-first: không có script ở đây.
10
10
 
11
11
  ## Gate
12
+
13
+ *Checkpoint: **chặn thường** — lệnh ghi 2 file artifact. `--yes` bỏ qua được (gate Bước 3a).*
14
+
12
15
  # Gate — Quy trình vào chuẩn cho mọi lệnh
13
16
 
14
17
  Mọi lệnh PHẢI chạy gate này trước khi thực thi phần logic riêng của nó.
@@ -162,7 +165,7 @@ Mỗi dòng ⚠️/🔴 phải ứng với một trạng thái **context-loader
162
165
  🔴/⚠️ (không chặn ≠ không báo — người đọc log sau này vẫn cần thấy).
163
166
 
164
167
 
165
- *Lưu ý: Với lệnh này, target ở Bước 1 là một UC-ID hoặc file feature/PRD. Đọc spec chính thức của UC đó — file `.feature` (mang `@trace.id={UC-ID}` và mỗi scenario `@trace.scenario={UC-ID}-SC{N}`), PRD, và design-spec — từ feature package `{paths.specs_dir}/{domain}/{prd-slug}/` (file `.feature` dưới `bdd/`, file PRD `{TICKET-ID}-{prd-slug}.md` ở gốc folder, và design-spec dưới `design-spec/`). Spec của framework CHÍNH LÀ source of truth; đừng suy lại các requirement đã có ở đó.*
168
+ *Lưu ý: Với lệnh này, target ở Bước 1 là một UC-ID hoặc file feature/PRD. Đọc spec chính thức của UC đó — file `.feature` (mang `@trace.id={UC-ID}` và mỗi scenario `@trace.scenario={UC-ID}-SC{N}`), PRD, và design-spec — từ feature package `{paths.specs_dir}/{domain}/{prd-slug}/` (file `.feature` dưới `bdd/`, file PRD `{TICKET-ID}-{prd-slug}.md` ở gốc folder, và design-spec dưới `design-spec/`). Spec của framework CHÍNH LÀ source of truth; đừng suy lại các requirement đã có ở đó. **Ngoài ra đọc tech-doc gộp** `{paths.tech_docs_dir}/{domain}/{prd-slug}/tech-docs/{TICKET-ID}-tech-design.md` làm **nguồn thứ hai** — xem §Đối chiếu tài liệu kỹ thuật.*
166
169
 
167
170
  ## Context
168
171
  **BẮT BUỘC — đọc `.agent/steps/context-loader.md` và thực thi TOÀN BỘ quy trình trong đó**,
@@ -174,30 +177,103 @@ placeholder bên dưới sẽ rỗng và lệnh sẽ đọc/ghi sai chỗ.
174
177
 
175
178
  ---
176
179
 
180
+ ## Platform Resolution *(thiết lập cho cả QC pass — mọi stage sau kế thừa)*
181
+
182
+ > **PHẢI chạy TRƯỚC Guard bên dưới.** `{UC-ID}-SC{N}` chỉ độc nhất trong (UC × platform), nên
183
+ > một UC đa nền có **nhiều file `.feature`** — `bdd/web/`, `bdd/app/`, `bdd/system/` — và mỗi
184
+ > file mang `@trace.status` **riêng**: bản web có thể `approved` trong khi bản app còn `draft`.
185
+ > Guard đọc "file `.feature` của UC" khi chưa biết platform là đọc một file **bất kỳ trong ba**:
186
+ > báo `approved` trong khi bản đang dùng còn nháp, hoặc chặn oan một bản đã duyệt.
187
+ > Chốt platform trước thì Guard mới có đúng một file để đọc.
188
+
189
+ `{UC-ID}-SC{N}` chỉ độc nhất trong (UC × platform) — `web SC3` và `app SC3` là hai scenario khác nhau, và sổ trace tách theo `{UC-ID}-{platform}.tsv`. Nên **một QC pass khoá đúng MỘT platform**, và mọi artifact QC nằm dưới `{paths.qc_dir}/{UC-ID}/{active_platform}/`.
190
+
191
+ Phân giải `active_platform` — theo thứ tự, dừng ở cái đầu tiên khớp:
192
+ 1. `$ARGUMENTS` nêu platform (`web`/`app`/`system`) → dùng.
193
+ 2. Target ở Bước 1 là một file `.feature` → đọc `# @trace.platform` của nó.
194
+ 3. Glob `{paths.specs_dir}/{domain}/{prd-slug}/bdd/*/` — **đúng một** thư mục platform tồn tại → dùng nó.
195
+ 4. Nhiều platform mà không suy được → hỏi *"QC pass này cho platform nào? (web/app/system)"*.
196
+ **Có `--yes`:** không hỏi — dừng với lỗi rõ ràng, vì đoán bừa platform là ghi artifact vào sai
197
+ thư mục và ghi `qc_status` vào sai sổ trace:
198
+ ```
199
+ ❌ {UC-ID} có {n} platform ({list}) — không suy được platform nào cho QC pass này.
200
+ Chạy headless thì phải nêu tường minh: /qc-analyze {UC-ID} web --yes
201
+ ```
202
+
203
+ Lưu `active_platform`. Đọc **đúng file `.feature` của platform đó** (`{paths.specs_dir}/{domain}/{prd-slug}/bdd/{active_platform}/{UC-ID}*.feature`) làm nguồn SC — không trộn SC chéo platform.
204
+
205
+ ---
206
+
177
207
  ## Guard — BDD đã duyệt chưa
178
208
 
179
- Đọc `# @trace.status:` từ header file `.feature` của UC target:
209
+ *Chạy SAU Platform Resolution xem do khối trên.*
210
+
211
+ Đọc `# @trace.status:` từ header file `.feature` **của `{active_platform}` đã phân giải**:
180
212
  - `approved` → tiếp tục bình thường.
181
213
  - `draft` (hoặc khác `approved`) → **CHECKPOINT cảnh báo mềm** (không chặn cứng — cho phép QC sớm/prototype):
182
214
  ```
183
- ⚠️ BDD của {UC-ID} đang ở @trace.status: {status} (chưa duyệt). QC chạy trên BDD chưa chốt có thể phải làm lại.
215
+ ⚠️ BDD của {UC-ID} ({active_platform}) đang ở @trace.status: {status} (chưa duyệt). QC chạy trên BDD chưa chốt có thể phải làm lại.
184
216
  Khuyến nghị: review-context (BDD) sạch + người duyệt đặt `# @trace.status: approved` rồi mới chạy QC.
185
217
  Vẫn chạy QC bây giờ? (Y/N)
186
218
  ```
187
- Chỉ tiếp khi chọn Y.
219
+ Chỉ tiếp khi chọn Y — **trừ khi `$ARGUMENTS` có `--yes`**: coi như Y, **nhưng vẫn IN khối cảnh
220
+ báo** (không chặn ≠ không báo — người đọc log sau này vẫn cần thấy QC đã chạy trên BDD nháp).
221
+
222
+ > **Vì sao `--yes` phải phủ cả guard mềm này.** `steps/gate.md` mở đường chạy headless
223
+ > (`claude -p "… --yes"`), nhưng `--yes` chỉ khai là bỏ qua CHECKPOINT của gate. Guard mềm ở đây
224
+ > là một `(Y/N)` thứ hai — nên lệnh vẫn treo vô hạn ở chế độ không có người trả lời, và đường
225
+ > headless mà gate hứa bị bít cho đúng lệnh này. Một cổng chỉ chặn được khi có người ngồi đó
226
+ > thì ở chế độ headless nó không phải cổng, nó là treo.
188
227
 
189
228
  ---
190
229
 
191
- ## Platform Resolution *(thiết lập cho cả QC pass mọi stage sau kế thừa)*
230
+ ## Đối chiếu tài liệu kỹ thuật *(nguồn thứ hai bắt lệch nghiệp vụ kỹ thuật)*
192
231
 
193
- `{UC-ID}-SC{N}` chỉ độc nhất trong (UC × platform) — `web SC3` và `app SC3` là hai scenario khác nhau, và sổ trace tách theo `{UC-ID}-{platform}.tsv`. Nên **một QC pass khoá đúng MỘT platform**, và mọi artifact QC nằm dưới `{paths.qc_dir}/{UC-ID}/{active_platform}/`.
232
+ Định vị tech-doc gộp cấp PRD: `{paths.tech_docs_dir}/{domain}/{prd-slug}/tech-docs/{TICKET-ID}-tech-design.md`.
233
+ Nó phủ **nhiều UC** — lọc theo `@trace.ucs` ở header, chỉ đọc phần chạm `{UC-ID}` đang xét.
194
234
 
195
- Phân giải `active_platform`:
196
- - Target ở Bước 1 là một file `.feature` → đọc `# @trace.platform` của nó.
197
- - `$ARGUMENTS` nêu platform (`web`/`app`/`system`) dùng.
198
- - Ngược lại hỏi: *"QC pass này cho platform nào? (web/app/system)"* chờ chọn.
235
+ **Không tìm thấy → cảnh báo mềm, KHÔNG chặn** (dự án có thể chưa dựng tech-doc):
236
+ ```
237
+ ⚠️ Không có tech-doc cho {TICKET-ID} phân tích chỉ dựa trên PRD + BDD + design-spec.
238
+ Lệch giữa yêu cầu nghiệp vụ hợp đồng kỹ thuật (enum, mã lỗi, ràng buộc field) sẽ KHÔNG được phát hiện ở trạm này.
239
+ ```
199
240
 
200
- Lưu `active_platform`. Đọc **đúng file `.feature` của platform đó** (`{paths.specs_dir}/{domain}/{prd-slug}/bdd/{active_platform}/{UC-ID}*.feature`) làm nguồn SC không trộn SC chéo platform.
241
+ **Có đối chiếu các mục sau với PRD/BDD, mỗi chỗ vênh một gap `CONTRADICTORY`:**
242
+
243
+ | Mục tech-doc | Đối chiếu gì với PRD/BDD |
244
+ |---|---|
245
+ | §3 Mô hình dữ liệu | thực thể/field/quan hệ PRD nhắc tới có khớp không |
246
+ | **§4 Hợp đồng API** | **enum & tập giá trị hợp lệ** · ràng buộc field (độ dài, định dạng, bắt buộc) · **mã lỗi** — PRD nêu bao nhiêu nhánh lỗi, contract định nghĩa bao nhiêu |
247
+ | §4.5 Ánh xạ component UI | màn/component PRD·design-spec mô tả có mặt đủ không |
248
+ | §5 Luồng chính | thứ tự bước, nhánh rẽ có khớp scenario `.feature` không |
249
+ | §6 Điểm tích hợp | side-effect PRD nêu (gửi sự kiện, gọi dịch vụ khác) có được định nghĩa không |
250
+ | §8 Xử lý lỗi & biên | trường hợp biên PRD nêu có đường xử lý không, và ngược lại |
251
+
252
+ > **Vì sao mục này tồn tại.** Có một lớp gap **chỉ lộ ra khi so hai loại tài liệu với nhau** —
253
+ > đọc riêng bên nào cũng thấy hợp lý. Ca điển hình: PRD viết *"chọn lớp 1–6"*, contract định
254
+ > nghĩa enum `1..9`. Không ai đọc cả hai thì không ai thấy, và nó ra tận lúc chạy thật.
255
+ > **Đây là lý do trạm này đọc tech-doc — không phải để hiểu kỹ thuật, mà để bắt chỗ hai bên nói khác nhau.**
256
+
257
+ ### §12 GAP Register — ĐỌC, KHÔNG GHI
258
+
259
+ Tech-doc có sổ ẩn số thiết kế riêng (`§12`), với vòng đời và người chịu trách nhiệm riêng, và
260
+ `/generate-code` đã canh nó. **Trạm này chỉ đọc, tuyệt đối không ghi vào.**
261
+
262
+ Với mỗi mục `open` trong §12 chạm `{UC-ID}`:
263
+ - **KHÔNG mở gap mới** trong `DOC_GAP.md` về cùng chuyện đó.
264
+ - Ghi vào `REQUIREMENT_ANALYSIS.md` mục *"Đang chờ chốt (từ §12 tech-doc)"*: `{id}` · điều chưa biết · owner · severity.
265
+ - Test case chạm nó về sau sẽ bị chặn — nhưng bị chặn bởi **một mục đã có người xử lý**, không phải bởi một câu hỏi mới gửi PO.
266
+
267
+ > **Vì sao không ghi vào.** Một ẩn số đã nằm trong §12 nghĩa là **đã có người đang lo**: có
268
+ > owner, có mức chặn, có cổng chặn sinh code. Mở lại nó thành gap QC là gửi PO một câu hỏi
269
+ > về thứ không phải việc của PO, và tạo hai sổ cùng theo dõi một chuyện — rồi chúng lệch nhau.
270
+ > Đây đúng là **câu hỏi lọc Q1** của `steps/gap-verify.md` (*"chỗ này đã được trả lời ở tài liệu
271
+ > khác chưa?"*), chỉ mở rộng phạm vi "tài liệu khác" thêm một nguồn.
272
+
273
+ **Ngoại lệ — mục `spec-defect` là việc của PO.** §12 phân ba loại: `nội tại` (backend tự quyết) ·
274
+ `cross-service` (đội khác) · `spec-defect` (PRD/BDD sai hoặc thiếu). Hai loại đầu → ghi "đang chờ".
275
+ Loại thứ ba **đúng là gap tài liệu** → vẫn ghi vào `DOC_GAP.md`, trỏ ngược về `{id}` của §12 để
276
+ không đếm hai lần.
201
277
 
202
278
  ---
203
279
 
@@ -228,42 +304,204 @@ File `.feature` chính thức đã định nghĩa scenario là `@trace.scenario=
228
304
  và ghi lại mapping — qc-design-test và qc-run-test cần nó để gắn tag
229
305
  `@trace.verifies` cho test và ghi `qc_status` theo từng scenario.
230
306
 
231
- ## DOC_GAPS (bắt buộc)
307
+ ## Quét gap — hai nguồn, gộp rồi mới thẩm định
308
+
309
+ Gap đến từ **hai chỗ**, và chúng bổ sung nhau chứ không thay thế:
310
+
311
+ | Nguồn | Trả lời câu | Gap là |
312
+ |---|---|---|
313
+ | **4 kỹ năng phân tích** ở trên | *"yêu cầu là gì?"* | sản phẩm phụ — đang bóc luật nghiệp vụ thì gặp chỗ mâu thuẫn |
314
+ | **Quét theo lăng kính** *(dưới đây)* | *"còn thiếu gì?"* | mục tiêu chính |
315
+
316
+ ### Quét theo lăng kính
317
+
318
+ Chạy `steps/review-fanout.md` với:
232
319
 
233
- Luôn tạo một file gaps theo `{paths.qc_skills_dir}/qa-analyst/DOC_GAPS.template.md`:
320
+ | Tham số | Giá trị |
321
+ |---|---|
322
+ | `DIMENSIONS` | **4 lăng kính** — `D2 Xử lý lỗi` · `D3 Giao diện` · `D4 Dữ liệu & cấu hình` · `D5 Đối chiếu chéo` **(thu hẹp — xem dưới)** *(định nghĩa ở `{paths.qc_skills_dir}/qa-analyst/exhaustive-gap-scanner.md`)* |
323
+ | `FINDINGS SCHEMA` | như §Output dưới đây |
324
+ | `GRANULARITY` | **`auto`** — chia theo ngưỡng kích thước, KHÔNG ép mịn theo từng UC |
325
+ | `VERIFY` | **`off`** — thẩm định chạy MỘT lần ở bước sau, trên tập đã gộp |
326
+
327
+ **`D5` chạy ở dạng THU HẸP — chỉ 2 trong 4 cặp tài liệu:**
328
+
329
+ | Cặp | |
330
+ |---|---|
331
+ | `PRD ↔ design-spec/` | ✅ **SO** — không ai đối chiếu nội dung. `/generate-bdd` chỉ kiểm `Built from PRD` (số phiên bản); cùng phiên bản mà nội dung lệch thì lọt |
332
+ | `bdd/{platform}/ ↔ design-spec/` | ✅ **SO** — không ai |
333
+ | `PRD ↔ bdd/` | ❌ bỏ — `/review-context` **B1** đã làm |
334
+ | `PRD·bdd/ ↔ tech-docs/` | ❌ bỏ — §Đối chiếu tài liệu kỹ thuật **ở trên** đã làm |
335
+
336
+ > **Cả hai cặp SO đều dính `design-spec/`** — artifact duy nhất trong feature package mà **không
337
+ > tài liệu nào đối chiếu nội dung với nó**. Đừng lẫn với `tech-docs/`: `design-spec/` là *giao diện
338
+ > Designer vẽ*, `tech-docs/` là *hợp đồng hệ thống* — và `tech-docs/` đã được phủ ở §trên.
339
+ >
340
+ > Trạm này **đã đọc `design-spec/`** từ trước (nó nằm trong danh sách nguồn ở Gate), nên `D5`
341
+ > không nạp thêm file nào — chỉ bắt nó **so** thay vì chỉ **đọc**. Rẻ hơn một lăng kính thường.
342
+
343
+ **`D1 Luật nghiệp vụ` là lăng kính duy nhất KHÔNG bật:** `qa-analyst/business-rules.md` đã hỏi
344
+ 4/5 câu của nó, và hỏi cụ thể hơn — *"min/max · ký tự cho phép · trim · định dạng"* thay vì
345
+ *"ngưỡng đã chốt chưa"*.
346
+
347
+ > **Ghi lại vì sao `D5` từng bị tắt:** lý do ban đầu là *"trùng nhiều"* — **đúng một nửa**. Nó phủ
348
+ > **bốn** cặp, chỉ **hai** cặp đã có người làm. Sai vì suy từ ấn tượng thay vì đếm danh sách; bảng
349
+ > kiểm chứng 31 câu hỏi (`docs/plans/qc-implementation-log.md`) là thứ đáng lẽ phải làm **trước**
350
+ > khi quyết. Đừng tắt lại `D5` mà không đọc bảng đó.
351
+
352
+ > **`GRANULARITY = auto`, không phải `per-uc`.** `/refine-prd` ép mịn theo từng UC vì ở tầng PRD
353
+ > một gap bỏ sót **im lặng đi tiếp** tới tận lúc chạy thật. Ở đây khác: gap bỏ sót còn **bốn lớp
354
+ > chặn phía sau** — trạm 3 bật ngược khi không viết nổi giá trị mong đợi, trạm 4 soát độ phủ,
355
+ > trạm 5 phân loại lỗi thật vs script sai. Ép mịn ở đây tốn gấp ~3 lần cho tính năng nhỏ mà đổi
356
+ > lấy một lưới an toàn đã có sẵn ba lớp khác.
357
+
358
+ ### Gộp trước, thẩm định sau
359
+
360
+ Gộp gap từ **cả hai nguồn** vào một tập trước khi sang bước thẩm định.
361
+
362
+ > **Không thẩm định từng nguồn riêng.** Phép kiểm `T6` của `gap-verify` là *"hai gap cùng gốc
363
+ > thì gộp lại"* — nó chỉ chạy được khi **thấy toàn bộ** tập. Thẩm định hai lần trên hai tập rời
364
+ > thì không bắt được trùng lặp chéo nguồn, và PO nhận hai câu hỏi giống nhau.
365
+
366
+ ---
367
+
368
+ ## DOC_GAP (bắt buộc)
369
+
370
+ Luôn tạo một file gaps theo `{paths.qc_skills_dir}/qa-analyst/DOC_GAP.template.md`:
234
371
  - Mỗi gap `GAP-xx`, phân loại MISSING / AMBIGUOUS / CONTRADICTORY / ASSUMPTION / OPEN QUESTION, với severity (🔴 Blocker → 🟢 Low) và function/BR/AC bị ảnh hưởng.
235
372
  - Không bao giờ bịa câu trả lời; đánh dấu giả định là `ASSUMPTION` để PO/dev confirm.
236
373
  - Bất kỳ `🔴 Blocker` nào còn `Open` ⇒ UC chưa sẵn sàng cho qc-design-test — bàn giao cho qc-plan.
237
374
  - **Đẩy các defect spec thực sự lên PO (không chỉ giữ local).** Một blocker là lỗi thật
238
375
  trong spec chính thức — `AMBIGUOUS` / `CONTRADICTORY` / `MISSING` trong PRD/BDD — phải tới
239
- PO qua feedback flow, không chỉ nằm trong `DOC_GAPS.md`: tạo `/report-bug {UC-ID} {desc}`
376
+ PO qua feedback flow, không chỉ nằm trong `DOC_GAP.md`: tạo `/report-bug {UC-ID} {desc}`
240
377
  (BUG_FLOW của nó phân loại PRD vs BDD), hoặc `/propose-scenario {UC-ID}` nếu gap là thiếu test
241
378
  coverage. Gap `ASSUMPTION` / `OPEN QUESTION` được confirm qua questions-for-dev của qc-plan — không file thành bug.
242
379
 
380
+ ### Thẩm định trước khi bàn giao *(bắt buộc)*
381
+
382
+ Sinh xong `DOC_GAP.md`, **đọc `.agent/steps/gap-verify.md` và chạy toàn bộ quy trình trong đó**
383
+ với:
384
+ - `FINDINGS` = mọi gap đang `Open` trong `DOC_GAP.md`
385
+ - `EVIDENCE_ROOT` = `{paths.specs_dir}` — spec repo của PO, **không** phải `{paths.qc_dir}`
386
+ - `VERDICT_FIELD` = cột `Trạng thái` + `Câu trả lời` của bảng gap
387
+ - `RERATE` = `on`
388
+
389
+ Gap rớt thẩm định được **đóng kèm lý do**, KHÔNG xoá — người đọc phải kiểm chứng được vì sao
390
+ nó bị loại. Cập nhật `Tổng số gap` + bảng ưu tiên sau khi áp verdict, và in khối
391
+ `[GAP VERIFY]` + cam kết cuối vào report.
392
+
393
+ > **Vì sao bắt buộc, không phải tuỳ chọn.** Bước phân tích ở trên chỉ có lực **tìm thêm** —
394
+ > bốn skill lần lượt quét spec và mỗi cái đều được khuyến khích ghi ra chỗ nghi ngờ. Không có
395
+ > bước nào hỏi ngược *"cái vừa ghi có thật không?"*. Hệ quả đo được ở đội QC: phần lớn gap sinh
396
+ > ra là gap ảo — spec đã trả lời ở tài liệu khác, hoặc trích dẫn sai, hoặc là chuyện QC tự quyết
397
+ > được. Mà gap ảo không chỉ tốn thời gian PO: nó **làm PO mất tin vào cả danh sách**, và lúc đó
398
+ > những gap thật cũng chết theo. `gap-verify` là bộ lọc duy nhất đứng giữa hai chuyện đó.
399
+
243
400
  ## Output
244
401
 
245
- Ghi **đúng HAI file** dưới `{paths.qc_dir}/{UC-ID}/{active_platform}/` — **đừng** tách phân tích
402
+ Ghi **hai file** dưới `{paths.qc_dir}/{UC-ID}/{active_platform}/` + **một** dưới `{paths.refinement_dir}/` — **đừng** tách phân tích
246
403
  thành một-file-mỗi-bước (không có file spec-breakdown / business-rules / data-flow / AC riêng):
247
404
 
248
405
  1. **`{paths.qc_dir}/{UC-ID}/{active_platform}/REQUIREMENT_ANALYSIS.md`** — bản phân tích hợp nhất duy nhất.
249
406
  Section theo thứ tự: phân rã requirement → bảng business-rule (`BR-xx`) → data-flow →
250
407
  acceptance-criteria (`AC-xx`), mỗi `BR`/`AC` map tới `{UC-ID}-SC{N}` (của `.feature` platform này) sở hữu nó.
251
- 2. **`{paths.qc_dir}/{UC-ID}/{active_platform}/DOC_GAPS.md`**file gaps (theo `{paths.qc_skills_dir}/qa-analyst/DOC_GAPS.template.md`).
408
+ Cuối file thêm mục **"Đang chờ chốt (từ §12 tech-doc)"**các ẩn số thiết kế `open` chạm UC này
409
+ (`{id}` · điều chưa biết · owner · severity). Rỗng thì ghi "Không có"; **đừng bỏ mục**.
410
+ 2. **`{paths.qc_dir}/{UC-ID}/{active_platform}/DOC_GAP.md`** — file gap, theo
411
+ `{paths.qc_skills_dir}/qa-analyst/DOC_GAP.template.md` + luật viết ở
412
+ `{paths.qc_skills_dir}/qa-analyst/spec-issue-reporter.md`. Bắt buộc:
413
+ - **Bảng 10 cột** đúng thứ tự, có cột **Giao cho đội** (Dev / PO / BA / Design / Kiến trúc / Dữ liệu).
414
+ - Ô câu hỏi đủ **bốn phần** tách bằng `<br/>`: **Bối cảnh → Vấn đề → Tại sao quan trọng → Gợi ý**.
415
+ *Ba phần đầu cho PO xếp ưu tiên; phần cuối cho PO trả lời nhanh mà không phải nghĩ lại từ đầu.*
416
+ - Section **"Tài liệu đầu vào đã đọc để phân tích"** đặt ngay sau metadata — liệt kê **đủ** mọi
417
+ file đã mở. Đây là căn cứ độ phủ: không có nó thì không ai phân biệt được *"đã đọc và không thấy"*
418
+ với *"chưa đọc"*.
419
+ - Mức nặng nhất dùng từ **`🔴 Blocker`** (không phải `Critical`) — `/qc-run-test` đọc đúng từ này
420
+ để đặt *"scenario đang chờ PO"* vào sổ trace.
421
+ 3. **`{paths.refinement_dir}/{UC-ID}-qa-findings.yaml`** — **cùng dữ liệu gap**, ở định dạng Review Board đọc được. Xem §Bản findings dưới đây.
252
422
 
253
423
  `{paths.qc_dir}` là folder top-level NHÌN THẤY trong QC repo (mặc định `docs/`, **không** phải
254
424
  `.agent/review/` ẩn) để team QC mở và xử lý output dễ dàng. Spec chính thức ở lại
255
425
  spec submodule của PO — đừng ghi phân tích vào đó.
256
426
 
427
+ ### Bản findings — một nguồn, hai mặt
428
+
429
+ File `.yaml` và `DOC_GAP.md` là **cùng một tập gap**, không phải hai tập. Sinh `DOC_GAP.md`
430
+ trước (nó là bản người đọc), rồi **render** sang `.yaml` — đừng phân tích lại lần hai.
431
+
432
+ Dùng **đúng schema của `/refine-prd`** để Review Board đọc được cả hai loại file:
433
+
434
+ ```yaml
435
+ prd_source: "{paths.specs_dir}/{domain}/{prd-slug}/{TICKET-ID}-{prd-slug}.md"
436
+ uc_id: "{UC-ID}"
437
+ platform: "{active_platform}"
438
+ generated_at: "{ISO datetime}"
439
+ generated_by: "qc-analyze"
440
+ status: "pending_review"
441
+
442
+ findings:
443
+ - id: "F001"
444
+ lens: "QA" # LUÔN là QA — file này chỉ có một lăng kính
445
+ severity: "critical" # critical | major | minor ← map từ 🔴/🟠/🟡⚪ của DOC_GAP
446
+ section: "{section PRD/BDD chứa vấn đề}"
447
+ uc_id: "{UC-ID}"
448
+ quote: "{trích nguyên văn ≤120 ký tự từ spec tại đúng chỗ}"
449
+ finding: "{gap là gì}"
450
+ suggestion: "{cần PO/BA làm rõ điều gì}"
451
+ resolution_edge_cases: [] # để [] — phân tích bậc-hai là việc của /refine-prd
452
+ auto_fixable: false # LUÔN false — xem cảnh báo dưới
453
+ status: "pending"
454
+ applied_via: ""
455
+ gap_ref: "GAP-xx" # trỏ ngược về hàng trong DOC_GAP.md
456
+
457
+ summary:
458
+ total_findings: {N}
459
+ by_severity: { critical: {N}, major: {N}, minor: {N} }
460
+ by_lens: { QA: {N} }
461
+ recommendation: "APPROVED_WITH_MINOR_CHANGES | NEEDS_REVISION | BLOCKED"
462
+ ```
463
+
464
+ > **`auto_fixable` LUÔN `false`, và KHÔNG có `--resume` cho file này.**
465
+ >
466
+ > Review Board có nút *"chấp nhận rồi tự sửa PRD"*. Với gap của `/refine-prd` thì đúng — nó chạy
467
+ > ở **thời điểm PRD**, sửa PRD lúc đó là sửa đúng chỗ đúng lúc.
468
+ >
469
+ > Gap của lệnh này phát hiện **sau khi code đã xong**. Tự sửa PRD ở thời điểm đó là **sửa sau lưng
470
+ > cả dây chuyền**: BDD sinh từ PRD cũ, code sinh từ BDD đó, sổ kết quả kiểm thử neo vào scenario
471
+ > của BDD đó. Đổi PRD mà không đi lại đường ấy thì mọi thứ phía sau nói dối.
472
+ >
473
+ > Đường đúng vẫn là kênh đã có: `/report-bug` cho defect spec thật, `/propose-scenario` cho thiếu
474
+ > độ phủ. File `.yaml` này để **PO đọc và quyết trong một chỗ quen**, không phải để máy tự áp.
475
+
476
+ **File riêng, không ghi chung với `/refine-prd`.** Tên có `{UC-ID}` nên không đụng
477
+ `{prd-slug}-findings.yaml`. Ghi chung sẽ phá trường `applied_to_version` mà `/refine-prd` dùng để
478
+ phân biệt *"PRD đổi do chính tôi áp fix"* với *"có người lạ sửa"* — và nó sẽ mãi mãi tưởng có
479
+ người sửa sau lưng, mỗi lần chạy đều quét lại toàn bộ kèm cảnh báo giả.
480
+
481
+ ---
482
+
257
483
  ## Report
258
484
 
259
485
  **Đọc `.agent/steps/report-footer.md`** và áp đúng khuôn footer trong đó (Status Badge ·
260
486
  Output Artifacts · Next) cho report cuối, kèm khối bên dưới.
261
487
 
262
488
  ```
263
- /qc-analyze Hoàn tất — {UC-ID}
264
- Files: {paths.qc_dir}/{UC-ID}/{active_platform}/REQUIREMENT_ANALYSIS.md + DOC_GAPS.md (2 files)
265
- Gaps: {N} ({blockers} blocker) ← blocker là spec-defect? → /report-bug {UC-ID} | coverage gap /propose-scenario {UC-ID}
266
- SC mapping: {M} BR/AC map tới {K} scenario
267
- Next: /qc-plan {UC-ID} ← risk / what-if / questions-for-dev
268
- (giải quyết các gap 🔴 Blocker với PO/Dev trước)
489
+ /qc-analyze Hoàn tất — {UC-ID} ({active_platform})
490
+ Files : {paths.qc_dir}/{UC-ID}/{active_platform}/REQUIREMENT_ANALYSIS.md + DOC_GAP.md (10 cột)
491
+ {paths.refinement_dir}/{UC-ID}-qa-findings.yaml ← mở bằng Review Board (chuột phải)
492
+ Nguồn : PRD · BDD({active_platform}) · design-spec · tech-doc{nếu thiếu tech-doc: " (THIẾU — không đối chiếu được nghiệp vụ ↔ kỹ thuật)"}
493
+ Quét : 4 kỹ năng phân tích + 4 lăng kính (xử lý lỗi · giao diện · dữ liệu & cấu hình
494
+ · đối chiếu chéo: PRD↔design-spec, bdd↔design-spec)
495
+ Verify: {raw} gap thô → {N} còn Open (❌ {invalid} bịa/đã-trả-lời · ⚠️ {reclass} không phải gap nghiệp vụ · 🔁 {merge} trùng)
496
+ Gaps : {N} ({blockers} blocker) ← blocker là spec-defect? → /report-bug {UC-ID} | coverage gap → /propose-scenario {UC-ID}
497
+ Chờ chốt: {G} ẩn số §12 tech-doc đang open chạm UC này (đã ghi vào REQUIREMENT_ANALYSIS, KHÔNG hỏi lại PO)
498
+ SC map: {M} BR/AC map tới {K} scenario
499
+ Next : /qc-plan {UC-ID} {active_platform} ← rủi ro / what-if / câu hỏi cho dev
500
+ (giải quyết các gap 🔴 Blocker với PO/Dev trước)
269
501
  ```
502
+
503
+ > **Dòng `Next` là bắt buộc in, không phải trang trí.** Dây gốc của đội QC ra **cả** file gap
504
+ > **và** kế hoạch test trong một lần chạy (13/14 lần đo được ở repo của họ). Ở framework đó là
505
+ > **hai lệnh**. Người quen dây cũ sẽ dừng lại ở đây và tưởng đã xong — dòng này là chỗ duy nhất
506
+ > nói cho họ biết còn một bước nữa.
507
+
@@ -162,7 +162,7 @@ Mỗi dòng ⚠️/🔴 phải ứng với một trạng thái **context-loader
162
162
  🔴/⚠️ (không chặn ≠ không báo — người đọc log sau này vẫn cần thấy).
163
163
 
164
164
 
165
- *Lưu ý: Với lệnh này, target ở Bước 1 là một UC-ID. **Phân giải `active_platform`** (QC pass khoá 1 platform): nếu `$ARGUMENTS` nêu platform → dùng; else glob `{paths.qc_dir}/{UC-ID}/*/` — đúng 1 folder platform → dùng nó, nhiều folder → hỏi. Đọc output của qc-analyze + qc-plan (`REQUIREMENT_ANALYSIS.md`, `DOC_GAPS.md`, `TEST_PLAN.md`) từ `{paths.qc_dir}/{UC-ID}/{active_platform}/` và file `.feature` của đúng platform đó (với `@trace.scenario` mỗi scenario). Với layer GUI, cũng đọc bảng **Test Selectors** §4.5.6 (block platform) của tech-doc gộp tại `{paths.tech_docs_dir}/{domain}/{prd-slug}/tech-docs/{TICKET-ID}-tech-design.md` (nếu có — bảng gộp mọi UC của platform, **lọc theo cột "Serves SC" khớp SC của UC này**) — các test-id ổn định mà QC sẽ định vị theo.*
165
+ *Lưu ý: Với lệnh này, target ở Bước 1 là một UC-ID. **Phân giải `active_platform`** (QC pass khoá 1 platform): nếu `$ARGUMENTS` nêu platform → dùng; else glob `{paths.qc_dir}/{UC-ID}/*/` — đúng 1 folder platform → dùng nó, nhiều folder → hỏi. Đọc output của qc-analyze + qc-plan (`REQUIREMENT_ANALYSIS.md`, `DOC_GAP.md`, `TEST_PLAN.md`) từ `{paths.qc_dir}/{UC-ID}/{active_platform}/` và file `.feature` của đúng platform đó (với `@trace.scenario` mỗi scenario). Với layer GUI, cũng đọc bảng **Test Selectors** §4.5.6 (block platform) của tech-doc gộp tại `{paths.tech_docs_dir}/{domain}/{prd-slug}/tech-docs/{TICKET-ID}-tech-design.md` (nếu có — bảng gộp mọi UC của platform, **lọc theo cột "Serves SC" khớp SC của UC này**) — các test-id ổn định mà QC sẽ định vị theo.*
166
166
 
167
167
  ## Context
168
168
  **BẮT BUỘC — đọc `.agent/steps/context-loader.md` và thực thi TOÀN BỘ quy trình trong đó**,
@@ -162,7 +162,7 @@ Mỗi dòng ⚠️/🔴 phải ứng với một trạng thái **context-loader
162
162
  🔴/⚠️ (không chặn ≠ không báo — người đọc log sau này vẫn cần thấy).
163
163
 
164
164
 
165
- *Lưu ý: Với lệnh này, target ở Bước 1 là một UC-ID. **Phân giải `active_platform`** (QC pass khoá 1 platform — artifact dưới `{paths.qc_dir}/{UC-ID}/{active_platform}/`): nếu `$ARGUMENTS` nêu platform (web/app/system) → dùng; else glob `{paths.qc_dir}/{UC-ID}/*/` — đúng 1 folder platform → dùng nó, nhiều folder → hỏi platform nào. Đọc output của qc-analyze (`REQUIREMENT_ANALYSIS.md` + `DOC_GAPS.md`) từ `{paths.qc_dir}/{UC-ID}/{active_platform}/` và file `.feature` của đúng platform đó.*
165
+ *Lưu ý: Với lệnh này, target ở Bước 1 là một UC-ID. **Phân giải `active_platform`** (QC pass khoá 1 platform — artifact dưới `{paths.qc_dir}/{UC-ID}/{active_platform}/`): nếu `$ARGUMENTS` nêu platform (web/app/system) → dùng; else glob `{paths.qc_dir}/{UC-ID}/*/` — đúng 1 folder platform → dùng nó, nhiều folder → hỏi platform nào. Đọc output của qc-analyze (`REQUIREMENT_ANALYSIS.md` + `DOC_GAP.md`) từ `{paths.qc_dir}/{UC-ID}/{active_platform}/` và file `.feature` của đúng platform đó.*
166
166
 
167
167
  ## Context
168
168
  **BẮT BUỘC — đọc `.agent/steps/context-loader.md` và thực thi TOÀN BỘ quy trình trong đó**,
@@ -183,9 +183,12 @@ thiết kế test case cụ thể (đó là qc-design-test).
183
183
 
184
184
  ## Skills (`{paths.qc_skills_dir}/qa-planner/`)
185
185
 
186
- - `test-plan.md` — tự đủ: risk matrix, what-if, scope theo từng test layer
187
- (functional / integration / e2e / non-functional), entry/exit criteria, và
188
- danh sách questions-for-dev suy ra từ `DOC_GAPS.md`.
186
+ - `test-plan.md` — khung plan: scope theo từng test layer (functional / integration /
187
+ e2e / non-functional), what-if, entry/exit criteria, và danh sách questions-for-dev
188
+ suy ra từ `DOC_GAP.md`.
189
+ - `risk-model.md` — **cách tính** mức rủi ro: 7 nguồn rủi ro · khả năng × thiệt hại → P0–P3 ·
190
+ và dùng mức đó chia **độ sâu** test. `test-plan.md` có khung bảng `§6`; file này là cách điền.
191
+ Nạp cùng lúc, không phải thay thế.
189
192
 
190
193
  ## Output
191
194
 
@@ -225,7 +225,7 @@ Sau khi chạy, cập nhật **sổ của platform đang test** `{paths.trace_di
225
225
  | `qc_status` | Đọc cột `status` của row **TRƯỚC** — xem §Guard ngay dưới bảng. Row `OK`/`GAP`/`UNTRACKED`: `pass` nếu mọi QC test của SC này pass · `fail` nếu có cái fail · `skip` nếu tất cả skip/xfail · `not_run` nếu không QC test nào phủ nó. Row **`DRIFT`/`ORPHANED`**: **không bao giờ ghi `pass`** — hạ về `not_run` |
226
226
  | `qc_run_at` | hôm nay `YYYY-MM-DD` |
227
227
  | `last_updated` | hôm nay `YYYY-MM-DD` |
228
- | `qc_owner` | **SC đang chờ ai** (view "pending" của PM/PO): `dev` nếu FAIL = product-gap (defect thật → dev fix) · `po` nếu `skip`/`not_run` vì một **`DOC_GAPS` 🔴 Blocker đang open** chặn test (PO phải làm rõ PRD/BDD) · `—` nếu `pass`, hoặc FAIL = script-bug (QC tự fix — tạm thời) |
228
+ | `qc_owner` | **SC đang chờ ai** (view "pending" của PM/PO): `dev` nếu FAIL = product-gap (defect thật → dev fix) · `po` nếu `skip`/`not_run` vì một **`DOC_GAP` 🔴 Blocker đang open** chặn test (PO phải làm rõ PRD/BDD) · `—` nếu `pass`, hoặc FAIL = script-bug (QC tự fix — tạm thời) |
229
229
  | `qc_blocked_by` | artifact liên kết: `GAP-{id}` khi bị chặn bởi spec gap (set ở đây) · `BUG-{id}` khi `/report-bug` đã được file cho product-gap (backfill bởi `/report-bug`) · `—` ngược lại |
230
230
 
231
231
  Set `qc_owner`/`qc_blocked_by` cùng với `qc_status`. Khi `pass`, **clear** cả hai về `—` — nhưng **PHẢI chạy §Đóng bug đã verify bên dưới TRƯỚC**, vì `qc_blocked_by` chính là con trỏ tới bug và clear xong là mất đường về.