@educa-corp/sdd-framework 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/qc-base-map.json +595 -0
- package/bin/self-check.js +146 -0
- package/core/FRAMEWORK_VERSION +1 -1
- package/core/commands/propose-scenario.md +1 -1
- package/core/commands/qc-analyze.md +398 -37
- package/core/commands/qc-design-test.md +141 -2
- package/core/commands/qc-plan.md +159 -10
- package/core/commands/qc-review.md +134 -1
- package/core/commands/qc-run-test.md +135 -2
- package/core/commands/refine-prd.md +47 -20
- package/core/commands/report-bug.md +1 -1
- package/core/commands/review-context.md +27 -1
- package/core/modules/qc-playwright/stack-profile.yaml +3 -3
- package/core/skills/qc/qa-analyst/DOC_GAP.template.md +147 -0
- package/core/skills/qc/qa-analyst/acceptance-criteria.md +5 -3
- package/core/skills/qc/qa-analyst/business-rules.md +39 -5
- package/core/skills/qc/qa-analyst/data-flow.md +6 -4
- package/core/skills/qc/qa-analyst/exhaustive-gap-scanner.md +174 -0
- package/core/skills/qc/qa-analyst/spec-breakdown.md +10 -8
- package/core/skills/qc/qa-analyst/spec-issue-reporter.md +112 -0
- package/core/skills/qc/qa-designer/e2e/journey.md +3 -3
- package/core/skills/qc/qa-designer/exploratory/charter.md +1 -1
- package/core/skills/qc/qa-designer/exploratory/explore-to-functional.md +2 -2
- package/core/skills/qc/qa-designer/functional/api.md +3 -3
- package/core/skills/qc/qa-designer/functional/gui-feature.md +3 -3
- package/core/skills/qc/qa-designer/functional/gui-screen.md +3 -3
- package/core/skills/qc/qa-designer/integration/api.md +3 -3
- package/core/skills/qc/qa-designer/integration/db.md +3 -3
- package/core/skills/qc/qa-designer/integration/gui.md +3 -3
- package/core/skills/qc/qa-designer/integration/kafka.md +3 -3
- package/core/skills/qc/qa-designer/non-functional.md +3 -3
- package/core/skills/qc/qa-planner/risk-model.md +106 -0
- package/core/skills/qc/qa-planner/test-plan.md +35 -21
- package/core/skills/qc/qa-reviewer/script/e2e.md +1 -1
- package/core/skills/qc/qa-reviewer/script/exploratory.md +1 -1
- package/core/skills/qc/qa-reviewer/script/functional.md +1 -1
- package/core/skills/qc/qa-reviewer/script/integration.md +1 -1
- package/core/skills/qc/qa-reviewer/script/non-functional.md +1 -1
- package/core/skills/qc/qa-reviewer/test-case/e2e.md +1 -1
- package/core/skills/qc/qa-reviewer/test-case/exploratory.md +1 -1
- package/core/skills/qc/qa-reviewer/test-case/functional.md +1 -1
- package/core/skills/qc/qa-reviewer/test-case/integration.md +2 -2
- package/core/skills/qc/qa-reviewer/test-case/non-functional.md +1 -1
- package/core/skills/qc/qa-runner/e2e.md +1 -1
- package/core/skills/qc/qa-runner/exploratory/session.md +2 -2
- package/core/skills/qc/qa-runner/functional/api.md +1 -1
- package/core/skills/qc/qa-runner/functional/gui-feature.md +1 -1
- package/core/skills/qc/qa-runner/functional/gui-screen.md +1 -1
- package/core/skills/qc/qa-runner/integration.md +1 -1
- package/core/skills/qc/qa-runner/non-functional.md +1 -1
- package/core/skills/qc/qa-runner/report/report.md +1 -1
- package/core/steps/context-loader.md +1 -1
- package/core/steps/gap-verify.md +231 -0
- package/core/steps/qc-scope.md +119 -0
- package/core/steps/review-fanout.md +27 -1
- package/core/templates/project-context.yaml +5 -3
- package/docs/02-concepts/pipeline-steps/08-qc-automation.md +2 -2
- package/docs/04-reference/commands.md +1 -1
- package/docs/04-reference/configuration.md +146 -146
- package/docs/explain/03-refine-prd.md +8 -6
- package/docs/explain/15-qc-analyze.md +10 -7
- package/docs/explain/16-qc-plan.md +3 -3
- package/docs/explain/17-qc-design-test.md +1 -1
- package/docs/plans/qc-implementation-log.md +1587 -0
- package/docs/plans/qc-merge-plan.md +502 -0
- package/docs/plans/qc-sync-command.md +359 -0
- package/package.json +1 -1
- package/scripts/migrate-qc-docs.js +261 -0
- package/core/skills/qc/qa-analyst/DOC_GAPS.template.md +0 -63
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# Plan — lệnh sync skill QC từ upstream vào framework
|
|
2
|
+
|
|
3
|
+
> Tài liệu này là **phần 2** của [`qc-merge-plan.md`](qc-merge-plan.md).
|
|
4
|
+
> Plan kia trả lời *"gộp cái gì, và ba quyết định nào đang chặn"*.
|
|
5
|
+
> Plan này trả lời *"về sau QC sửa skill thì làm sao đưa vào framework mà không vỡ"*.
|
|
6
|
+
|
|
7
|
+
| | |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Snapshot | `upstream/qc-base/` — 82 file, 10.008 dòng, **không ship** (ngoài `package.json` `files`) |
|
|
10
|
+
| Upstream | `D:\projects\ui-automation-testing` @ `596149f` (git repo thật → pin được) |
|
|
11
|
+
| Đích | `skills/qc/qa-*` (35 file) + 6 lệnh `commands/qc-*.tmpl` |
|
|
12
|
+
| Trạng thái | plan — chưa code |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1 · Vì sao đây không phải copy file
|
|
17
|
+
|
|
18
|
+
Ba lý do, xếp theo độ khó tăng dần.
|
|
19
|
+
|
|
20
|
+
### (a) Ánh xạ không phải 1:1
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
upstream framework
|
|
24
|
+
qa-tc-analyst (1 agent) ─┬──► /qc-analyze (trạm 1) + skills/qc/qa-analyst/
|
|
25
|
+
23 skill └──► /qc-plan (trạm 2) + skills/qc/qa-planner/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Một đơn vị upstream nở ra hai đơn vị framework. `test-plan.md` của `qa-tc-analyst`
|
|
29
|
+
đã nằm ở `qa-planner/`. Copy theo thư mục là sai ngay từ bước đầu.
|
|
30
|
+
|
|
31
|
+
### (b) File được **adapt**, không phải copy
|
|
32
|
+
|
|
33
|
+
Cả 4 file trùng tên đều khác nội dung, và khác có chủ ý:
|
|
34
|
+
|
|
35
|
+
| | upstream | framework | framework thêm gì |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| `business-rules` | 90 dòng | 59 | `{paths.qc_dir}` · rào *"chỉ trả 2 file"* |
|
|
38
|
+
| `acceptance-criteria` | 57 | 60 | như trên |
|
|
39
|
+
| `data-flow` | 61 | 64 | như trên |
|
|
40
|
+
| `spec-breakdown` | 58 | 61 | như trên |
|
|
41
|
+
|
|
42
|
+
Copy đè = **xoá sạch phần adapt**. Và `business-rules` bản upstream **dài hơn 31 dòng** —
|
|
43
|
+
nên chiều ngược lại cũng đúng: bản framework có thể đã cắt mất nội dung thật, không chỉ đổi path.
|
|
44
|
+
|
|
45
|
+
### (c) Đây là bài toán **merge ba chiều**, và hiện đang thiếu một chiều
|
|
46
|
+
|
|
47
|
+
Khi QC sửa `business-rules.md`, có ba bản:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
BASE THEIRS OURS
|
|
51
|
+
bản upstream mà bản upstream mới bản framework
|
|
52
|
+
framework đã port từ đã adapt
|
|
53
|
+
│ │ │
|
|
54
|
+
└────────── diff A ───────┘ │
|
|
55
|
+
"QC đã đổi gì" │
|
|
56
|
+
└──────────────────── diff B ──────────────────────┘
|
|
57
|
+
"ta đã adapt gì"
|
|
58
|
+
|
|
59
|
+
Không có BASE ⇒ không phân biệt được A với B
|
|
60
|
+
⇒ mọi merge đều là đoán
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**`upstream/qc-base/` chính là BASE** — nhưng chỉ khi nó được **pin**: ghi lại nó ứng với
|
|
64
|
+
commit upstream nào. Một thư mục snapshot không có pin thì chỉ là "một bản copy cũ nào đó",
|
|
65
|
+
không dùng làm base merge được.
|
|
66
|
+
|
|
67
|
+
> **Đây là lý do kiến trúc của toàn bộ plan này:** lệnh cần dựng không phải lệnh copy,
|
|
68
|
+
> mà là một **vendor-branch merge tool có pin**.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 2 · Ba tạo tác phải có trước khi viết lệnh
|
|
73
|
+
|
|
74
|
+
### 2.1 · Pin — `upstream/qc-base/.pin.json`
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"upstream": {
|
|
79
|
+
"repo": "ui-automation-testing",
|
|
80
|
+
"url": "<git remote>",
|
|
81
|
+
"ref": "main",
|
|
82
|
+
"commit": "596149f",
|
|
83
|
+
"captured_at": "2026-08-24"
|
|
84
|
+
},
|
|
85
|
+
"files": {
|
|
86
|
+
"skills/qa-tc-analyst/business-rules.md": "sha1…",
|
|
87
|
+
"skills/qa-tc-analyst/gap-verifier.md": "sha1…"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Hash từng file, không chỉ commit. Lý do: snapshot có thể bị sửa tay (ai đó "sửa nhanh một
|
|
93
|
+
dòng trong `upstream/`"), và lúc đó base không còn là base. Hash bắt được, commit thì không.
|
|
94
|
+
|
|
95
|
+
### 2.2 · Bản đồ ánh xạ — `bin/qc-base-map.json`
|
|
96
|
+
|
|
97
|
+
Đây là tạo tác quan trọng nhất. Mỗi entry trả lời: file upstream này đi đâu, adapt thế nào,
|
|
98
|
+
ai chịu trách nhiệm.
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"map": [
|
|
103
|
+
{
|
|
104
|
+
"upstream": "skills/qa-tc-analyst/business-rules.md",
|
|
105
|
+
"targets": ["skills/qc/qa-analyst/business-rules.md"],
|
|
106
|
+
"adapt": ["path-placeholders", "two-file-guard"],
|
|
107
|
+
"state": "ported",
|
|
108
|
+
"base_sha": "sha1 của bản upstream đã port từ"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"upstream": "skills/qa-tc-analyst/gap-verifier.md",
|
|
112
|
+
"targets": ["steps/gap-verify.md"],
|
|
113
|
+
"adapt": ["path-placeholders", "generalize-doc-gap-to-findings"],
|
|
114
|
+
"state": "planned",
|
|
115
|
+
"note": "P0 — dùng chung cho refine-prd / review-context / qc-analyze"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"upstream": "skills/qa-tc-designer/manual-excel-testcase.md",
|
|
119
|
+
"targets": [],
|
|
120
|
+
"state": "skipped",
|
|
121
|
+
"why": "README upstream khai đã bỏ nhánh manual-Excel; file còn sót"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"upstream": "skills/qa-gate/styled-report-template.py",
|
|
125
|
+
"targets": [],
|
|
126
|
+
"state": "undecided",
|
|
127
|
+
"why": "script thật, không phải prompt — framework QC hiện 100% markdown-first"
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Bốn `state`, và cái thứ tư là cái quan trọng:**
|
|
134
|
+
|
|
135
|
+
| state | Nghĩa |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `ported` | đã đưa vào, có `base_sha` để merge lần sau |
|
|
138
|
+
| `planned` | đã quyết sẽ đưa, chưa làm |
|
|
139
|
+
| `skipped` | **cố ý** không đưa, có lý do ghi lại |
|
|
140
|
+
| `undecided` | chưa ai quyết ← **đây là hàng đợi việc** |
|
|
141
|
+
|
|
142
|
+
File upstream **không có entry nào** = bị bỏ quên trong im lặng. Đó là failure mode duy nhất
|
|
143
|
+
mà bản đồ này tồn tại để chặn — và §5 để máy canh nó.
|
|
144
|
+
|
|
145
|
+
### 2.3 · Luật adapt dạng **dữ liệu**, không phải người nhớ
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"adapt_rules": {
|
|
150
|
+
"path-placeholders": [
|
|
151
|
+
{ "from": "inputs/free-trial-spec/specs/", "to": "{paths.specs_dir}/" },
|
|
152
|
+
{ "from": "docs/<feature>/", "to": "{paths.qc_dir}/{TICKET-ID}/{active_platform}/",
|
|
153
|
+
"note": "sau B11 phép map này gần như đồng nhất — upstream vốn đã là cấp feature (`docs/<feature>/`); chỉ thêm một cấp nền" },
|
|
154
|
+
{ "from": "inputs/", "to": "{paths.specs_dir}/", "note": "khái niệm nguồn-evidence" }
|
|
155
|
+
],
|
|
156
|
+
"two-file-guard": { "kind": "append-block", "block": "…rào 'chỉ trả 2 file'…" },
|
|
157
|
+
"orchestrator-to-agent-mode": {
|
|
158
|
+
"kind": "manual",
|
|
159
|
+
"why": "upstream nhận routing plan từ qa-orchestrator; framework dùng gate Bước 0 _agent_mode. Không map máy được."
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Ba loại luật: **thay chuỗi** (máy làm) · **chèn block** (máy làm) · **`manual`** (buộc người
|
|
166
|
+
xem, lệnh dừng lại hỏi). Loại thứ ba là chỗ tránh được cái bẫy "tool tự tin adapt sai".
|
|
167
|
+
|
|
168
|
+
> **37 file upstream hardcode `inputs/…`** — đây là khối lượng chính của `path-placeholders`.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 3 · Chia việc: script làm phần cơ học, lệnh làm phần phán đoán
|
|
173
|
+
|
|
174
|
+
Framework đã có tiền lệ cho cách chia này: `bin/lint-trace.js` (script, chạy được trong CI)
|
|
175
|
+
vs `/validate-traces` (LLM, không chạy được trong CI thường). Áp đúng khuôn đó.
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
bin/qc-sync.js /sync-qc-base (lệnh markdown)
|
|
179
|
+
───────────────────────────── ────────────────────────────────
|
|
180
|
+
hash + so pin "thay đổi này thuộc trạm 1 hay trạm 2?"
|
|
181
|
+
diff ba chiều "skill mới này có thuộc phạm vi framework?"
|
|
182
|
+
áp adapt_rules loại string/block áp adapt_rules loại `manual`
|
|
183
|
+
bookkeeping pin + base_sha đọc diff, quyết giữ/bỏ phần adapt của ta
|
|
184
|
+
phát hiện entry thiếu / mồ côi viết lại đoạn cần diễn đạt theo giọng framework
|
|
185
|
+
in báo cáo máy-đọc-được CHECKPOINT trước khi ghi
|
|
186
|
+
───────────────────────────── ────────────────────────────────
|
|
187
|
+
deterministic · CI-able · --dry-run cần người · cần ngữ cảnh · có gate
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Nguyên tắc: **script không bao giờ tự ghi vào `skills/qc/qa-*` hay `commands/`
|
|
191
|
+
khi có `manual` rule hoặc conflict.** Nó dựng patch + báo; lệnh mới là chỗ quyết.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 4 · Sáu lệnh con
|
|
196
|
+
|
|
197
|
+
Framework không dùng sub-command thật — nó dùng cờ trên một lệnh (`--resume`, `--fix`,
|
|
198
|
+
`--full`). Giữ đúng quy ước đó:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
/sync-qc-base [--status] [--pull] [--diff] [--port] [--adopt] [--verify]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
| Cờ | Trả lời câu | Đọc | Ghi | Chặn |
|
|
205
|
+
|---|---|---|---|---|
|
|
206
|
+
| `--status` *(mặc định)* | "upstream đi trước ta bao nhiêu?" | pin, snapshot, map, upstream | — | không |
|
|
207
|
+
| `--pull` | "lấy bản upstream mới về snapshot" | upstream git | `upstream/qc-base/` + `.pin.json` | không¹ |
|
|
208
|
+
| `--diff` | "QC đổi gì, ta adapt gì, có đè nhau không?" | 3 chiều | — | không |
|
|
209
|
+
| `--port` | "đưa thay đổi upstream vào file framework" | 3 chiều + adapt_rules | `skills/qc/qa-*`, `commands/*.tmpl` | **CỨNG** |
|
|
210
|
+
| `--adopt` | "nhận một skill upstream CHƯA có trong map" | snapshot + map | map entry + file đích mới | **CỨNG** |
|
|
211
|
+
| `--verify` | "pin/map/frontmatter còn khớp nhau không?" | tất cả | — | không |
|
|
212
|
+
|
|
213
|
+
¹ `--pull` không chặn vì nó chỉ đụng `upstream/` — nhưng nó **phải** cảnh báo nếu snapshot
|
|
214
|
+
đang bị sửa tay (hash lệch pin), vì lúc đó base sắp bị ghi đè mất.
|
|
215
|
+
|
|
216
|
+
### Chi tiết từng cờ
|
|
217
|
+
|
|
218
|
+
**`--status`** — bức ảnh một màn hình:
|
|
219
|
+
```
|
|
220
|
+
Snapshot : 596149f (2026-08-24) Upstream: a3f21c8 (2026-09-02) → đi trước 7 commit
|
|
221
|
+
Đổi : 12 file
|
|
222
|
+
├─ 4 ported → cần --port
|
|
223
|
+
├─ 2 planned → chưa làm, không ảnh hưởng
|
|
224
|
+
├─ 1 skipped → bỏ qua đúng chủ ý
|
|
225
|
+
└─ 5 KHÔNG CÓ TRONG MAP ← chạy --adopt hoặc khai skipped
|
|
226
|
+
Framework: 1 file đã bị sửa tay sau lần port (base_sha lệch) → --diff xem trước
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**`--pull`** — làm mới snapshot. Không đụng framework. Ba việc: fetch/checkout ref,
|
|
230
|
+
ghi lại `.pin.json`, và **giữ bản pin cũ** thành `.pin.prev.json` — vì merge ba chiều lần
|
|
231
|
+
`--port` sau cần BASE là bản *trước khi* pull.
|
|
232
|
+
|
|
233
|
+
> Đây là chỗ dễ sai nhất của cả thiết kế: `--pull` mà không lưu base cũ thì `--port`
|
|
234
|
+
> mất chiều thứ ba, và ta quay về đúng vấn đề §1(c).
|
|
235
|
+
|
|
236
|
+
**`--diff`** — bề mặt review. Mỗi file đổi, in ba khối: QC đổi gì · ta adapt gì · hai cái
|
|
237
|
+
có chạm nhau không. Chạm nhau = conflict thật, cần người.
|
|
238
|
+
|
|
239
|
+
**`--port`** — merge + adapt + ghi. Chặn CỨNG (`--yes` không bỏ được) vì nó ghi đè file
|
|
240
|
+
framework đã có. Với mỗi file: 3-way merge → áp `adapt_rules` string/block → gặp rule
|
|
241
|
+
`manual` hoặc conflict thì **để lại** vào file findings, không tự quyết → cập nhật
|
|
242
|
+
`base_sha` cho các file đã port sạch.
|
|
243
|
+
|
|
244
|
+
**`--adopt`** — đường vào cho skill upstream mới (và cho **P1** của plan kia: 18 skill của
|
|
245
|
+
`qa-tc-analyst` chưa có đều đi qua đây). Việc nó làm: hỏi đích (`skills/qc/…`? `steps/`?
|
|
246
|
+
một `commands/*.tmpl`?), hỏi `adapt` nào áp, dựng bản adapt đầu tiên, tạo entry map với
|
|
247
|
+
`state: ported` + `base_sha`.
|
|
248
|
+
|
|
249
|
+
**`--verify`** — invariant check, chạy được trong CI:
|
|
250
|
+
|
|
251
|
+
| Kiểm | Bắt lỗi gì |
|
|
252
|
+
|---|---|
|
|
253
|
+
| mọi file snapshot có entry trong map | **skill upstream bị bỏ quên im lặng** |
|
|
254
|
+
| entry map trỏ tới file tồn tại (cả 2 phía) | map lỗi thời sau khi upstream đổi tên |
|
|
255
|
+
| `base_sha` của entry `ported` khớp một file snapshot | base đã trôi, merge lần sau sẽ sai |
|
|
256
|
+
| frontmatter `upstream_path` + `upstream_sha` của file đích khớp map | file đích bị sửa tay ngoài luồng |
|
|
257
|
+
| `skills/qc/qa-*` không còn chuỗi `inputs/` sót | adapt làm nửa vời |
|
|
258
|
+
| lệnh `qc-*` không trỏ vào skill đã mất | **đúng lỗi vừa xảy ra hôm nay** |
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 5 · Rule máy canh — `R16`
|
|
263
|
+
|
|
264
|
+
`--verify` là lệnh người gọi. Nhưng framework này có một nguyên tắc lặp lại năm lần trong
|
|
265
|
+
loạt GAP: *luật đúng, viết rõ, và không ai canh*. Nên phần **cốt lõi** của `--verify` phải
|
|
266
|
+
thành rule trong `bin/self-check.js`, fail build:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
R16 — snapshot upstream và framework không được trôi khỏi nhau trong im lặng
|
|
270
|
+
|
|
271
|
+
a) mọi file trong upstream/qc-base/ phải có entry trong bin/qc-base-map.json
|
|
272
|
+
→ thiếu entry = bị bỏ quên. state `skipped` là câu trả lời hợp lệ; VẮNG MẶT thì không.
|
|
273
|
+
b) mọi entry `state: ported` phải có base_sha, và base_sha phải khớp file snapshot
|
|
274
|
+
c) mọi target của entry `ported` phải tồn tại
|
|
275
|
+
d) mọi lệnh qc-* trỏ vào {paths.qc_skills_dir}/X phải có X tồn tại trong skills/qc/
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**(d) là rule đáng lẽ đã chặn sự cố hôm nay** — 35 file skill bị xoá, `self-check` xanh,
|
|
279
|
+
`build` xanh, `.agent` vẫn chạy được nên dogfood không phát hiện, và installer thì sẽ
|
|
280
|
+
*chủ động xoá* chúng ở project người dùng. Ba tầng che nhau, không tầng nào bắt.
|
|
281
|
+
|
|
282
|
+
`R16(d)` **đáng làm ngay, độc lập với toàn bộ plan này.**
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## 6 · Luồng điển hình
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
QC push skill mới
|
|
290
|
+
│
|
|
291
|
+
▼
|
|
292
|
+
/sync-qc-base --status "đi trước 7 commit, 12 file đổi, 5 chưa có trong map"
|
|
293
|
+
│
|
|
294
|
+
▼
|
|
295
|
+
/sync-qc-base --pull snapshot: 596149f → a3f21c8 (giữ .pin.prev.json)
|
|
296
|
+
│
|
|
297
|
+
▼
|
|
298
|
+
/sync-qc-base --diff đọc: QC đổi gì · ta adapt gì · chỗ nào chạm nhau
|
|
299
|
+
│
|
|
300
|
+
├──► 5 file chưa có trong map
|
|
301
|
+
│ │
|
|
302
|
+
│ ▼
|
|
303
|
+
│ /sync-qc-base --adopt quyết đích + adapt cho từng cái
|
|
304
|
+
│
|
|
305
|
+
▼
|
|
306
|
+
/sync-qc-base --port CHECKPOINT CỨNG → merge + adapt → conflict ra findings
|
|
307
|
+
│
|
|
308
|
+
▼
|
|
309
|
+
node bin/self-check.js R16 xanh
|
|
310
|
+
node bin/build.js 41/41
|
|
311
|
+
│
|
|
312
|
+
▼
|
|
313
|
+
/sync-qc-base --verify invariant sạch
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## 7 · Phase triển khai
|
|
319
|
+
|
|
320
|
+
| Phase | Việc | Phụ thuộc |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| **S0** | **`R16(d)`** — rule chặn "lệnh qc-* trỏ vào skill không tồn tại" | không · **làm ngay** |
|
|
323
|
+
| **S1** | Dựng `.pin.json` cho snapshot hiện tại (`596149f`) + hash 82 file | không |
|
|
324
|
+
| **S2** | Dựng `bin/qc-base-map.json` — 82 entry. Phần lớn là `undecided`, 5 là `ported` (4 analyst + test-plan), phần còn lại phân loại dần | S1 |
|
|
325
|
+
| **S3** | `bin/qc-sync.js` — `--status` + `--verify` trước (read-only, rẻ, có giá trị ngay) | S1, S2 |
|
|
326
|
+
| **S4** | `R16(a)(b)(c)` vào `self-check.js` | S2 |
|
|
327
|
+
| **S5** | `--pull` + `--diff` (vẫn read-only với framework) | S3 |
|
|
328
|
+
| **S6** | `--adopt` — rồi dùng chính nó chạy **P1** của plan kia (18 skill analyst) | S5 |
|
|
329
|
+
| **S7** | `--port` — merge ba chiều. Phần khó nhất, làm cuối | S6 |
|
|
330
|
+
|
|
331
|
+
> **Thứ tự này có chủ ý:** mọi thứ read-only xong trước, ghi-file để sau cùng.
|
|
332
|
+
> `--status` + `--verify` đã trả giá trị (biết mình đang lệch bao nhiêu) trước khi
|
|
333
|
+
> viết một dòng logic merge nào.
|
|
334
|
+
|
|
335
|
+
Và `--adopt` (S6) đứng **trước** `--port` (S7) vì `--adopt` là việc phải làm ngay
|
|
336
|
+
(18 skill đang chờ), còn `--port` chỉ cần khi QC push lần sửa đầu tiên.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## 8 · Câu chưa quyết
|
|
341
|
+
|
|
342
|
+
| # | Câu | Ghi chú |
|
|
343
|
+
|---|---|---|
|
|
344
|
+
| 1 | `upstream/qc-base/` **track git hay không?** | Nên track — cần nó làm BASE và cần diff được qua lịch sử. Đổi lại: +10k dòng vào repo. |
|
|
345
|
+
| 2 | Snapshot lấy bằng **copy tay** hay **git submodule**? | Submodule đúng bản chất hơn nhưng `--pull` phức tạp hơn, và submodule *là* moving target trong khi BASE cần đóng băng. Copy + pin đơn giản hơn, tôi nghiêng về nó. |
|
|
346
|
+
| 3 | `bin/qc-base-map.json` hay gộp vào `bin/trace-schema.json`? | Tách riêng: đây không phải contract về trace. Nhưng `self-check.js` phải đọc cả hai. |
|
|
347
|
+
| 4 | 2 file `.py` của `qa-gate` xử sao? | `state: undecided`. Framework QC hiện 100% markdown-first — nhận script là đổi một nguyên tắc. |
|
|
348
|
+
| 5 | `ported_from: ai-automation-qc-base` đang **sai tên repo** (thật là `ui-automation-testing`) | Sửa luôn khi thêm `upstream_path` + `upstream_sha` vào frontmatter. |
|
|
349
|
+
| 6 | `shared/` của upstream — framework có nhận khái niệm này? | `qa-tc-designer/shared/` 7 file dùng chung. Framework hiện "mỗi skill tự đủ". Nhận `shared/` là đổi kiến trúc skill. |
|
|
350
|
+
| 7 | `read-doc-gap-inputs.md` tồn tại **2 bản khác nội dung** (designer + reviewer) | Trùng tên khác ruột — chọn sai là sai im lặng. Phải chốt 1 bản, hoặc map thành 2 entry riêng. |
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Liên quan
|
|
355
|
+
|
|
356
|
+
- [`qc-implementation-log.md`](qc-implementation-log.md) — nhật ký triển khai, viết cho người không cần biết code
|
|
357
|
+
|
|
358
|
+
- [`qc-merge-plan.md`](qc-merge-plan.md) — gộp cái gì, ba quyết định đang chặn (§5), plan P0–P4
|
|
359
|
+
- `--adopt` (S6 ở đây) **chính là** P1 của plan kia — không phải hai việc, là một việc có công cụ
|
package/package.json
CHANGED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* migrate-qc-docs — move per-UC QC working docs into the per-PRD layout.
|
|
4
|
+
*
|
|
5
|
+
* docs/{TICKET-ID}-UC{N}/{platform}/…
|
|
6
|
+
* → docs/{TICKET-ID}/{platform}/…
|
|
7
|
+
*
|
|
8
|
+
* The per-UC layout shattered one feature across N sibling folders (10 UCs = 10 root
|
|
9
|
+
* folders, 30 files). It was the framework's own imposition when the QC skills were
|
|
10
|
+
* ported: upstream has always been PRD-scoped — their real file is `DOC_GAP_FEAT-02-3.md`
|
|
11
|
+
* (no UC suffix), the gap-ID format `GAP-<UC>-001` only means anything when ONE file holds
|
|
12
|
+
* several UCs, and `qa-planner/test-plan.md` says "Test Plan cho một feature". See B11.
|
|
13
|
+
*
|
|
14
|
+
* WHY THIS SCRIPT DOES NOT MERGE. Collapsing N UC folders into one makes UC1's
|
|
15
|
+
* REQUIREMENT_ANALYSIS.md and UC2's collide on the same target path, and two prose
|
|
16
|
+
* documents cannot be merged mechanically. So:
|
|
17
|
+
*
|
|
18
|
+
* test-cases/* → MOVED (filenames carry <FEATURE>, they do not collide)
|
|
19
|
+
* the 3 top docs → ARCHIVED to {qc}/_archive-per-uc/{UC-ID}/{platform}/
|
|
20
|
+
* and listed as NEEDS REGENERATE
|
|
21
|
+
*
|
|
22
|
+
* Regenerating beats hand-merging: a PRD-level pass reads the PRD, design-spec and
|
|
23
|
+
* tech-doc ONCE instead of once per UC, and it sees cross-UC contradictions that N
|
|
24
|
+
* separate passes structurally cannot. Nothing is deleted — the archive is the way back,
|
|
25
|
+
* and the only way to check whether the PRD-level run caught anything the old ones missed.
|
|
26
|
+
*
|
|
27
|
+
* Also folds in the pending `DOC_GAPS.md` → `DOC_GAP.md` rename (B9), in the same pass,
|
|
28
|
+
* including for folders that are ALREADY in the new layout. Without it `/qc-plan` reports
|
|
29
|
+
* "DOC_GAP not found" while the analysis sits right there under the old name.
|
|
30
|
+
*
|
|
31
|
+
* Reports (does NOT guess) two conditions that need a human:
|
|
32
|
+
* - NO PLATFORM : files sit directly under {qc}/{UC-ID}/ with no platform level. The
|
|
33
|
+
* platform cannot be inferred from a QC artifact, so they are left alone.
|
|
34
|
+
* - OCCUPIED : the target path already holds a file — moving would overwrite it.
|
|
35
|
+
*
|
|
36
|
+
* DRY-RUN by default (prints the plan, changes nothing). Pass --apply to execute.
|
|
37
|
+
* Tracked files move with `git mv` to preserve history; the rest with fs.rename.
|
|
38
|
+
*
|
|
39
|
+
* Usage (from the consumer project root):
|
|
40
|
+
* node scripts/migrate-qc-docs.js # dry-run, prints plan
|
|
41
|
+
* node scripts/migrate-qc-docs.js --apply # execute
|
|
42
|
+
* node scripts/migrate-qc-docs.js --qc docs --root .
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
'use strict';
|
|
46
|
+
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
const path = require('path');
|
|
49
|
+
const { execSync } = require('child_process');
|
|
50
|
+
|
|
51
|
+
// ── args ──────────────────────────────────────────────────────────────────────
|
|
52
|
+
const argv = process.argv.slice(2);
|
|
53
|
+
const has = f => argv.includes(f);
|
|
54
|
+
const flag = (f, d) => { const i = argv.indexOf(f); return i !== -1 ? argv[i + 1] : d; };
|
|
55
|
+
|
|
56
|
+
const APPLY = has('--apply');
|
|
57
|
+
const ROOT = path.resolve(flag('--root', '.'));
|
|
58
|
+
const QC = flag('--qc', 'docs');
|
|
59
|
+
|
|
60
|
+
const qcAbs = path.join(ROOT, QC);
|
|
61
|
+
const ARCHIVE = '_archive-per-uc';
|
|
62
|
+
|
|
63
|
+
// The three PRD-level documents. Anything else at that level is archived too (safe
|
|
64
|
+
// default) but called out separately so nobody loses a file without seeing its name.
|
|
65
|
+
const TOP_DOCS = ['REQUIREMENT_ANALYSIS.md', 'DOC_GAP.md', 'DOC_GAPS.md', 'TEST_PLAN.md'];
|
|
66
|
+
|
|
67
|
+
// `{TICKET-ID}-UC{N}` — the framework's UC-ID contract (steps/gate.md Bước 1: TICKET-ID is
|
|
68
|
+
// the part before `-UC`). Anchored at both ends so `FEAT-01-2-UC1` matches but a folder
|
|
69
|
+
// that merely contains the letters "uc" does not.
|
|
70
|
+
const UC_DIR = /^(.+)-UC(\d+)$/i;
|
|
71
|
+
|
|
72
|
+
// ── git tracked set (for `git mv`) ────────────────────────────────────────────
|
|
73
|
+
let tracked = null;
|
|
74
|
+
try {
|
|
75
|
+
const out = execSync('git ls-files', { cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
76
|
+
tracked = new Set(out.split('\n').filter(Boolean).map(p => p.replace(/\\/g, '/')));
|
|
77
|
+
} catch { tracked = null; }
|
|
78
|
+
|
|
79
|
+
const rel = abs => path.relative(ROOT, abs).replace(/\\/g, '/');
|
|
80
|
+
const isTracked = abs => !!(tracked && tracked.has(rel(abs)));
|
|
81
|
+
const isDir = p => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
|
|
82
|
+
|
|
83
|
+
function walk(dir) {
|
|
84
|
+
let out = [];
|
|
85
|
+
if (!isDir(dir)) return out;
|
|
86
|
+
for (const n of fs.readdirSync(dir)) {
|
|
87
|
+
const p = path.join(dir, n);
|
|
88
|
+
out = out.concat(isDir(p) ? walk(p) : [p]);
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── plan ──────────────────────────────────────────────────────────────────────
|
|
94
|
+
const moves = []; // { from, to, kind }
|
|
95
|
+
const noPlatform = []; // { ucDir, files }
|
|
96
|
+
const occupied = []; // { from, to }
|
|
97
|
+
const regen = new Map();// "TICKET|platform" → { ticket, platform, ucs:Set }
|
|
98
|
+
|
|
99
|
+
if (!isDir(qcAbs)) {
|
|
100
|
+
console.log(`\n${QC}/ does not exist under ${ROOT} — nothing to migrate.\n`);
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ucDirs = fs.readdirSync(qcAbs)
|
|
105
|
+
.filter(n => n !== ARCHIVE)
|
|
106
|
+
.map(n => ({ name: n, abs: path.join(qcAbs, n) }))
|
|
107
|
+
.filter(d => isDir(d.abs) && UC_DIR.test(d.name));
|
|
108
|
+
|
|
109
|
+
function plan(from, to, kind) {
|
|
110
|
+
// Never overwrite, and never let two sources land on one target.
|
|
111
|
+
if (fs.existsSync(to) || moves.some(m => m.to === to)) { occupied.push({ from, to }); return; }
|
|
112
|
+
moves.push({ from, to, kind });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const d of ucDirs) {
|
|
116
|
+
const m = UC_DIR.exec(d.name);
|
|
117
|
+
const ticket = m[1];
|
|
118
|
+
const ucNum = m[2];
|
|
119
|
+
const ucId = d.name;
|
|
120
|
+
|
|
121
|
+
// Files sitting directly under {qc}/{UC-ID}/ mean the platform level is absent. A QC
|
|
122
|
+
// artifact carries no @trace.platform, so there is nothing to infer it from — leave them.
|
|
123
|
+
const loose = fs.readdirSync(d.abs).filter(x => !isDir(path.join(d.abs, x)));
|
|
124
|
+
if (loose.length) noPlatform.push({ ucDir: ucId, files: loose });
|
|
125
|
+
|
|
126
|
+
for (const platform of fs.readdirSync(d.abs).filter(x => isDir(path.join(d.abs, x)))) {
|
|
127
|
+
const src = path.join(d.abs, platform);
|
|
128
|
+
|
|
129
|
+
const key = `${ticket}|${platform}`;
|
|
130
|
+
if (!regen.has(key)) regen.set(key, { ticket, platform, ucs: new Set() });
|
|
131
|
+
regen.get(key).ucs.add(`UC${ucNum}`);
|
|
132
|
+
|
|
133
|
+
for (const abs of walk(src)) {
|
|
134
|
+
const within = path.relative(src, abs).replace(/\\/g, '/');
|
|
135
|
+
const base = path.basename(abs);
|
|
136
|
+
|
|
137
|
+
if (within.startsWith('test-cases/')) {
|
|
138
|
+
// Survives the merge — the filename carries <FEATURE>, and the Trace SC column
|
|
139
|
+
// is what says which UC a TC belongs to.
|
|
140
|
+
plan(abs, path.join(qcAbs, ticket, platform, within), 'test-case');
|
|
141
|
+
} else {
|
|
142
|
+
// Archive, renaming DOC_GAPS.md → DOC_GAP.md so the archive uses one name.
|
|
143
|
+
const outName = base === 'DOC_GAPS.md' ? 'DOC_GAP.md' : base;
|
|
144
|
+
const outRel = path.join(path.dirname(within), outName);
|
|
145
|
+
plan(abs, path.join(qcAbs, ARCHIVE, ucId, platform, outRel),
|
|
146
|
+
TOP_DOCS.includes(base) ? 'archive' : 'archive-other');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── second pass — DOC_GAPS.md → DOC_GAP.md for folders ALREADY in the new layout ──
|
|
153
|
+
for (const abs of walk(qcAbs)) {
|
|
154
|
+
if (path.basename(abs) !== 'DOC_GAPS.md') continue;
|
|
155
|
+
if (rel(abs).includes(`/${ARCHIVE}/`)) continue;
|
|
156
|
+
if (moves.some(mv => mv.from === abs)) continue; // already handled above
|
|
157
|
+
plan(abs, path.join(path.dirname(abs), 'DOC_GAP.md'), 'rename');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── report ────────────────────────────────────────────────────────────────────
|
|
161
|
+
const pad = (s, n) => s + ' '.repeat(Math.max(0, n - s.length));
|
|
162
|
+
console.log('');
|
|
163
|
+
console.log('╔════════════════════════════════════════════════╗');
|
|
164
|
+
console.log(`║ migrate-qc-docs — ${pad(APPLY ? 'APPLY' : 'DRY RUN', 25)}║`);
|
|
165
|
+
console.log('╚════════════════════════════════════════════════╝');
|
|
166
|
+
console.log(`Root : ${ROOT}`);
|
|
167
|
+
console.log(`QC : ${QC}/ Git: ${tracked ? 'yes (git mv)' : 'no (fs move)'}`);
|
|
168
|
+
console.log('');
|
|
169
|
+
|
|
170
|
+
if (!moves.length && !noPlatform.length && !occupied.length) {
|
|
171
|
+
console.log('Nothing to migrate — no per-UC folder found, and no DOC_GAPS.md to rename.');
|
|
172
|
+
console.log(`(Scanned ${QC}/ for folders matching {TICKET-ID}-UC{N}.)`);
|
|
173
|
+
console.log('');
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const byKind = k => moves.filter(m => m.kind === k);
|
|
178
|
+
const show = (title, list) => {
|
|
179
|
+
if (!list.length) return;
|
|
180
|
+
console.log(`${title} (${list.length})`);
|
|
181
|
+
for (const m of list) console.log(` ${rel(m.from)}\n -> ${rel(m.to)}`);
|
|
182
|
+
console.log('');
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
show('📦 MOVE — test cases (survive the merge)', byKind('test-case'));
|
|
186
|
+
show('🗄 ARCHIVE — the 3 PRD-level docs (regenerate instead of merging)', byKind('archive'));
|
|
187
|
+
show('🗄 ARCHIVE — other files found at that level', byKind('archive-other'));
|
|
188
|
+
show('✏️ RENAME — DOC_GAPS.md -> DOC_GAP.md (B9, already-new layout)', byKind('rename'));
|
|
189
|
+
|
|
190
|
+
if (noPlatform.length) {
|
|
191
|
+
console.log(`⚠️ NO PLATFORM — left in place (${noPlatform.length})`);
|
|
192
|
+
console.log(' Files sit directly under the UC folder with no web/app/system level.');
|
|
193
|
+
console.log(' A QC artifact carries no @trace.platform, so the platform cannot be');
|
|
194
|
+
console.log(' inferred — move these by hand into the right platform folder.');
|
|
195
|
+
for (const x of noPlatform) console.log(` ${QC}/${x.ucDir}/ -> ${x.files.join(', ')}`);
|
|
196
|
+
console.log('');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (occupied.length) {
|
|
200
|
+
console.log(`❌ OCCUPIED — NOT moved, target already exists (${occupied.length})`);
|
|
201
|
+
console.log(' Moving would overwrite. Resolve by hand, then re-run.');
|
|
202
|
+
for (const x of occupied) console.log(` ${rel(x.from)}\n x ${rel(x.to)}`);
|
|
203
|
+
console.log('');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (regen.size) {
|
|
207
|
+
console.log('🔄 NEEDS REGENERATE — run these after the move:');
|
|
208
|
+
for (const r of regen.values()) {
|
|
209
|
+
console.log(` /qc-analyze ${r.ticket} ${r.platform} <- was ${[...r.ucs].sort().join(' + ')}`);
|
|
210
|
+
console.log(` /qc-plan ${r.ticket} ${r.platform}`);
|
|
211
|
+
}
|
|
212
|
+
console.log('');
|
|
213
|
+
console.log(' The archived per-UC docs are the way back, and the only way to check');
|
|
214
|
+
console.log(' whether the PRD-level run caught cross-UC contradictions the old ones');
|
|
215
|
+
console.log(' could not see. Compare, then delete the archive when you are satisfied.');
|
|
216
|
+
console.log('');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!APPLY) {
|
|
220
|
+
console.log('DRY RUN — nothing changed. Re-run with --apply to execute.');
|
|
221
|
+
console.log('');
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── execute ───────────────────────────────────────────────────────────────────
|
|
226
|
+
function mv(from, to) {
|
|
227
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
228
|
+
if (isTracked(from)) execSync(`git mv -k "${rel(from)}" "${rel(to)}"`, { cwd: ROOT, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
229
|
+
else fs.renameSync(from, to);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let moved = 0, failed = 0;
|
|
233
|
+
for (const m of moves) {
|
|
234
|
+
try { mv(m.from, m.to); moved++; }
|
|
235
|
+
catch (err) {
|
|
236
|
+
console.log(` ❌ ${rel(m.from)} — ${err.message.split('\n')[0]}`);
|
|
237
|
+
failed++;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Prune folders the move emptied — deepest first, and only if genuinely empty.
|
|
242
|
+
let pruned = 0;
|
|
243
|
+
const dirsDeepFirst = [];
|
|
244
|
+
(function collect(dir) {
|
|
245
|
+
if (!isDir(dir)) return;
|
|
246
|
+
for (const n of fs.readdirSync(dir)) collect(path.join(dir, n));
|
|
247
|
+
dirsDeepFirst.push(dir);
|
|
248
|
+
})(qcAbs);
|
|
249
|
+
for (const d of dirsDeepFirst) {
|
|
250
|
+
if (d === qcAbs) continue;
|
|
251
|
+
try { if (fs.readdirSync(d).length === 0) { fs.rmdirSync(d); pruned++; } } catch { /* keep */ }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(`✅ Moved ${moved} file(s)${failed ? `, ${failed} FAILED` : ''}.`);
|
|
255
|
+
console.log(`✅ Pruned ${pruned} empty folder(s).`);
|
|
256
|
+
console.log('');
|
|
257
|
+
console.log('Next:');
|
|
258
|
+
console.log(' 1. git status — review the moves (nothing was deleted)');
|
|
259
|
+
console.log(' 2. Run the /qc-analyze + /qc-plan commands listed above');
|
|
260
|
+
console.log(` 3. Compare against ${QC}/${ARCHIVE}/, then remove the archive when satisfied`);
|
|
261
|
+
console.log('');
|