@educa-corp/sdd-framework 0.6.0 → 0.7.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.
Files changed (205) hide show
  1. package/bin/gate-trace.js +25 -2
  2. package/bin/index.js +32 -5
  3. package/bin/lint-trace.js +41 -0
  4. package/bin/self-check.js +430 -3
  5. package/bin/trace-schema.json +391 -30
  6. package/core/FRAMEWORK_VERSION +1 -1
  7. package/{commands/extend-prd.md → core/commands/amend-prd.md} +205 -173
  8. package/core/commands/dev-run-test.md +47 -9
  9. package/core/commands/extend-prd.md +39 -12
  10. package/core/commands/generate-bdd.md +43 -4
  11. package/core/commands/generate-code.md +33 -0
  12. package/core/commands/generate-tech-docs.md +34 -2
  13. package/core/commands/qc-run-test.md +29 -3
  14. package/core/commands/refine-prd.md +13 -2
  15. package/core/commands/review-context.md +43 -8
  16. package/core/commands/sync.md +105 -1
  17. package/core/commands/validate-traces.md +284 -11
  18. package/core/rules/workflow.md +34 -0
  19. package/core/steps/context-loader.md +26 -5
  20. package/core/templates/feature.template +1 -1
  21. package/docs/02-concepts/architecture.md +36 -0
  22. package/docs/04-reference/commands.md +148 -134
  23. package/docs/04-reference/trace-schema.md +39 -0
  24. package/docs/explain/02b-extend-prd.md +1 -1
  25. package/docs/explain/02c-amend-prd.md +152 -0
  26. package/docs/explain/28-sync.md +25 -0
  27. package/docs/explain/README.md +136 -135
  28. package/package.json +1 -8
  29. package/commands/debug.md +0 -529
  30. package/commands/debug.tmpl +0 -260
  31. package/commands/define-product.md +0 -438
  32. package/commands/define-product.tmpl +0 -225
  33. package/commands/dev-gen-test.md +0 -700
  34. package/commands/dev-gen-test.tmpl +0 -490
  35. package/commands/dev-run-test.md +0 -435
  36. package/commands/dev-run-test.tmpl +0 -225
  37. package/commands/dev-smoke-test.md +0 -374
  38. package/commands/dev-smoke-test.tmpl +0 -217
  39. package/commands/extend-prd.tmpl +0 -273
  40. package/commands/fix-bug.md +0 -519
  41. package/commands/fix-bug.tmpl +0 -197
  42. package/commands/generate-architecture.md +0 -354
  43. package/commands/generate-architecture.tmpl +0 -197
  44. package/commands/generate-bdd.md +0 -923
  45. package/commands/generate-bdd.tmpl +0 -590
  46. package/commands/generate-code.md +0 -859
  47. package/commands/generate-code.tmpl +0 -649
  48. package/commands/generate-design-spec.md +0 -737
  49. package/commands/generate-design-spec.tmpl +0 -524
  50. package/commands/generate-prd.md +0 -722
  51. package/commands/generate-prd.tmpl +0 -226
  52. package/commands/generate-spec-manifest.md +0 -321
  53. package/commands/generate-spec-manifest.tmpl +0 -164
  54. package/commands/generate-tech-docs.md +0 -920
  55. package/commands/generate-tech-docs.tmpl +0 -273
  56. package/commands/learn.md +0 -399
  57. package/commands/learn.tmpl +0 -130
  58. package/commands/map-testids.md +0 -238
  59. package/commands/map-testids.tmpl +0 -81
  60. package/commands/propose-scenario.md +0 -359
  61. package/commands/propose-scenario.tmpl +0 -202
  62. package/commands/qc-analyze.md +0 -269
  63. package/commands/qc-analyze.tmpl +0 -112
  64. package/commands/qc-design-test.md +0 -226
  65. package/commands/qc-design-test.tmpl +0 -69
  66. package/commands/qc-plan.md +0 -206
  67. package/commands/qc-plan.tmpl +0 -49
  68. package/commands/qc-report.md +0 -217
  69. package/commands/qc-report.tmpl +0 -60
  70. package/commands/qc-review.md +0 -210
  71. package/commands/qc-review.tmpl +0 -53
  72. package/commands/qc-run-test.md +0 -326
  73. package/commands/qc-run-test.tmpl +0 -116
  74. package/commands/refine-prd.md +0 -653
  75. package/commands/refine-prd.tmpl +0 -281
  76. package/commands/report-bug.md +0 -305
  77. package/commands/report-bug.tmpl +0 -148
  78. package/commands/review-code.md +0 -415
  79. package/commands/review-code.tmpl +0 -146
  80. package/commands/review-context.md +0 -902
  81. package/commands/review-context.tmpl +0 -530
  82. package/commands/review-tech-docs.md +0 -561
  83. package/commands/review-tech-docs.tmpl +0 -404
  84. package/commands/setup-ai-first.md +0 -602
  85. package/commands/setup-ai-first.tmpl +0 -450
  86. package/commands/sync.md +0 -430
  87. package/commands/sync.tmpl +0 -429
  88. package/commands/update-framework.md +0 -203
  89. package/commands/update-framework.tmpl +0 -202
  90. package/commands/validate-traces.md +0 -1077
  91. package/commands/validate-traces.tmpl +0 -920
  92. package/hooks/data-guard.js +0 -232
  93. package/hooks/settings.json +0 -19
  94. package/modules/android-compose/module.yaml +0 -13
  95. package/modules/android-compose/stack-profile.yaml +0 -57
  96. package/modules/angular/architecture-snippets/component-patterns.md +0 -187
  97. package/modules/angular/module.yaml +0 -6
  98. package/modules/angular/stack-profile.yaml +0 -38
  99. package/modules/context-engineering/architecture-snippets/context-design.md +0 -119
  100. package/modules/context-engineering/module.yaml +0 -9
  101. package/modules/context-engineering/stack-profile.yaml +0 -61
  102. package/modules/dotnet/architecture-snippets/clean-arch.md +0 -160
  103. package/modules/dotnet/module.yaml +0 -6
  104. package/modules/dotnet/stack-profile.yaml +0 -50
  105. package/modules/flutter/module.yaml +0 -14
  106. package/modules/flutter/stack-profile.yaml +0 -59
  107. package/modules/golang/architecture-snippets/domain-layout.md +0 -283
  108. package/modules/golang/module.yaml +0 -6
  109. package/modules/golang/stack-profile.yaml +0 -40
  110. package/modules/ios-swiftui/module.yaml +0 -13
  111. package/modules/ios-swiftui/stack-profile.yaml +0 -55
  112. package/modules/java-spring/architecture-snippets/layered-arch.md +0 -201
  113. package/modules/java-spring/module.yaml +0 -15
  114. package/modules/java-spring/stack-profile.yaml +0 -28
  115. package/modules/nextjs/architecture-snippets/app-router-patterns.md +0 -269
  116. package/modules/nextjs/module.yaml +0 -14
  117. package/modules/nextjs/stack-profile.yaml +0 -74
  118. package/modules/nuxt/module.yaml +0 -14
  119. package/modules/nuxt/stack-profile.yaml +0 -58
  120. package/modules/phaser-game/architecture-snippets/phaser-scene-patterns.md +0 -646
  121. package/modules/phaser-game/module.yaml +0 -15
  122. package/modules/phaser-game/stack-profile.yaml +0 -90
  123. package/modules/php-laravel/architecture-snippets/service-repository.md +0 -302
  124. package/modules/php-laravel/module.yaml +0 -15
  125. package/modules/php-laravel/stack-profile.yaml +0 -56
  126. package/modules/qc-playwright/stack-profile.yaml +0 -66
  127. package/modules/react/architecture-snippets/hooks-query-patterns.md +0 -254
  128. package/modules/react/module.yaml +0 -14
  129. package/modules/react/stack-profile.yaml +0 -63
  130. package/modules/react-native/module.yaml +0 -14
  131. package/modules/react-native/stack-profile.yaml +0 -56
  132. package/modules/vue/module.yaml +0 -14
  133. package/modules/vue/stack-profile.yaml +0 -65
  134. package/rules/data-protection.md +0 -80
  135. package/rules/workflow.md +0 -99
  136. package/skills/code/SKILL.md +0 -19
  137. package/skills/code/SKILL.tmpl +0 -19
  138. package/skills/debug/SKILL.md +0 -19
  139. package/skills/debug/SKILL.tmpl +0 -19
  140. package/skills/design-spec/SKILL.md +0 -11
  141. package/skills/design-spec/SKILL.tmpl +0 -11
  142. package/skills/discovery/SKILL.md +0 -14
  143. package/skills/discovery/SKILL.tmpl +0 -14
  144. package/skills/prd/SKILL.md +0 -19
  145. package/skills/prd/SKILL.tmpl +0 -19
  146. package/skills/qc/qa-analyst/DOC_GAPS.template.md +0 -63
  147. package/skills/qc/qa-analyst/acceptance-criteria.md +0 -60
  148. package/skills/qc/qa-analyst/business-rules.md +0 -59
  149. package/skills/qc/qa-analyst/data-flow.md +0 -64
  150. package/skills/qc/qa-analyst/spec-breakdown.md +0 -61
  151. package/skills/qc/qa-designer/e2e/journey.md +0 -41
  152. package/skills/qc/qa-designer/exploratory/charter.md +0 -68
  153. package/skills/qc/qa-designer/exploratory/explore-to-functional.md +0 -43
  154. package/skills/qc/qa-designer/functional/api.md +0 -45
  155. package/skills/qc/qa-designer/functional/gui-feature.md +0 -46
  156. package/skills/qc/qa-designer/functional/gui-screen.md +0 -52
  157. package/skills/qc/qa-designer/integration/api.md +0 -42
  158. package/skills/qc/qa-designer/integration/db.md +0 -39
  159. package/skills/qc/qa-designer/integration/gui.md +0 -40
  160. package/skills/qc/qa-designer/integration/kafka.md +0 -40
  161. package/skills/qc/qa-designer/non-functional.md +0 -40
  162. package/skills/qc/qa-planner/test-plan.md +0 -120
  163. package/skills/qc/qa-reviewer/script/e2e.md +0 -87
  164. package/skills/qc/qa-reviewer/script/exploratory.md +0 -45
  165. package/skills/qc/qa-reviewer/script/functional.md +0 -101
  166. package/skills/qc/qa-reviewer/script/integration.md +0 -91
  167. package/skills/qc/qa-reviewer/script/non-functional.md +0 -126
  168. package/skills/qc/qa-reviewer/test-case/e2e.md +0 -73
  169. package/skills/qc/qa-reviewer/test-case/exploratory.md +0 -43
  170. package/skills/qc/qa-reviewer/test-case/functional.md +0 -76
  171. package/skills/qc/qa-reviewer/test-case/integration.md +0 -69
  172. package/skills/qc/qa-reviewer/test-case/non-functional.md +0 -73
  173. package/skills/qc/qa-runner/e2e.md +0 -49
  174. package/skills/qc/qa-runner/exploratory/session.md +0 -36
  175. package/skills/qc/qa-runner/functional/api.md +0 -35
  176. package/skills/qc/qa-runner/functional/gui-feature.md +0 -51
  177. package/skills/qc/qa-runner/functional/gui-screen.md +0 -55
  178. package/skills/qc/qa-runner/integration.md +0 -47
  179. package/skills/qc/qa-runner/non-functional.md +0 -49
  180. package/skills/qc/qa-runner/report/report.md +0 -37
  181. package/skills/setup-ai-first/SKILL.md +0 -19
  182. package/skills/setup-ai-first/SKILL.tmpl +0 -19
  183. package/skills/spec/SKILL.md +0 -19
  184. package/skills/spec/SKILL.tmpl +0 -19
  185. package/skills/test/SKILL.md +0 -18
  186. package/skills/test/SKILL.tmpl +0 -18
  187. package/steps/business-language.md +0 -56
  188. package/steps/capture-lesson.md +0 -112
  189. package/steps/context-loader.md +0 -406
  190. package/steps/gate.md +0 -151
  191. package/steps/report-footer.md +0 -125
  192. package/steps/review-fanout.md +0 -159
  193. package/steps/spawn-agent.md +0 -129
  194. package/steps/trace-mirror.md +0 -53
  195. package/templates/README.md +0 -70
  196. package/templates/architecture.template.md +0 -394
  197. package/templates/ci/trace-gate.yml +0 -146
  198. package/templates/design-spec.template.md +0 -217
  199. package/templates/feature.template +0 -123
  200. package/templates/hooks/pre-push +0 -61
  201. package/templates/platform-guide.template.md +0 -145
  202. package/templates/prd.template.md +0 -283
  203. package/templates/product-definition.template.md +0 -188
  204. package/templates/project-context.yaml +0 -212
  205. package/templates/tech-design.template.md +0 -490
@@ -159,7 +159,7 @@ Mỗi dòng ⚠️/🔴 phải ứng với một trạng thái **context-loader
159
159
  🔴/⚠️ (không chặn ≠ không báo — người đọc log sau này vẫn cần thấy).
160
160
 
161
161
 
162
- *Lưu ý: Với lệnh này, target Bước 1 một tên domain hoặc UC-ID cụ thể từ `$ARGUMENTS`. Không một file đơn để phân giải lệnh quét nhiều thư mục.*
162
+ *Lưu ý: Lệnh này **không có target file đơn** — nó quét nhiều thư mục, nên gate Bước 1 không phân giải file. Phạm vi audit do **Step 0-A** phân giải từ `$ARGUMENTS` (`--domain` / `--prd` / `--uc`; không cờ nào = toàn bộ).*
163
163
 
164
164
  ## Context
165
165
  **BẮT BUỘC — đọc `.agent/steps/context-loader.md` và thực thi TOÀN BỘ quy trình trong đó**,
@@ -173,6 +173,60 @@ placeholder bên dưới sẽ rỗng và lệnh sẽ đọc/ghi sai chỗ.
173
173
 
174
174
  ## Process
175
175
 
176
+ ### Step 0-A — Phân giải **phạm vi audit** *(chạy TRƯỚC Step 0)*
177
+
178
+ *Contract: `bin/trace-schema.json` → `gate.report_root_keys`. Đây là đầu PRODUCER của một sợi dây mà đầu CONSUMER (`gate-trace`) đã có sẵn từ trước.*
179
+
180
+ Parse `$ARGUMENTS` (đã tách các cờ khác ở gate Bước 1):
181
+
182
+ | Cờ | `scope.kind` | `scope.value` | Hẹp lại những gì |
183
+ |---|---|---|---|
184
+ | *(không có)* | `all` | `"all"` | Không hẹp — audit toàn bộ |
185
+ | `--domain {domain}` | `domain` | tên domain | Chỉ sổ + spec dưới `{domain}/` |
186
+ | `--prd {TICKET-ID}` | `prd` | TICKET-ID | Chỉ **một** feature-package (phân giải `{domain}/{prd-slug}` từ TICKET-ID) |
187
+ | `--uc {UC-ID}` | `uc` | UC-ID | Chỉ **một** UC — **mọi platform của nó** (`{UC-ID}-*.tsv`) |
188
+
189
+ Nhiều cờ scope cùng lúc → **DỪNG**, báo lỗi: chúng loại trừ nhau, và tự ý ưu tiên một cái là hẹp phạm vi mà người dùng không biết.
190
+
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
+
193
+ Lưu `scope` — mọi step sau dùng nó:
194
+
195
+ | Step | Hẹp thế nào |
196
+ |---|---|
197
+ | Step 0 / Step 1 | `all_trace_dirs` giữ nguyên, nhưng chỉ đọc TSV **khớp scope**: `{trace_dir}/{domain}/**` · `{trace_dir}/{domain}/{prd-slug}/**` · `{trace_dir}/**/{UC-ID}-*.tsv` |
198
+ | Step 1.0 (lint) | truyền `--trace` như cũ — **lint luôn chạy toàn bộ**. Sổ hỏng ở domain khác vẫn là sổ hỏng, và lint rẻ (không LLM) |
199
+ | Step 2b · 3.9 · 4 · 5* · 7 | chỉ các PRD/UC trong scope |
200
+ | Step 6 · 6b | chỉ ghi lại TSV + mốc của phần trong scope |
201
+ | Step 8 | ghi `scope` **và** `domain` vào biên bản (xem dưới) |
202
+
203
+ **In phạm vi ngay đầu run**, trước khi làm gì:
204
+ ```
205
+ Phạm vi: {all | domain={d} | prd={TICKET-ID} | uc={UC-ID}}
206
+ {n} PRD · {m} UC · {k} sổ → {ước lượng: toàn bộ repo | một phần}
207
+ ```
208
+ *Không có dòng này thì người dùng không biết mình vừa gọi một lệnh cỡ nào — và đây là lệnh đắt nhất trong 33 lệnh (~33k token chỉ dẫn trước khi mở artifact nào).*
209
+
210
+ > **Vì sao step này là "nối dây", không phải "thêm tính năng" (GAPS-v4 G57).**
211
+ > Trước nó, **năm** chỗ trong lệnh này mô tả một *"domain argument"* / *"domain filter"* **không tồn
212
+ > tại**: ghi chú Gate (*"target là một tên domain hoặc UC-ID cụ thể từ `$ARGUMENTS`"*) · Step 1 đọc TSV
213
+ > *"khớp domain target"* · schema biên bản `"<domain argument, or 'all' if no filter>"` · Step 8
214
+ > *"nếu có domain filter, chỉ gồm các PRD đó"* · dòng `trace-history` mang field `domain`.
215
+ > Và `gate-trace` **đã** đọc `report.domain` rồi **chặn PR** nếu nó khác `all`.
216
+ >
217
+ > Nhưng Step 0 đặt `all_trace_dirs` = toàn bộ **vô điều kiện**, và chỗ duy nhất parse `$ARGUMENTS` là
218
+ > Step 5e cho hai cờ `--realign`. Nên ô đó **luôn** ghi `all`, và phần kiểm của gate **chưa bao giờ
219
+ > chạy một lần nào**.
220
+ >
221
+ > Đây đúng hình dạng **R1 fail build vì nó** — *"field có consumer mà không có producer"*, ca
222
+ > `@trace.sc_version` (3 consumer, 0 producer, sống qua nhiều version không ai bắt được). Nó sống
223
+ > được vì R1 chỉ canh field trong **sổ TSV** và tag trong **code**, không canh key ở cấp gốc **biên
224
+ > bản JSON**. `self-check` **R9(h)** giờ canh cả hai đầu.
225
+ >
226
+ > Và ghi chú Gate còn **tệ hơn im lặng** — nó gây nhầm: một agent đọc *"target là một tên domain hoặc
227
+ > UC-ID"* sẽ **tin rằng** scoping hoạt động.
228
+
229
+ ---
176
230
  ### Step 0 — Umbrella Mode Detection
177
231
 
178
232
  Kiểm tra mảng `services` có tồn tại trong `project-context.yaml` không.
@@ -257,6 +311,29 @@ Nếu version SC trong `.feature` khác `spec_ver` của `.tsv` → cập nhật
257
311
 
258
312
  Cũng phát hiện SC có trong `.feature` (platform đó) nhưng thiếu trong `.tsv` → thêm row mới với `status: UNTRACKED`. *(sc_id trùng số giữa các platform là 2 scenario khác nhau → mỗi sổ platform giữ tập SC riêng, không dedupe chéo platform.)*
259
313
 
314
+ ### Step 2c — Phân giải lại `service` từ config hiện tại *(G51)*
315
+
316
+ *Cùng tinh thần Step 2 với `spec_ver`: cột TSV là **cache**, `services:` trong `project-context.yaml`
317
+ là **nguồn**. Mỗi lần chạy, đối chiếu lại.*
318
+
319
+ Với mỗi row có `service` ∈ (`unrouted`, `unresolved`), tra lại `services:` theo
320
+ `domain` + `platform` (từ tên file sổ) + `prd_slug`:
321
+
322
+ | Kết quả tra | Hành động |
323
+ |---|---|
324
+ | Giờ **khớp** một entry | **Cập nhật `service` = path đó** (in memory, ghi lại ở Step 6). Không cần chạy lại `/generate-bdd`. |
325
+ | Vẫn không khớp | Giữ `unrouted` → gắn cờ 🟠 `SERVICE_UNROUTED` |
326
+ | Config vẫn sai cấu trúc | Giữ `unresolved` → cùng cờ, nhưng lý do khác (bug config, không phải chờ quyết) |
327
+
328
+ > **Vì sao lệnh này được ghi một cột do `/generate-bdd` sở hữu:** đây là **ngoại lệ có chủ ý** với luật
329
+ > *"mỗi cột một chủ"* (`rules/workflow.md` §Trace Contract), và nó **không vi phạm tinh thần** của luật:
330
+ > lệnh này **không ghi một giá trị mới** — nó chỉ **phân giải một placeholder mà `/generate-bdd` đã cố ý
331
+ > để lại**. Khai tường minh trong `bin/trace-schema.json`: `service.written_by = [generate-bdd, validate-traces]`.
332
+ >
333
+ > Đây là thứ làm sổ **tự lành**: architect thêm mapping → lần `/validate-traces` kế tiếp nâng
334
+ > `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
+ > vĩnh viễn** — đúng bệnh `TBD` mà G1/G28 đã chỉ ra.
336
+
260
337
  ### Step 2b — Reverse audit (bắt tag mồ côi)
261
338
 
262
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.*
@@ -291,6 +368,84 @@ Không tìm thấy tag mồ côi nào → bỏ qua im lặng.
291
368
 
292
369
  > **Vì sao DRIFT xét trước GAP:** một scenario đã có code, chưa test, **và** spec vừa drift phải hiện `DRIFT` (không phải `GAP`) — vì `generate-code` xử `GAP` = "skip codegen, chạy /dev-gen-test" còn `DRIFT` = "regenerate". Nếu GAP thắng, code lỗi thời bị bỏ qua và test lại sinh trên code cũ. UNTRACKED vẫn phải là Rule 1 để scenario chưa code (gen_ver `—`) không lọt vào DRIFT.
293
370
 
371
+ ### Step 3.9 — Phát hiện sửa spec ngoài đường chính thức *(chạy TRƯỚC Step 4)*
372
+
373
+ *Contract: `bin/trace-schema.json` → `spec_edit_detection`. Cờ: `PRD_UNTRACKED_EDIT`.*
374
+
375
+ **Vì sao bước này đứng TRƯỚC Step 4.** Step 4 và mọi bước sau nó đều dựa trên một giả định
376
+ **chưa được kiểm**: *nhãn `Version` của PRD phản ánh đúng nội dung hiện tại của nó.* Nếu ai sửa nội
377
+ dung mà không bump nhãn thì giả định đó sai, và Step 4 sẽ phán *"version khớp ⇒ sạch"* trên một tài
378
+ liệu đã đổi. Kiểm cấu trúc drift trên một nhãn không còn đúng thì cũng vô nghĩa như G1 kiểm cấu trúc
379
+ một quyển sổ sắp mất — nên hỏi trước.
380
+
381
+ **Điểm mù mà nó bịt (GAPS-v4 G54).** Toàn bộ lưới an toàn của framework so **nhãn version**, không so
382
+ **nội dung** — không có một content hash nào ở đâu. Nên một PRD bị sửa tay là điểm mù **tuyệt đối**:
383
+ Step 4 thấy `PRD Version == prd_version` ⇒ sạch · `gate-trace` thấy report khớp sổ ⇒ PASS ·
384
+ `require-fresh-audit` thấy PR không chạm tag ⇒ không đòi audit. **Cả ba tầng xanh, và cả ba đúng theo
385
+ định nghĩa của chính chúng.**
386
+
387
+ #### Đọc mốc của lần audit trước
388
+
389
+ Đọc khối **`spec_baseline`** trong `trace-report.json` của lần chạy trước (path:
390
+ `{living_docs_dir}/trace-report.json`). Mỗi entry: `prd_path` · `sha_at_audit` · `version_at_audit`.
391
+
392
+ - Khối **vắng** (lần audit đầu, hoặc report sinh bởi version cũ hơn) → **bỏ qua so sánh**, chỉ **ghi
393
+ mốc mới** ở Step 6b. In một dòng: `ⓘ spec_baseline: lần đầu ghi mốc — check sửa-ngoài-đường bắt đầu có hiệu lực từ lần chạy sau.`
394
+
395
+ #### So — hai nguồn bằng chứng, cần cả hai
396
+
397
+ Với mỗi file PRD trong phạm vi, ở repo chứa `{paths.specs_dir}`:
398
+
399
+ | Nguồn | Lệnh | Bắt ca nào |
400
+ |---|---|---|
401
+ | **git diff** | `git -C {specs repo} diff --name-only {sha_at_audit}..HEAD -- {prd_path}` | sửa **đã commit** |
402
+ | **git status** | `git -C {specs repo} status --porcelain -- {prd_path}` | sửa **CHƯA commit** — ca thường gặp nhất, vì PO đang gõ |
403
+
404
+ Thiếu nguồn thứ hai là mù với cả một lớp ca: PO sửa xong, chưa commit, chạy audit — và audit nói sạch.
405
+
406
+ #### Phán
407
+
408
+ | Nội dung đổi? | `Version` hiện tại vs `version_at_audit` | Kết luận |
409
+ |:---:|---|---|
410
+ | **có** | **BẰNG NHAU** | 🔴 **`PRD_UNTRACKED_EDIT`** — có người sửa ngoài `/generate-prd` · `/extend-prd` · `/amend-prd` · `/refine-prd` · `/review-context` |
411
+ | có | khác | ✅ không cờ — đã đi đường chính thức. Step 4 tiếp quản bình thường |
412
+ | không | bất kỳ | ✅ không cờ |
413
+
414
+ **Không có báo oan:** ca duy nhất bật cờ là *"đổi nội dung, giữ nguyên nhãn"*. Sửa **và** bump
415
+ version thì `version != version_at_audit` ⇒ im.
416
+
417
+ #### Ca không kiểm được → nói rõ là đang mù
418
+
419
+ Không phải git repo · `sha_at_audit` không còn (history bị rewrite) · `git` không khả dụng →
420
+ **bỏ qua** check này và in:
421
+ ```
422
+ ⚠️ Chưa kiểm được sửa-ngoài-đường cho {n} PRD ({lý do}) — điểm mù G54 đang MỞ ở các file này.
423
+ ```
424
+ **KHÔNG** bịa cờ, và **KHÔNG** im lặng. Im lặng là lựa chọn rẻ nhất và tệ nhất trong ba.
425
+
426
+ #### Đường ra — tự lành, cố ý KHÔNG có lệnh escape
427
+
428
+ Cách sửa đúng là **bump version + ghi một row changelog nêu UC bị ảnh hưởng** — tức đúng việc
429
+ `/amend-prd` làm hộ. Làm xong thì `version != version_at_audit` ⇒ cờ tự tắt, và logic `PRD_DRIFT`
430
+ bình thường tiếp quản (đúng, vì nội dung có đổi thật).
431
+
432
+ > **Vì sao KHÔNG có `--accept-edit`:** thêm nó là thêm một đường **dán nhãn lên thay đổi chưa ai
433
+ > xem** — đúng cái sai mà ba rào của `--realign` (Step 5e) tồn tại để chặn.
434
+
435
+ #### Vì sao cờ này KHÔNG nằm trong `gate.blocking`
436
+
437
+ Quyết định có chủ ý, ghi ở `spec_edit_detection.$comment`:
438
+
439
+ | | |
440
+ |---|---|
441
+ | `gate.blocking` nghĩa hẹp là **code đang hỏng** | Cờ này nói về **spec**; code có thể đang hoàn toàn đúng. R9(e) tồn tại để giữ đúng ranh giới đó |
442
+ | Nợ tồn khi mới bật | Mọi project đang chạy đều đã có PRD sửa tay ⇒ cờ chặn mới sẽ đỏ khắp nơi ở lần đầu ⇒ **người ta tắt cổng** ⇒ mất luôn 4 cờ 🔴 thật. Đó đúng là thất bại R9(e) được viết ra để chặn, chỉ đến bằng một cửa khác |
443
+ | Nhưng nó vẫn 🔴 | In mỗi lần chạy, có khối riêng trong report + counter trong `summary` — đủ để thấy, không đủ để làm tắt cổng |
444
+
445
+ Team nào đã dọn sạch nợ tồn thì tự thêm `prd_untracked_edit_count` vào `gate.blocking` (kèm `why`) —
446
+ `self-check` R13(e) canh việc đó.
447
+
448
+ ---
294
449
  ### Step 4 — PRD version drift check
295
450
 
296
451
  Với mỗi UC, so:
@@ -304,13 +459,43 @@ Nếu layer nào sau version PRD hiện tại → trích các changelog entry k
304
459
 
305
460
  *PRD là tài liệu **cấp feature** phủ nhiều UC, nhưng version của nó là **một scalar**. Nên thêm UC7 — không đụng một chữ nào của UC1–UC6 — vẫn làm cả 6 UC cũ lệch version. So version thuần thì cả 6 ăn cờ đỏ oan.*
306
461
 
307
- Đọc các row `# Change Log` của PRD **từ version của layer cũ nhất tới hiện tại**, trích tập UC/AC/BR được nêu. *(`/refine-prd` Phase 3 `/extend-prd` **bắt buộc** ghi thông tin này vào mỗi row — chính vì mục đích này. `/generate-bdd` Version Check đã dùng nó từ trước; ở đây chỉ là đọc cùng một dữ liệu.)*
462
+ Đọc các row `# Change Log` của PRD **từ version của layer cũ nhất tới hiện tại**. Mỗi row mang một `{changelog_scope}` — contract máy đọc, khai ở `bin/trace-schema.json` → `changelog_row_contract`; producer là `/refine-prd` Phase 3, `/extend-prd` Bước 6, `/review-context` Fix/Resume Phase 3, và **bắt buộc** ghi thông tin này chính vì mục đích ở đây. *(`/generate-bdd` Version Check đọc cùng dữ liệu.)*
463
+
464
+ **Dựng `affected_ucs` theo ba bước — KHÔNG bỏ bước 2:**
465
+
466
+ **1. Tách mệnh đề.** Mỗi row `{changelog_scope}` gồm các mệnh đề ngăn bằng `;`, mỗi mệnh đề mở đầu bằng đơn vị sở hữu: `{UC-ID}: {mô tả}` hoặc `PRD-global: {mô tả}`.
467
+
468
+ **2. Chuẩn hoá về UC — phép phân giải `BR/AC → UC sở hữu`.** Trong mỗi mệnh đề, ngoài UC-ID mở đầu còn có thể có BR/AC được nêu. Đưa **mọi** BR/AC về UC sở hữu **trước khi** so:
469
+
470
+ | Gặp | Phân giải thành |
471
+ |---|---|
472
+ | `BR{n}` | UC có `BR{n}` trong **bảng Business Rule** của nó (PRD §3) |
473
+ | `AC{n}` | UC có `AC{n}` ở dòng **`**AC liên quan:**`** của nó (PRD §3) |
474
+ | `PRD-global` | **không** thuộc UC nào — không thêm gì vào `affected_ucs` |
475
+ | BR/AC **không** phân giải được về UC nào *(ID đã bị xoá, hoặc PRD lệch cấu trúc)* | coi **cả row** là **mơ hồ** → hàng 3 dưới đây. **KHÔNG** bỏ qua im lặng mệnh đề đó |
476
+
477
+ *Không phát sinh I/O: Step 4 đã mở file PRD này ở đầu bước.*
478
+
479
+ **3. Phân loại từng UC** *(first-match-wins)*:
308
480
 
309
481
  | Điều kiện | Cờ | Hành động |
310
482
  |---|---|---|
311
- | UC này **có** trong tập bị ảnh hưởng | `PRD_DRIFT` 🟠 | Cần regen thật theo bảng `drifted_layers` bên dưới |
312
- | UC này **không** trong tập, **và** mọi row changelog trong khoảng đều nêu rõ scope | `PRD_STALE_REF` | Nội dung không đổi, chỉ con trỏ version cũ. **KHÔNG** route regen — dùng `--realign-prd-version` |
313
- | **Bất kỳ** row nào trong khoảng hồ (không nêu UC/AC/BR) | `PRD_DRIFT` 🟠 cho **MỌI** UC | Không suy đoán được thì quét rộng |
483
+ | **Bất kỳ** row nào trong khoảng **mơ hồ** (không mệnh đề nào nêu được đơn vị sở hữu, hoặc bước 2 không phân giải được) | `PRD_DRIFT` 🟠 cho **MỌI** UC | Không suy đoán được thì quét rộng |
484
+ | UC này **có** trong `affected_ucs` | `PRD_DRIFT` 🟠 | Cần regen thật theo bảng `drifted_layers` bên dưới |
485
+ | UC này trong `affected_ucs` **CHỈ** qua (các) mệnh đề mang hậu tố **`[no-behavior]`** | `PRD_STALE_REF` | Producer đã **chứng minh** thay đổi không đổi hành vi (chỉ thêm/bỏ vỏ cấu trúc). Xem `changelog_row_contract.neutral_checks` |
486
+ | UC này **không** có trong `affected_ucs`, **và** mọi row trong khoảng đều nêu rõ scope | `PRD_STALE_REF` ⓘ | Nội dung không đổi, chỉ con trỏ version cũ. **KHÔNG** route regen — dùng `--realign-prd-version` |
487
+
488
+ > **Vì sao bước 2 là bắt buộc (G53).** Bản cũ viết *"trích tập UC/AC/BR được nêu"* rồi so bằng phép thử *"**UC** này có trong tập?"*. Hai câu đó **không khớp nhau**: tập chứa lẫn UC, AC và BR, nhưng phép thử chỉ hỏi về UC. Nên một row như `thêm UC7: AC12-AC14; sửa BR8` cho tập `{UC7, AC12-14, BR8}` — và **UC3, chủ sở hữu BR8, không có trong đó**.
489
+ >
490
+ > Row đó nêu rất nhiều ID nên **không** rơi vào hàng "mơ hồ". Kết quả: UC3 → ⓘ *"không phải lỗi"*, trong khi BR mà nó sở hữu vừa đổi hành vi. Và ⓘ **mở cửa** cho `--realign-prd-version` (rào an toàn của nó chỉ **từ chối khi UC là 🟠**) ⇒ nhãn `prd_version` bị dán lại ở cả TSV lẫn tag code ⇒ **cờ sạch vĩnh viễn trên thay đổi chưa ai implement**.
491
+ >
492
+ > Đây là G1 đúng nghĩa — cờ **im lặng** — chỉ khác là lần này có thêm một lệnh tự động đóng dấu lên nó.
493
+
494
+ > **Vì sao hàng "mơ hồ" lên ĐẦU bảng.** Nó là điều kiện **cấp row**, không phải cấp UC: một row mơ hồ làm mọi phán đoán per-UC trong khoảng đó vô giá trị. Xét nó sau các hàng kia thì một UC có thể được xếp ⓘ **trước khi** ta biết là không suy đoán được gì — và ⓘ là hạng mở cửa cho `--realign`.
495
+
496
+ > **Hàng "mơ hồ" là lưới an toàn — hỏng theo hướng an toàn.** Changelog viết ẩu thì ta mất tính năng *lọc*, KHÔNG mất tính năng *cảnh báo*. Đồng bộ với `generate-bdd` Version Check: *"changelog row không nêu rõ scope (mơ hồ) → khuyến nghị F (gen lại toàn bộ)"*.
497
+ >
498
+ > ⚠️ **Nhưng lưới an toàn KHÔNG phải cái cớ để producer ghi bừa.** Nó đúng khi **thiếu** thông tin; nó sai khi producer **có** thông tin mà không ghi. Đó là G52: `/review-context --fix` từng ghi cứng `Auto-fix: applied {N} findings` — 0 scope — trong khi findings YAML của nó có `uc_id` bắt buộc cho từng finding. Một sửa từ trong UC5 làm **cả 8 UC** ăn 🟠. `self-check` R12 giờ fail build nếu producer nào không nhắc `{changelog_scope}`.
314
499
 
315
500
  > **Hàng thứ ba là lưới an toàn — hỏng theo hướng an toàn.** Changelog viết ẩu thì ta mất tính năng *lọc*, KHÔNG mất tính năng *cảnh báo*. Đồng bộ với `generate-bdd` Version Check: *"changelog row không nêu rõ UC/AC/BR (mơ hồ) → khuyến nghị F (gen lại toàn bộ)"*.
316
501
  >
@@ -340,13 +525,18 @@ Skip cột nào chưa có revision đã lưu (`—`), hoặc cả UC chưa có t
340
525
 
341
526
  *Tech-doc có **đúng cùng hình dạng** với PRD: một doc **GỘP cấp PRD**, một `@trace.revision` chung cho nhiều UC. Chế độ **APPEND** (`generate-tech-docs` Bước 1) thêm UC mới vào doc đã có → revision bump → **mọi UC cũ lệch**, dù phần của chúng không đổi một dòng.*
342
527
 
343
- Đọc **§10 UC Coverage** và **Changelog** của tech-doc, trích tập UC row changelog trong khoảng revision đó nêu:
528
+ Đọc **§10 UC Coverage** và **Changelog** của tech-doc. Mỗi row Changelog mang một `{changelog_scope}` cùng contract với PRD (`changelog_row_contract`); producer là `/generate-tech-docs` Bước 1b, đơn vị "global" đây tên `doc-global` thay cho `PRD-global`.
529
+
530
+ Dựng `affected_ucs` **theo đúng ba bước của Step 4**, kể cả bước 2 (`BR/AC → UC sở hữu`) — tech-doc cũng nêu §/SC/BR trong mô tả, và một mệnh đề nêu `§5.9` mà bỏ UC thì UC đó rơi vào ⓘ y như ca BR8 ở Step 4. Rồi phân loại *(first-match-wins, cùng thứ tự)*:
344
531
 
345
532
  | Điều kiện | Cờ |
346
533
  |---|---|
347
- | UC này **có** trong tập bị ảnh hưởng | `TECHDOC_DRIFT` / `FE_TECHDOC_DRIFT` 🟠 |
348
- | UC này **không** có, changelog nêu rõ scope | `TECHDOC_STALE_REF` dùng `--realign-techdoc-revision` |
349
- | Row changelog hồ | 🟠 cho **mọi** UC (lưới an toàn) |
534
+ | **Bất kỳ** row nào trong khoảng **mơ hồ** (không nêu được đơn vị sở hữu, hoặc bước 2 không phân giải được) | 🟠 cho **MỌI** UC (lưới an toàn) |
535
+ | UC này **có** trong `affected_ucs` | `TECHDOC_DRIFT` / `FE_TECHDOC_DRIFT` 🟠 |
536
+ | UC này trong `affected_ucs` **CHỈ** qua mệnh đề mang hậu tố `[no-behavior]` | `TECHDOC_STALE_REF` ⓘ |
537
+ | UC này **không** có trong `affected_ucs`, mọi row nêu rõ scope | `TECHDOC_STALE_REF` ⓘ — dùng `--realign-techdoc-revision` |
538
+
539
+ > **`doc-global` là đường ra bình thường cho tech-doc, không phải ngoại lệ.** Sửa §11 Cross-cutting hay §2 kiến trúc chung thì không mệnh đề nào nêu UC ⇒ `affected_ucs` rỗng ⇒ mọi UC ở lại ⓘ. Nên `/generate-tech-docs` **không cần** dùng `[no-behavior]`; marker đó dành cho producer biết chính xác `check_id` của từng fix (`/review-context --fix`). Consumer vẫn phải **hiểu** marker vì cùng một hàng bảng phục vụ cả hai artifact.
350
540
 
351
541
  ### Step 5e — Realign mode *(chỉ chạy khi có flag)*
352
542
 
@@ -462,6 +652,22 @@ Với mỗi file `.tsv` đã xử lý: ghi `spec_ver`, `status`, `last_updated`
462
652
  Và **đồng bộ `prd_status` ← `| **Status** |`** của PRD tương ứng (`{paths.specs_dir}/{domain}/{prd-slug}/{TICKET-ID}-{prd-slug}.md`) — đối xứng với `uc_status`: PRD Metadata là nguồn-sự-thật về duyệt PRD. Không có bước này thì `prd_status` là **write-once** (chỉ `/generate-bdd` ghi một lần) và sẽ giữ `approved` vĩnh viễn sau khi `/refine-prd` hay `/review-context --fix` reset PRD về `draft`. *(Step 4 đã đọc file PRD này rồi — không phát sinh I/O.)*
463
653
  **Đừng** sửa `dev_selftest`/`dev_selftest_at` (do `/dev-run-test` sở hữu) hay `qc_status`/`qc_run_at`/`qc_owner`/`qc_blocked_by` (do `/qc-run-test` + `/report-bug` sở hữu); lệnh này chỉ đọc chúng cho report.
464
654
 
655
+ ### Step 6b — Ghi mốc `spec_baseline` cho lần audit sau
656
+
657
+ *Đây là **nửa GHI** của check ở Step 3.9. Thiếu nó thì cờ `PRD_UNTRACKED_EDIT` **không bao giờ bật** — và mọi rule khác vẫn ✅, vì R6 chỉ canh "giá trị enum có xuất hiện" và R7 chỉ canh "cờ có counter". `self-check` R13 canh đúng nửa này.*
658
+
659
+ Với **mỗi** file PRD trong phạm vi lần chạy này, ghi một entry vào khối `spec_baseline` của `trace-report.json` (Step 8):
660
+
661
+ | Key | Giá trị |
662
+ |---|---|
663
+ | `prd_path` | path tương đối tới file PRD trong specs repo |
664
+ | `sha_at_audit` | `git -C {specs repo} rev-parse HEAD` — commit tại thời điểm audit |
665
+ | `version_at_audit` | `\| **Version** \|` hiện tại của PRD |
666
+
667
+ Không phải git repo / `git` không khả dụng → ghi `sha_at_audit: null` và **vẫn ghi** `version_at_audit` (lần sau Step 3.9 sẽ báo đang mù, không im lặng).
668
+
669
+ > **Ghi mốc là thao tác cuối, sau khi đã phán xong.** Ghi trước thì một lần chạy bị ngắt giữa đường sẽ để lại mốc mới mà chưa phán gì — và lần sau so với mốc đó thì thay đổi trong khoảng đó **biến mất khỏi tầm quan sát vĩnh viễn**.
670
+
465
671
  ### Step 7 — Tính aggregate cho dashboard
466
672
 
467
673
  ```
@@ -486,6 +692,9 @@ fe_integrated = rows where fe_phase == integrated # FE đã wire adapter th
486
692
  # công việc CHƯA XONG dù status có thể đã là OK (có code + có test trên mock).
487
693
  orphaned_count = rows where status == ORPHANED # code còn, scenario đã bị xoá khỏi .feature (Step 2b/Rule 0)
488
694
  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)
695
+ prd_untracked_edit_count = số file PRD có nội dung đổi kể từ `sha_at_audit` mà `Version` KHÔNG đổi
696
+ (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
+ của Step 4/5 trên file đó đang dựa vào một nhãn không còn đúng.
489
698
  prd_drift_count = số UC bị cờ PRD_DRIFT (lệch version VÀ changelog nêu UC này — Step 4)
490
699
  prd_stale_ref_count = số UC bị cờ PRD_STALE_REF (lệch version nhưng changelog KHÔNG nêu UC này —
491
700
  nội dung không đổi, chỉ con trỏ cũ). ⓘ không phải lỗi; sạch bằng --realign-prd-version
@@ -517,6 +726,11 @@ seam_unwired_count = số seam bị cờ SEAM_UNWIRED (hàng thật đã có nh
517
726
  seam_pending_count = số seam bị cờ SEAM_PENDING (owner UC chưa gen — ⓘ chưa phải lỗi; PM dùng để xếp thứ tự gen)
518
727
  stub_unresolved_count = số stub bị cờ STUB_UNRESOLVED (method còn trắng dù owner đã gen / có hàm song song — Step 5b)
519
728
  stub_pending_count = số stub bị cờ STUB_PENDING (owner BDD chưa gen — ⓘ chưa phải lỗi)
729
+ service_unrouted_count = số row có `service` ∈ (unrouted, unresolved) SAU khi đã phân giải lại ở
730
+ Step 2c. 🟠 KHÔNG chặn PR — "chưa ai quyết repo" là trạng thái hợp lệ ở feature
731
+ đầu tiên của một domain mới, không phải code hỏng. Nhưng phải NHÌN THẤY ĐƯỢC:
732
+ không có counter thì `unrouted` đọng lại vĩnh viễn và `by_service` có một ô rác
733
+ mà không ai để ý — đúng bệnh `TBD` (G51).
520
734
  # LUẬT (rules/workflow.md §Trace Contract): MỌI giá trị trong vocabularies.audit_flags phải có
521
735
  # một counter {flag_lowercase}_count ở đây VÀ trong summary của JSON. bin/self-check.js R7 ép
522
736
  # điều này — thiếu counter = cờ không quan sát được ở tầng tổng hợp, dashboard không thấy.
@@ -560,7 +774,11 @@ Schema:
560
774
  ```json
561
775
  {
562
776
  "generated_at": "<ISO-8601 timestamp>",
563
- "domain": "<domain argument, or 'all' if no filter>",
777
+ "scope": {
778
+ "kind": "all | domain | prd | uc",
779
+ "value": "<giá trị scope, hoặc 'all'>"
780
+ },
781
+ "domain": "<domain trong scope, hoặc 'all'>",
564
782
  "summary": {
565
783
  "total_prds": 0,
566
784
  "approved_prds": 0,
@@ -579,6 +797,7 @@ Schema:
579
797
  "fe_integrated": 0,
580
798
  "orphaned_count": 0,
581
799
  "trace_orphan_count": 0,
800
+ "prd_untracked_edit_count": 0,
582
801
  "prd_drift_count": 0,
583
802
  "prd_stale_ref_count": 0,
584
803
  "bdd_drift_count": 0,
@@ -592,6 +811,7 @@ Schema:
592
811
  "seam_pending_count": 0,
593
812
  "stub_unresolved_count": 0,
594
813
  "stub_pending_count": 0,
814
+ "service_unrouted_count": 0,
595
815
  "dev_selftest_passing": 0,
596
816
  "dev_selftest_failing": 0,
597
817
  "dev_selftest_not_run": 0,
@@ -658,7 +878,24 @@ Schema:
658
878
  ]
659
879
  }
660
880
  ],
881
+ "spec_baseline": [
882
+ {
883
+ "prd_path": "<specs/{domain}/{prd-slug}/{TICKET-ID}-{prd-slug}.md>",
884
+ "sha_at_audit": "<git rev-parse HEAD của specs repo, hoặc null nếu không phải git repo>",
885
+ "version_at_audit": "<Version của PRD tại thời điểm audit>"
886
+ }
887
+ ],
661
888
  "issues": {
889
+ "prd_untracked_edit": [
890
+ {
891
+ "prd_path": "<specs/{domain}/{prd-slug}/{TICKET-ID}-{prd-slug}.md>",
892
+ "version": "<Version hiện tại — KHÔNG đổi kể từ lần audit>",
893
+ "sha_at_audit": "<commit lần audit trước>",
894
+ "evidence": "git diff | git status | cả hai",
895
+ "affected_ucs": ["<mọi UC của PRD này — không suy đoán được ai bị đụng>"],
896
+ "fix": "bump Version + ghi row changelog nêu UC bị ảnh hưởng (đó là việc /amend-prd làm hộ). KHÔNG có --accept-edit: dán nhãn lên thay đổi chưa ai xem là đúng cái rào của --realign tồn tại để chặn."
897
+ }
898
+ ],
662
899
  "drift": [
663
900
  {
664
901
  "sc_id": "<SC-ID>",
@@ -797,6 +1034,16 @@ Schema:
797
1034
  "fix": "Trỏ binding của <consumer_uc> sang <RealClass> (xoá/thay stub), build lại"
798
1035
  }
799
1036
  ],
1037
+ "service_unrouted": [
1038
+ {
1039
+ "sc_id": "<SC-ID>",
1040
+ "platform": "web | app | system",
1041
+ "domain": "<domain của PRD>",
1042
+ "value": "unrouted | unresolved",
1043
+ "reason": "<lý do context-loader đã ghi — vd: domain chưa có entry trong services:>",
1044
+ "fix": "thêm mapping cho domain <domain> vào services: của .agent/project-context.yaml, rồi chạy lại /validate-traces (nó tự nâng unrouted → path). KHÔNG cần sinh lại BDD."
1045
+ }
1046
+ ],
800
1047
  "stub_unresolved": [
801
1048
  {
802
1049
  "artifact": "<ClassName#method>",
@@ -826,7 +1073,11 @@ Schema:
826
1073
  Row `ORPHANED` xuất ra JSON là: `"status": "DRIFT"` + `"orphaned": true`. Panel chưa hỗ trợ vẫn hiện nó như `DRIFT` — đủ đúng về nghĩa ("code không khớp spec, cần xử lý") và **không im lặng**; panel có đọc `orphaned` thì hiện nhãn riêng. Chi tiết đầy đủ luôn có ở `orphaned[]` và ở report terminal.
827
1074
  **TSV giữ nguyên chữ `ORPHANED`** trong cột `status` — TSV là nguồn-sự-thật, JSON chỉ là bản xuất cho panel.
828
1075
  - `orphaned` (boolean): `true` chỉ khi cột `status` của TSV là `ORPHANED`; mọi row khác ghi `false` (đừng bỏ trống — panel đọc field vắng dễ ra `undefined`).
829
- - Luôn ghi vào `{paths.trace_dir}/trace-report.json` bất kể domain filter — nếu có domain filter, chỉ gồm các PRD đó trong `prds[]` nhưng ghi domain vào field `domain`
1076
+ - Luôn ghi vào `{paths.trace_dir}/trace-report.json` bất kể phạm vi — nếu có scope, chỉ gồm các PRD/UC đó trong `prds[]`, ghi **cả hai** field:
1077
+ - **`scope`** = `{kind, value}` từ Step 0-A. Đây là field `gate-trace` dùng để quyết chặn — xem dưới.
1078
+ - **`domain`** = domain trong scope (`--domain` → chính nó · `--prd`/`--uc` → domain phân giải được · không scope → `all`). Giữ cho **tương thích ngược** với `gate-trace` bản cũ.
1079
+
1080
+ > ⚠️ **Biên bản có scope KHÔNG BAO GIỜ được coi là biên bản đầy đủ.** `gate-trace` G2 fail nếu `scope.kind !== "all"`, **không ngoại lệ** — nó không nhìn xem trên đĩa có bao nhiêu domain. Vì sao tuyệt đối: bản cũ hỏi *"còn domain nào khác không"*, nên trong repo **một domain** thì `others` là **rỗng** ⇒ không fail ⇒ một biên bản hẹp-theo-PRD được nhận là *"toàn bộ"*. Đó đúng là *"cấp giấy xanh cho thứ chưa ai xem"* mà chú thích của chính gate cảnh báo. Thêm cờ scope mà không siết G2 là biến cổng thành **sân khấu** — đúng cái G39 dựng lên để chống.
830
1081
  - **TSV `"—"` mapping**: khi đọc file TSV, map giá trị dash sang kiểu JSON: `implemented_by: "—"` → `null`; `test_count: "—"` → `0`; `test_classes: "—"` → `[]`; `tech_doc_revision: "—"` → `0`; `fe_tech_doc_revision: "—"` → `0`; `dev_selftest: "—"` → `"not_run"`; `dev_selftest_at: "—"` → `null`; `qc_status: "—"` → `"not_run"`; `qc_run_at: "—"` → `null`; `qc_owner: "—"` → `null`; `qc_blocked_by: "—"` → `null`; `service: "—"` → `null`; `design_spec_version: "—"` → `null`
831
1082
  - **Backward-compat:** TSV cũ có thể thiếu cột mới hơn trong header — coi cột vắng nào là giá trị rỗng của nó (**đừng báo lỗi, đừng bỏ qua cả file**): `qc_owner`/`qc_blocked_by` (pre-19-col) → `null`; `fe_tech_doc_revision` (pre-22-col) → `0`; `service`/`design_spec_version` (pre-24-col) → `null`. Lần `/generate-bdd` gen lại tiếp theo nâng header lên layout **24 cột** hiện tại.
832
1083
  > **Đọc theo TÊN CỘT ở header row, KHÔNG theo vị trí.** Header là dòng đầu mỗi `.tsv` — parse nó rồi tra theo tên. Đếm vị trí sẽ vỡ ở đúng file cũ mà luật này sinh ra để đỡ. Header thiếu hoàn toàn (file hỏng) → mới báo lỗi cho file đó và đi tiếp, không abort cả lệnh.
@@ -991,6 +1242,20 @@ Trace orphan (tag trỏ vào SC không tồn tại, KHÔNG có row .tsv nào):
991
1242
  (Không lệnh nào khác bắt được cái này — row .tsv đã bị xoá bởi version cũ,
992
1243
  hoặc tag ghi sai id ngay từ đầu.)
993
1244
 
1245
+ 🔴 PRD_UNTRACKED_EDIT — nội dung PRD đổi mà nhãn Version KHÔNG đổi ({n} file):
1246
+ specs/payment/create-invoice/PAY01-create-invoice.md Version 1.3 (không đổi từ lần audit)
1247
+ Bằng chứng : git status — sửa CHƯA commit
1248
+ Mốc cũ : sha a1b2c3d · Version 1.3
1249
+ ⚠️ MỌI phán đoán version bên dưới cho PRD này đang dựa vào một nhãn không còn đúng.
1250
+ Không suy đoán được UC nào bị đụng → phải coi cả {n} UC của nó là chưa rõ.
1251
+ Sửa: bump Version + ghi một row changelog NÊU UC bị ảnh hưởng.
1252
+ Đó đúng là việc /amend-prd làm hộ (kèm kiểm va chạm + guard sau-ghi).
1253
+ Không có --accept-edit: dán nhãn lên thay đổi chưa ai xem là đúng cái rào của
1254
+ --realign tồn tại để chặn.
1255
+
1256
+ (hoặc: ⚠️ Chưa kiểm được sửa-ngoài-đường cho {n} PRD ({lý do}) — điểm mù G54 đang MỞ)
1257
+ (hoặc: ⓘ spec_baseline: lần đầu ghi mốc — check có hiệu lực từ lần chạy sau)
1258
+
994
1259
  PRD Version Drift (changelog CÓ nêu UC này — nội dung đổi thật):
995
1260
  {UC}-UC2 — code ở PRD v1.0, PRD giờ ở v1.2 [lệch: tsv, code]
996
1261
  Thay đổi kể từ v1.0:
@@ -1026,6 +1291,14 @@ Seam & Stub Audit (mồ côi khi ghép luồng):
1026
1291
  → /generate-code {owner_uc} lấp logic vào {ClassName#method} tại chỗ, xoá hàm song song, build lại
1027
1292
  ⓘ STUB_PENDING — {ClassName#method} ({stub_for}): owner {owner_uc} chưa gen (chưa phải lỗi)
1028
1293
 
1294
+ {khối dưới CHỈ in khi service_unrouted_count > 0 — else bỏ cả khối}
1295
+ Routing chưa chốt ({service_unrouted_count} scenario) — 🟠 KHÔNG chặn PR:
1296
+ 🟠 SERVICE_UNROUTED — {sc_id} ({platform}), domain "{domain}": {reason}
1297
+ → thêm mapping cho domain "{domain}" vào `services:` của .agent/project-context.yaml,
1298
+ rồi chạy lại /validate-traces — nó tự nâng unrouted → path. KHÔNG cần sinh lại BDD.
1299
+ BDD của các scenario này KHÔNG sai. Đây là bước cấu hình của architect, và nó chỉ CHẶN
1300
+ ở /generate-code (lệnh đó buộc phải biết ghi file vào repo nào).
1301
+
1029
1302
  {khối dưới CHỈ in khi Step 7b tìm thấy ≥1 request Status: Open — else bỏ cả khối}
1030
1303
  📥 Yêu cầu đổi PRD đang chờ ({n} — chưa ai xử lý):
1031
1304
  {UC-ID} — "{title}" chờ {days_waiting} ngày
@@ -65,6 +65,40 @@ Ba mức, định nghĩa đầy đủ ở `steps/gate.md` Bước 3a — **đây
65
65
  **Ngoại lệ có chủ ý:** `qc_owner`/`qc_blocked_by` (con trỏ tới bug — spec đổi không làm bug biến
66
66
  mất) và `test_count`/`test_classes` (test vẫn tồn tại trên đĩa; số lượng không sai, chỉ nội dung
67
67
  cũ → **cảnh báo**, không hạ số, để tỷ lệ coverage không nhảy loạn).
68
+ - **Sửa spec phải đi qua một lệnh.** Mọi drift detector so **nhãn version**, không so **nội dung**
69
+ (0 content hash trong toàn bộ codebase) — nên một PRD/tech-doc bị sửa tay mà không bump version là
70
+ điểm mù **tuyệt đối**: cả `/validate-traces`, `gate-trace`, và `require-fresh-audit` đều xanh, và
71
+ cả ba **đúng theo định nghĩa của chính chúng**. Bốn cửa chính: `/generate-prd` (mới) ·
72
+ `/extend-prd` (**thêm**) · **`/amend-prd`** (**đổi**) · `/refine-prd`/`/review-context --resume`
73
+ (áp finding). `/validate-traces` Step 3.9 canh cửa sau bằng cờ 🔴 `PRD_UNTRACKED_EDIT`
74
+ (`spec_edit_detection`: git diff **và** git status vs mốc `spec_baseline`).
75
+ *Đường ra cố ý **tự lành**, không có `--accept-edit`: bump version + ghi row changelog nêu UC là
76
+ hết cờ. Một cờ escape sẽ là một đường dán nhãn lên thay đổi chưa ai xem — đúng cái ba rào của
77
+ `--realign` tồn tại để chặn.*
78
+ - **Làm mất hiệu lực có MỆNH ĐỀ ĐỐI NGẪU: ai KHẲNG ĐỊNH một giá trị dương phải được phép khẳng
79
+ định.** Luật ngay trên nói *"ai làm giá trị hết đúng thì phải hạ nó"* — đúng, và được thực thi tốt.
80
+ Nhưng thiếu nửa này thì chuỗi thành **hạ xuống → dựng lại**: `/generate-bdd` hạ
81
+ `dev_selftest → not_run` khi spec đổi, rồi `/dev-run-test` (lệnh kế tiếp trong vòng lặp dev bình
82
+ thường) ghi lại `pass` kèm **ngày hôm nay** vì test cũ + code cũ vẫn xanh.
83
+ `pass` **không** mang nghĩa *"test đã chạy xanh"* — nó mang nghĩa *"scenario này đã được nghiệm thu
84
+ theo spec **hiện tại**"*. Trên row `DRIFT` nghĩa thứ nhất đúng và nghĩa thứ hai **sai**. Nên `status`
85
+ trực giao với **kết quả chạy**, **KHÔNG** trực giao với **quyền khẳng định**.
86
+ Contract: `bin/trace-schema.json` → `positive_assertion_guards`; `self-check` **R14** canh chủ cột
87
+ thực sự rẽ nhánh theo `status`, `lint-trace` **T12** bắt trạng thái ở sổ thật bất kể ai ghi.
88
+ **`fail` không bao giờ bị chặn** — đây là guard chống *báo cáo sai*, không phải guard *che tin xấu*.
89
+ - **Dòng changelog là contract máy đọc, không phải ghi chú cho người đọc.** PRD và tech-doc gộp
90
+ đều phủ nhiều UC nhưng chỉ có **một** nhãn version, nên `/validate-traces` Step 4/5 lọc 🟠 `*_DRIFT`
91
+ vs ⓘ `*_STALE_REF` **bằng chính dòng đó**. Grammar khai ở `bin/trace-schema.json` →
92
+ `changelog_row_contract`; `self-check` **R12** fail build nếu lệch. Ba luật:
93
+ **(1)** mỗi mệnh đề mở đầu bằng **đơn vị sở hữu** — `{UC-ID}:` hoặc `PRD-global:`/`doc-global:`;
94
+ **(2)** BR/AC **luôn đi kèm UC sở hữu** (`UC3: sửa BR8`), **không bao giờ đứng một mình** —
95
+ consumer khớp theo UC, nên `sửa BR8` trơ trọi làm UC3 bị xếp ⓘ trong khi BR8 vừa đổi hành vi, và
96
+ `--realign-prd-version` (chỉ chặn 🟠) sẽ dán nhãn version lại lên đó;
97
+ **(3)** hậu tố `[no-behavior]` **chỉ** cho thay đổi mà producer **chứng minh được** là không đổi
98
+ hành vi (`changelog_row_contract.neutral_checks`) — không dành cho người tự khai.
99
+ *Lưới an toàn "row mơ hồ → 🟠 cho MỌI UC" đúng khi **thiếu** thông tin, và sai khi producer **có**
100
+ thông tin mà không ghi: đó là G52 — `/review-context --fix` từng ghi cứng một dòng 0 scope trong
101
+ khi findings YAML của nó có `uc_id` bắt buộc cho từng finding.*
68
102
  - **Mỗi audit flag phải quan sát được ở CẢ BA tầng.** Mọi giá trị trong
69
103
  `vocabularies.audit_flags` bắt buộc có đủ: **(1)** một counter `{flag_lowercase}_count`
70
104
  trong Step 7 + `summary` của `trace-report.json` · **(2)** một mảng trong `issues` ·
@@ -161,11 +161,25 @@ services:
161
161
 
162
162
  *(Cả 2a/2b/2c: override `paths.specs_dir`/`paths.tech_docs_dir` per-service CHỈ khi `setup.spec_source` KHÔNG được đặt. Khi `spec_source` ĐƯỢC đặt, MỌI BDD/tech-doc là artifact liên team → để bước 4 route sang spec repo; KHÔNG pin per-service ở đây.)*
163
163
 
164
- **3. Fallback**:
165
- - Không phát hiện được domain, hoặc domain không khớp key nào trong `services` → giữ path mặc định từ Bước 1, đặt `active_service = unresolved`.
166
- - Domain khớp một map-theo-platform (2b) nhưng `active_platform` xác định mà thiếu sub-key tương ứng → `active_service = unresolved`, ghi do để lệnh DỪNG báo lỗi cấu hình (không tự đoán platform).
167
- - Entry là map-theo-prd_slug (2c) nhưng `prd_slug` xác định thiếu key tương ứng `active_service = unresolved`, ghi do rõ (không tự đoán submodule).
168
- - Entry sai cấu trúc (vừa có `path` vừa có `by_prd_slug`, hoặc `by_prd_slug` lồng nhau) → `active_service = unresolved`, nêu đúng key sai để người dùng sửa `project-context.yaml`.
164
+ **3. Fallback** — **hai trạng thái khác nhau, đừng gộp** *(G51)*:
165
+
166
+ > `unrouted` = **chưa ai quyết** repo. Hợp lệ, bình thường feature đầu tiên của domain mới.
167
+ > `unresolved` = **config sai cấu trúc**. **bug** cần sửa file, không phải trạng thái chờ.
168
+ >
169
+ > Trước G51 cả hai dùng chung tên `unresolved` nên chịu chung hình phạt: `/generate-bdd` DỪNG HẲN.
170
+ > Nhưng PRD/BDD là artifact **nghiệp vụ** — PO biết `domain` và biết `platform`, **không** biết code
171
+ > sẽ nằm repo nào, và thường lúc đó chưa ai quyết. Cổng đặt sai phase.
172
+
173
+ **→ `unrouted`** (chưa có mapping — **không** phải lỗi):
174
+ - Không phát hiện được domain, hoặc domain **không khớp key nào** trong `services` → giữ path mặc định từ Bước 1, đặt `active_service = unrouted`.
175
+ - Domain khớp map-theo-platform (2b) nhưng thiếu sub-key cho `active_platform` → `active_service = unrouted`, ghi lý do rõ (không tự đoán platform).
176
+ - Entry là map-theo-prd_slug (2c) nhưng thiếu key cho `prd_slug` → `active_service = unrouted`, ghi lý do rõ (không tự đoán submodule).
177
+
178
+ **→ `unresolved`** (config **sai cấu trúc** — bug):
179
+ - Entry vừa có `path` vừa có `by_prd_slug`, hoặc `by_prd_slug` lồng nhau → `active_service = unresolved`, nêu đúng key sai để người dùng sửa `project-context.yaml`.
180
+
181
+ *Cả hai đều KHÔNG chặn việc nạp context. Lệnh nào chặn là quyết định của lệnh đó: `/generate-bdd`
182
+ đi tiếp với `unrouted` (Step 1.6) · `/generate-code` DỪNG ở cả hai (nó buộc phải biết ghi vào đâu).*
169
183
 
170
184
  **4. Tự động override theo spec source** — nếu `setup.spec_source` được đặt VÀ path tương ứng chưa được set tường minh trong `paths:`:
171
185
  - Override `paths.specs_dir` → `{spec_source}/specs` — **luôn khi `spec_source` được đặt.** Mọi spec artifact (PRD, BDD, tech-docs, design-spec) nằm dưới gốc spec thống nhất trong spec repo dùng chung theo bố cục feature-package: `{spec_source}/specs/{domain}/{prd-slug}/`. Mọi umbrella (FE/App/BE) đều đọc từ đây. *(`specs/` theo service chỉ khi không có `spec_source`.)*
@@ -209,6 +223,13 @@ Khi `active_service` đã được phân giải thành một path thật ở Bư
209
223
 
210
224
  **4. Nếu không tìm thấy config của service** — giữ mặc định umbrella, vẫn set `service_root = {active_service}` (luôn cần mốc path kể cả khi không có config override).
211
225
 
226
+ > ⚠️ **`service_root` KHÔNG BAO GIỜ được là một chuỗi trạng thái** *(G51)*. Nếu `active_service` là
227
+ > `unrouted` / `unresolved` / `multi` / `—` thì đặt **`service_root = null`** và giữ path mặc định
228
+ > umbrella — **đừng** nội suy giá trị đó thành tên thư mục.
229
+ > Bản trước đặt `service_root = {active_service}` vô điều kiện, nên `/generate-code` (ghi file
230
+ > **tương đối với `service_root`**) sẽ ghi source vào một thư mục tên đúng chữ `unresolved/`.
231
+ > `service_root = null` là tín hiệu để `/generate-code` DỪNG thay vì ghi bừa.
232
+
212
233
  ---
213
234
 
214
235
  ## Bước 2 — [PROJECT-CONFIG] Nạp module stack profile (có điều kiện)
@@ -4,7 +4,7 @@
4
4
  # @trace.revision: 1 ← field tĩnh; version theo dõi bằng @trace.bdd_version
5
5
  # @trace.domain: <domain>
6
6
  # @trace.platform: {active_platform — web | app | system} ← BẮT BUỘC mọi mode; phải khớp segment bdd/{platform}/ của path
7
- # @trace.service: {active_service BẮT BUỘC mọi mode. "" single-service/spec repo mode; "multi" nếu chưa chốt; "unresolved" nếu routing sai. Nguồn của cột TSV `service` trace gộp không tách theo service nên đây là chỗ DUY NHẤT mang thông tin sở hữu}
7
+ # @trace.service: {service của ĐÚNG platform file nàyBẮT BUỘC mọi mode. Nguồn của cột TSV `service`; trace gộp không tách theo service nên đây là chỗ DUY NHẤT mang thông tin sở hữu ở cấp row. Bốn giá trị: {path} · "unrouted" (chưa ai quyết repo — HỢP LỆ, cờ 🟠, KHÔNG chặn) · "unresolved" (config sai cấu trúc — bug) · "—" (single-service). KHÔNG ghi "multi": file này đã có MỘT platform xác định nên service_candidates.{platform}.path đã biết — ghi path đó (G51)}
8
8
  # @trace.module: {active_module trong umbrella mode; "unknown" trong spec repo mode}
9
9
  # @trace.status: draft
10
10
  # @trace.author: AI-generated
@@ -86,6 +86,42 @@ Kết quả: `/generate-code` 108 KB → **69 KB**, `/refine-prd` 85 KB → **45
86
86
  > **G50 gỡ hẳn legacy mode**, nên giờ mọi bản cài đều có `.agent/steps/` và nhánh build thứ hai
87
87
  > biến mất. Hai nhánh build gần giống nhau là nợ chờ lệch.
88
88
 
89
+ ### Tập publish — chỉ ship MỘT bản *(GAPS-v4 G59)*
90
+
91
+ `npm pack` chỉ mang **`bin/` · `core/` · `scripts/` · `docs/`**.
92
+
93
+ Trước G59, `files` có **10 mục**, và **7 trong 10** là bản sao của thứ đã có trong `core/` —
94
+ `commands/` (33/33 file `.md` **byte-identical** với `core/commands/`), cộng
95
+ `hooks/ modules/ rules/ skills/ steps/ templates/` (`diff -rq` không khác gì). Installer đọc
96
+ **chỉ `core/`** (`installCore(coreDir, agentDir, …)`), nên bản thứ hai không bao giờ được dùng.
97
+
98
+ | | Trước | Sau |
99
+ |---|---:|---:|
100
+ | Tarball nén | 1.2 MB | **710 kB** |
101
+ | Giải nén | 4.5 MB | **2.3 MB** |
102
+ | Số file | 395 | **215** |
103
+
104
+ **Nhưng cái đáng sửa hơn là một phép phân biệt bị vô hiệu.** `bin/index.js` dùng
105
+ `hasSources = exists(commands/generate-code.tmpl)` để biết *"đây là dev checkout hay bản cài từ
106
+ npm"*, và chú thích của nó viết thẳng: *"Chỉ nói trong DEV CHECKOUT. **Consumer không cần biết bước
107
+ này tồn tại**"*. Nhưng `commands/` được ship ⇒ `.tmpl` có mặt ⇒ phép thử **luôn đúng** ⇒ **mọi**
108
+ người dùng `npx` thấy `"Vừa sửa commands/*.tmpl ? Chạy npm run build trước"` — một câu họ không thể
109
+ làm gì với nó. *(Kiểm bằng cách `npm pack` rồi chạy tarball thật, không phải suy đoán.)*
110
+
111
+ Bốn nhánh sau khi sửa, `hasSources` đặt tên **một lần**:
112
+
113
+ | `corePrebuilt` | `hasSources` | Nghĩa | Làm gì |
114
+ |:---:|:---:|---|---|
115
+ | ✗ | ✓ | dev checkout, `core/` vắng/lệch | build từ nguồn |
116
+ | ✗ | ✗ | **bản cài npm bị thiếu/hỏng** | **lỗi rõ ràng + `exit 1`** — không cố build (G43: build ghi vào npx cache / global `node_modules`, có thể read-only, và hai `--init` song song sẽ đua nhau) |
117
+ | ✓ | ✓ | dev checkout, đã khớp version | in lời nhắc *"sửa `.tmpl` thì build lại"* |
118
+ | ✓ | ✗ | **bản cài npm, mọi thứ đúng** | **im lặng** ← đường của consumer |
119
+
120
+ Bất biến được `test/run.js` canh, viết theo **hình dạng** chứ không theo danh sách tên nên tự khớp
121
+ với dir thêm sau này: *không mục nào trong `files` được có bản mirror dưới `core/`*.
122
+
123
+ `scripts/` **phải giữ** — `bin/index.js` `require('../scripts/migrate-specs.js')` cho `--migrate-*`.
124
+
89
125
  - Cơ chế `{{include:steps/...}}` → single source of truth ở `.tmpl` + `steps/`.
90
126
  - **Không sửa tay** `commands/*.md` / `.agent/` — sửa `.tmpl`/`steps` rồi `node bin/build.js`. *(Quy ước + memory bảo vệ, không phải hook.)*
91
127
  - **Sửa `steps/context-loader.md` hay `report-footer.md` giờ có hiệu lực NGAY** ở project đã cài — chúng được đọc lúc chạy, không còn phải build + publish + `/update-framework`.