@educa-corp/sdd-framework 0.9.2 → 0.9.4
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/build.js +11 -0
- package/bin/qc-base-map.json +119 -49
- package/bin/self-check.js +30 -0
- package/core/FRAMEWORK_VERSION +1 -1
- package/core/commands/qc-analyze.md +85 -75
- package/core/commands/qc-design-test.md +144 -25
- package/core/commands/qc-plan.md +40 -7
- package/core/commands/qc-review.md +74 -7
- package/core/commands/qc-run-test.md +21 -1
- package/core/commands/setup-ai-first.md +5 -5
- package/core/commands/update-framework.md +1 -1
- package/core/commands/validate-traces.md +1 -1
- package/core/modules/qc-playwright/stack-profile.yaml +3 -3
- package/core/rules/workflow.md +1 -1
- package/core/skills/qc/qa-analyst/DOC_GAP.template.md +47 -17
- package/core/skills/qc/qa-analyst/acceptance-criteria.md +1 -1
- package/core/skills/qc/qa-analyst/business-rules.md +2 -2
- package/core/skills/qc/qa-analyst/data-flow.md +2 -2
- package/core/skills/qc/qa-analyst/spec-breakdown.md +4 -4
- package/core/skills/qc/qa-analyst/spec-issue-reporter.md +14 -2
- package/core/skills/qc/qa-designer/api/auth-chain.md +155 -0
- package/core/skills/qc/qa-designer/api/auth-sequence.md +75 -0
- package/core/skills/qc/qa-designer/api/common-headers.md +61 -0
- package/core/skills/qc/qa-designer/api/crud-sequence.md +122 -0
- package/core/skills/qc/qa-designer/api/endpoint.md +231 -0
- package/core/skills/qc/qa-designer/api/http-status-codes.md +102 -0
- package/core/skills/qc/qa-designer/e2e/journey.md +13 -8
- package/core/skills/qc/qa-designer/exploratory/charter.md +2 -0
- package/core/skills/qc/qa-designer/exploratory/explore-to-functional.md +7 -4
- package/core/skills/qc/qa-designer/functional/api.md +87 -18
- package/core/skills/qc/qa-designer/functional/gui-feature.md +12 -9
- package/core/skills/qc/qa-designer/functional/gui-screen.md +12 -10
- package/core/skills/qc/qa-designer/integration/api.md +12 -5
- package/core/skills/qc/qa-designer/integration/db.md +12 -6
- package/core/skills/qc/qa-designer/integration/gui.md +12 -5
- package/core/skills/qc/qa-designer/integration/kafka.md +12 -5
- package/core/skills/qc/qa-designer/non-functional.md +12 -5
- package/core/skills/qc/qa-designer/shared/action-keywords-glossary.md +91 -0
- package/core/skills/qc/qa-designer/shared/duplicate-check-procedure.md +105 -0
- package/core/skills/qc/qa-designer/shared/implicit-scenarios.md +22 -0
- package/core/skills/qc/qa-designer/shared/precision-rules.md +198 -0
- package/core/skills/qc/qa-designer/shared/read-doc-gap-inputs.md +25 -0
- package/core/skills/qc/qa-designer/shared/skill-decision-tree.md +93 -0
- package/core/skills/qc/qa-designer/shared/tc-metadata-format.md +243 -0
- package/core/skills/qc/qa-planner/risk-model.md +1 -1
- package/core/skills/qc/qa-planner/test-plan.md +24 -13
- package/core/skills/qc/qa-reviewer/script/e2e.md +9 -1
- package/core/skills/qc/qa-reviewer/script/exploratory.md +9 -1
- package/core/skills/qc/qa-reviewer/script/functional.md +9 -1
- package/core/skills/qc/qa-reviewer/script/integration.md +9 -1
- package/core/skills/qc/qa-reviewer/script/non-functional.md +9 -1
- package/core/skills/qc/qa-reviewer/shared/read-doc-gap-inputs.md +26 -0
- package/core/skills/qc/qa-reviewer/shared/review-check-groups.md +207 -0
- package/core/skills/qc/qa-reviewer/shared/review-file-template.md +228 -0
- package/core/skills/qc/qa-reviewer/test-case/e2e.md +71 -13
- package/core/skills/qc/qa-reviewer/test-case/exploratory.md +53 -4
- package/core/skills/qc/qa-reviewer/test-case/functional.md +63 -15
- package/core/skills/qc/qa-reviewer/test-case/integration.md +64 -12
- package/core/skills/qc/qa-reviewer/test-case/non-functional.md +72 -13
- package/core/skills/qc/qa-runner/e2e.md +1 -1
- package/core/skills/qc/qa-runner/exploratory/session.md +1 -1
- package/core/steps/context-loader.md +1 -1
- package/core/steps/qc-scope.md +119 -0
- package/core/templates/project-context.yaml +3 -1
- package/docs/02-concepts/pipeline-steps/08-qc-automation.md +1 -1
- package/docs/02-concepts/pipeline-steps/09-validate-traces.md +1 -1
- package/docs/04-reference/configuration.md +146 -146
- package/docs/04-reference/trace-schema.md +1 -1
- package/docs/explain/00-setup-ai-first.md +1 -1
- package/docs/explain/15-qc-analyze.md +1 -1
- package/docs/explain/16-qc-plan.md +1 -1
- package/docs/explain/17-qc-design-test.md +1 -1
- package/docs/plans/qc-implementation-log.md +288 -5
- package/docs/plans/qc-sync-command.md +2 -1
- package/package.json +1 -1
- package/scripts/migrate-qc-docs.js +261 -0
|
@@ -1373,6 +1373,287 @@ Lăng kính này phân biệt được. Hiện **không ai làm**.
|
|
|
1373
1373
|
|
|
1374
1374
|
---
|
|
1375
1375
|
|
|
1376
|
+
# ✅ B11 — Gộp tài liệu QC về cấp PRD (thôi chia nhỏ theo từng UC)
|
|
1377
|
+
|
|
1378
|
+
**Việc này khởi từ một ảnh chụp thư mục.** Đội QC chạy trạm phân tích và nhận về một cây vỡ
|
|
1379
|
+
vụn: mỗi UC một thư mục gốc riêng — 10 UC là 10 thư mục, 30 file, cùng một tính năng.
|
|
1380
|
+
|
|
1381
|
+
## Điều làm tôi phải xem lại kết luận của chính mình
|
|
1382
|
+
|
|
1383
|
+
Câu hỏi ban đầu tôi hiểu là *"xếp lại thư mục cho gọn"*. Nó không phải. Nó là **đổi độ mịn của
|
|
1384
|
+
chính tài liệu**: một file gap cho cả tính năng, không phải một file mỗi UC.
|
|
1385
|
+
|
|
1386
|
+
Và khi đi kiểm, hoá ra **framework đang làm sai, không phải đội QC**. Ba bằng chứng độc lập,
|
|
1387
|
+
đều nằm sẵn trong repo từ trước:
|
|
1388
|
+
|
|
1389
|
+
| # | Bằng chứng | Nó nói gì |
|
|
1390
|
+
|---|---|---|
|
|
1391
|
+
| 1 | File thật của đội QC tên `DOC_GAP_FEAT-02-3.md` | Mã tính năng, **không có** hậu tố UC. Họ vốn làm ở cấp tính năng |
|
|
1392
|
+
| 2 | Mã gap trong template gốc là `GAP-<UC>-001` | Mã **mang tên UC** chỉ có nghĩa khi **một file chứa nhiều UC**. Trong file một-UC nó là thừa |
|
|
1393
|
+
| 3 | Kỹ năng lập kế hoạch test viết *"Test Plan cho một **feature**"*, khuôn là `# Test Plan – <Feature>` | Cấp tính năng, từ đầu |
|
|
1394
|
+
|
|
1395
|
+
Cả ba đều chỉ về cấp tính năng. **Việc chia nhỏ theo UC là framework tự áp vào lúc port** —
|
|
1396
|
+
không ai yêu cầu, và nó phá đúng cái mà mã `GAP-<UC>-…` được thiết kế để làm.
|
|
1397
|
+
|
|
1398
|
+
Thêm một chỗ bất nhất đã tồn tại: **sổ kết quả kiểm thử vốn đã gom theo tính năng**
|
|
1399
|
+
(`.trace/{domain}/{tính-năng}/…`). Nên trước B11, hai nơi cùng nói về một UC lại xếp theo hai
|
|
1400
|
+
kiểu khác nhau.
|
|
1401
|
+
|
|
1402
|
+
## Ba thứ đổi
|
|
1403
|
+
|
|
1404
|
+
```
|
|
1405
|
+
TRƯỚC SAU
|
|
1406
|
+
docs/FEAT-01-2-UC1/web/DOC_GAP.md docs/FEAT-01-2/web/DOC_GAP.md
|
|
1407
|
+
docs/FEAT-01-2-UC2/web/DOC_GAP.md ← UC1…UC5 chung một bảng,
|
|
1408
|
+
docs/FEAT-01-2-UC3/web/DOC_GAP.md phân biệt bằng cột "UC"
|
|
1409
|
+
docs/FEAT-01-2-UC4/web/DOC_GAP.md docs/FEAT-01-2/web/TEST_PLAN.md
|
|
1410
|
+
docs/FEAT-01-2-UC5/web/DOC_GAP.md docs/FEAT-01-2/web/REQUIREMENT_ANALYSIS.md
|
|
1411
|
+
(× 3 loại tài liệu = 15 file) (3 file)
|
|
1412
|
+
```
|
|
1413
|
+
|
|
1414
|
+
**Nền vẫn là một cấp thư mục — bắt buộc.** Một kịch bản `SC3` của web và `SC3` của app là hai
|
|
1415
|
+
thứ khác nhau, và sổ kết quả tách theo nền. Gộp nền lại là trộn hai bộ kịch bản.
|
|
1416
|
+
|
|
1417
|
+
## Chỗ tôi tưởng phải trả giá, mà hoá ra được lãi
|
|
1418
|
+
|
|
1419
|
+
Tôi tưởng gộp 5 UC vào một lần chạy sẽ tốn gấp 5 lần. **Ngược lại.**
|
|
1420
|
+
|
|
1421
|
+
Chạy từng UC thì tài liệu yêu cầu, bản thiết kế, tài liệu kỹ thuật **bị đọc lại mỗi lần** —
|
|
1422
|
+
5 UC là đọc 5 lượt cùng một tài liệu. Phần dùng chung chiếm đa số đầu vào; chỉ phần kịch bản
|
|
1423
|
+
test là riêng theo UC.
|
|
1424
|
+
|
|
1425
|
+
Và có lợi thêm về **chất**, không chỉ chi phí: **chỗ hai UC của cùng tính năng nói khác nhau**
|
|
1426
|
+
— UC1 một kiểu, UC3 kiểu khác — chỉ nhìn thấy được khi đọc cùng lúc. Chạy tách UC thì về
|
|
1427
|
+
**cấu trúc** là không thể thấy, không phải "khó thấy". Nên trạm phân tích giờ có thêm một mục
|
|
1428
|
+
bắt buộc: *"Mâu thuẫn chéo UC"*.
|
|
1429
|
+
|
|
1430
|
+
## Chỗ nguy hiểm — và vì sao nó đã được giải sẵn
|
|
1431
|
+
|
|
1432
|
+
Gộp nhiều UC vào một bảng thì mã gap có thể đè nhau. Nếu đánh số liền `GAP-01…GAP-60` thì
|
|
1433
|
+
**chạy lại UC1 sẽ đánh số lại toàn bộ UC2–UC5** — mà khoảng 15 kỹ năng đang ghi
|
|
1434
|
+
`🚫 Chặn bởi: GAP-xx` vào từng test case. Test case sẽ trỏ sang gap khác, **im lặng, không ai
|
|
1435
|
+
báo lỗi**.
|
|
1436
|
+
|
|
1437
|
+
Điểm hay: **template gốc đã giải sẵn** — mã của nó là `GAP-<UC>-001`, mang tên UC. Chạy lại UC1
|
|
1438
|
+
không đụng số của UC khác. B11 chỉ **định nghĩa rõ** thành `GAP-UC1-001`, và thêm `GAP-GEN-001`
|
|
1439
|
+
cho gap thuộc cả tính năng.
|
|
1440
|
+
|
|
1441
|
+
*(Kết quả đội QC chạy hôm trước ghi `GAP-01` phẳng — tức nó **không theo template**. Đó là một
|
|
1442
|
+
vấn đề khác, xem §Cố ý chưa làm.)*
|
|
1443
|
+
|
|
1444
|
+
## Một luật, một chỗ
|
|
1445
|
+
|
|
1446
|
+
Luật *"xác định tính năng nào, nền nào, những UC nào"* trước đây **bị chép ở 5 lệnh** và câu chữ
|
|
1447
|
+
đã lệch nhau. Năm bản của một luật là nơi lỗi sống: sửa bốn, quên một, và trạm bị quên ghi tài
|
|
1448
|
+
liệu vào sai thư mục **mà không báo gì**.
|
|
1449
|
+
|
|
1450
|
+
Giờ nó là **một file dùng chung**, cả 6 trạm cùng nạp. Đổi bố cục lần này buộc phải sửa cả 5
|
|
1451
|
+
bản dù sao — nên dồn về một chỗ là rẻ hơn, không đắt hơn.
|
|
1452
|
+
|
|
1453
|
+
## Trạm nào chạy cả tính năng, trạm nào vẫn theo UC
|
|
1454
|
+
|
|
1455
|
+
| Trạm | Phạm vi | Vì sao |
|
|
1456
|
+
|---|---|---|
|
|
1457
|
+
| Phân tích yêu cầu · Lập kế hoạch test | **cả tính năng × 1 nền** | Chúng sinh ra 3 tài liệu cần gộp |
|
|
1458
|
+
| Thiết kế test · Soát · Chạy test | **vẫn theo từng UC** | Thiết kế và chạy test **thật sự** làm tăng dần theo UC. Giữ được việc làm UC1 khi UC3 chưa xong là đúng — chỉ **chỗ đọc/ghi** đổi |
|
|
1459
|
+
| Sổ kết quả kiểm thử | **không đổi** | Mỗi hàng là một kịch bản; liên kết với bảng gap đi qua cột `UC` |
|
|
1460
|
+
|
|
1461
|
+
**Gap chặn theo từng UC, không chặn cả tính năng.** Một gap nghiêm trọng ở UC3 không có lý do
|
|
1462
|
+
gì dừng việc thiết kế test cho UC1.
|
|
1463
|
+
|
|
1464
|
+
## UC chưa duyệt: ghi "chưa xét", không im lặng bỏ
|
|
1465
|
+
|
|
1466
|
+
Trạm phân tích chỉ lấy UC có kịch bản test **đã duyệt**. UC còn nháp **vẫn có một hàng** trong
|
|
1467
|
+
bảng *Phạm vi phân tích*, đánh dấu `⏸ Chưa xét` kèm lý do.
|
|
1468
|
+
|
|
1469
|
+
> **Vì sao không bỏ khỏi bảng.** *"Chưa xét"* khác *"đã xét, không thấy gap"*. Bỏ khỏi bảng là
|
|
1470
|
+
> để hai chuyện đó trông giống nhau — và một UC bị bỏ sót sẽ trông như một UC sạch.
|
|
1471
|
+
|
|
1472
|
+
Giữ lại cờ `--include-draft` cho ai cố ý chạy sớm trên bản nháp. Cách dùng đó **vốn được cho
|
|
1473
|
+
phép** (cổng cũ là cảnh báo mềm, không phải chặn cứng); bỏ hẳn là lấy đi một năng lực đang có
|
|
1474
|
+
mà không ai khai.
|
|
1475
|
+
|
|
1476
|
+
## Dữ liệu cũ của đội QC: lưu trữ, không gộp tay
|
|
1477
|
+
|
|
1478
|
+
Gộp 5 thư mục về 1 thì tài liệu phân tích của UC1 và UC2 **đâm nhau ở cùng một đường dẫn** —
|
|
1479
|
+
không có cách gộp tự động hai tài liệu văn xuôi. Nên script `migrate-qc-docs.js` **không giả vờ
|
|
1480
|
+
gộp**:
|
|
1481
|
+
|
|
1482
|
+
| Loại | Xử lý |
|
|
1483
|
+
|---|---|
|
|
1484
|
+
| Test case | **dời** sang thư mục mới — tên file mang tên tính năng nên không đâm nhau |
|
|
1485
|
+
| 3 tài liệu cấp trên | **lưu trữ** vào `docs/_archive-per-uc/`, in danh sách cần sinh lại |
|
|
1486
|
+
| `DOC_GAPS.md` (tên cũ) | đổi thành `DOC_GAP.md` — trả luôn món nợ B9 còn treo |
|
|
1487
|
+
| File nằm ngoài mọi nền | **để nguyên + báo** — tài liệu QC không mang dấu nền nên không suy được |
|
|
1488
|
+
| Đích đã có file | **không ghi đè**, báo ra để người xử lý |
|
|
1489
|
+
|
|
1490
|
+
**Sinh lại tốt hơn gộp tay**, vì bản cấp tính năng thấy được mâu thuẫn chéo UC mà 5 bản rời
|
|
1491
|
+
không thấy. Không xoá gì — thư mục lưu trữ là đường lùi, và là **cách duy nhất** để đối chiếu
|
|
1492
|
+
xem bản mới có bắt thêm gap thật hay không.
|
|
1493
|
+
|
|
1494
|
+
## Đã kiểm chứng
|
|
1495
|
+
|
|
1496
|
+
| Phép kiểm | Kết quả |
|
|
1497
|
+
|---|---|
|
|
1498
|
+
| Dựng lại toàn bộ | 41/41 mẫu lệnh ✅ |
|
|
1499
|
+
| Bộ kiểm tra nội bộ (16 luật) | sạch ✅ |
|
|
1500
|
+
| Bộ test tự động | **199/199** ✅ (không tụt so với trước B11) |
|
|
1501
|
+
| Đường dẫn cũ còn sót trong mã | **0** (34 chỗ → 0) |
|
|
1502
|
+
| Luật dùng chung đã lan vào cả 6 trạm | ✅ đếm được trong bản đã đóng gói |
|
|
1503
|
+
| Script dời file — chạy thử trên cây giả lập 31 file | **31 → 31 file, không mất gì**; chạy lại lần hai không đổi gì thêm |
|
|
1504
|
+
| 5 ca biên của script (tên cũ · hai nền · file lạc · bố cục đã mới · đích bị chiếm) | đều xử lý đúng ✅ |
|
|
1505
|
+
|
|
1506
|
+
## Cố ý chưa làm — và vì sao để riêng
|
|
1507
|
+
|
|
1508
|
+
| Việc | Vì sao |
|
|
1509
|
+
|---|---|
|
|
1510
|
+
| **Máy kiểm cấu trúc file gap** | Đây mới là cách chữa thật cho bệnh *"không theo template"* (kết quả hôm trước ra 6 cột, dùng chữ `Major` — không theo template nào cả). Nhưng nó là **năng lực mới**, không phải đổi bố cục. Trộn vào thì sau này không biết cái nào chữa được bệnh gì. Làm sau khi có phép thử thật |
|
|
1511
|
+
| **`Blocker` ↔ `Critical`** | File thật của đội QC dùng `🔴 Critical`; framework đổi thành `Blocker` vì trạm chạy test đọc chữ đó. Nhìn kết quả hôm trước thì rõ là **đổi từ ngữ của họ làm tăng khả năng lệch, không giảm** — nhưng đó là quyết định riêng, một dòng, và cần bạn chốt |
|
|
1512
|
+
|
|
1513
|
+
---
|
|
1514
|
+
|
|
1515
|
+
# ✅ B12 — Rà trạm viết kịch bản kiểm thử theo bản gốc
|
|
1516
|
+
|
|
1517
|
+
Đi theo đúng thứ tự dây chuyền: xong trạm 1 và 2 (B11) thì tới trạm 3 — **trạm quyết định có
|
|
1518
|
+
test hay không có test**.
|
|
1519
|
+
|
|
1520
|
+
## Hai điều rà ra
|
|
1521
|
+
|
|
1522
|
+
**① Trạm này chưa hề được đụng trong cả đợt merge.** Bản đồ port ghi **21/21 file gốc ở trạng
|
|
1523
|
+
thái "chưa quyết"**. 11 kỹ năng đang chạy đến từ **một nguồn khác** (`lms_autotest`), không phải
|
|
1524
|
+
từ đội QC.
|
|
1525
|
+
|
|
1526
|
+
| | File | Dòng |
|
|
1527
|
+
|---|---|---|
|
|
1528
|
+
| Bản gốc đội QC | 21 | **2.509** |
|
|
1529
|
+
| Framework | 11 | **496** *(~20%)* |
|
|
1530
|
+
|
|
1531
|
+
**② Một lỗi im lặng, cùng lớp với `DOC_GAPS` / `DOC_GAP`:**
|
|
1532
|
+
|
|
1533
|
+
| Ai | Nói tên file là |
|
|
1534
|
+
|---|---|
|
|
1535
|
+
| 2 kỹ năng | `TC_<FEATURE>.md` |
|
|
1536
|
+
| 1 kỹ năng | `TC_<FEATURE>_API.md` |
|
|
1537
|
+
| **4 kỹ năng** | **không nêu tên gì cả** |
|
|
1538
|
+
| Lệnh thiết kế · lệnh chạy · lệnh soát · module Playwright *(13 chỗ)* | `*.Test.md` |
|
|
1539
|
+
|
|
1540
|
+
**Trạm thiết kế ghi ra thứ trạm chạy không tìm thấy.** Và không có gì báo lỗi — file vẫn nằm
|
|
1541
|
+
đó, trạm sau chỉ đơn giản không thấy.
|
|
1542
|
+
|
|
1543
|
+
Framework có **phương pháp** (EP/BVA/bảng quyết định, danh sách nhóm TC); bản gốc có **chi
|
|
1544
|
+
tiết** (cấm từ mơ hồ, khuôn TC, từ điển hành động, bảng tra mã HTTP). Đúng khuôn đã gặp ở B1.
|
|
1545
|
+
|
|
1546
|
+
## Bốn quyết định
|
|
1547
|
+
|
|
1548
|
+
| # | Chốt | Nghĩa cho người dùng |
|
|
1549
|
+
|---|---|---|
|
|
1550
|
+
| **1** | **Luật ATOMIC, có cả chế độ tách tối đa** | Mỗi test case đúng **một** dòng kết quả mong đợi. Nhiều điểm kiểm → tách thành nhiều TC độc lập |
|
|
1551
|
+
| **2** | **Bỏ bảng trong file test case** | Toàn bộ dạng danh sách, không ký tự `\|` — kể cả mục Tổng hợp và Trace Matrix |
|
|
1552
|
+
| **3** | **Hai file, chia theo "có qua giao diện"** | mặc định → file giao diện (6 nhóm) · `--api` → file API (2 nhóm) · `--all` → cả hai |
|
|
1553
|
+
| **4** | **Lấy trọn 7 file luật chung + cả nhánh API, nhưng VIẾT LẠI 2 file mẫu** | 1.523 dòng |
|
|
1554
|
+
|
|
1555
|
+
## Chỗ suýt tự bắn vào chân mình
|
|
1556
|
+
|
|
1557
|
+
Nhánh API của bản gốc có 2 file **test case mẫu**. Đọc kỹ thì chúng viết **trước** luật ATOMIC
|
|
1558
|
+
(đội QC chốt 2026-07-09, và văn bản ghi rõ *"ĐẢO rule cũ"*):
|
|
1559
|
+
|
|
1560
|
+
```
|
|
1561
|
+
MẪU GỐC (cũ hơn luật) ĐÃ VIẾT LẠI
|
|
1562
|
+
**Expected:** #### Expected Result
|
|
1563
|
+
- HTTP 201; body.id = x; - HTTP 201 AND body.id exists
|
|
1564
|
+
bản ghi có trong DB
|
|
1565
|
+
▲ ba kết cục nối bằng ";" → TC_002: body.<field> = <giá trị>
|
|
1566
|
+
mã: API-<FEATURE>-001 → TC_003: DB có bản ghi id = body.id
|
|
1567
|
+
▲ hệ mã khác mã: TC_<FEATURE>_NNN
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
**Lấy nguyên hai file này là dạy AI làm ngược đúng cái luật vừa chốt** — vì mẫu cụ thể luôn
|
|
1571
|
+
thắng luật trừu tượng. Đã viết lại cả hai, và kiểm bằng máy: **13/13 khối kết quả mong đợi có
|
|
1572
|
+
đúng 1 dòng**.
|
|
1573
|
+
|
|
1574
|
+
Cũng bỏ 26 dòng là **bản nháp cũ** của chính họ: một bảng tra mã lỗi 14 dòng nằm gọn trong bảng
|
|
1575
|
+
95 dòng, một file chuỗi gọi 12 dòng nằm gọn trong hai file dài hơn. Lấy cả hai là có hai bảng
|
|
1576
|
+
tra cùng một thứ, rồi chúng lệch nhau.
|
|
1577
|
+
|
|
1578
|
+
## Bốn thứ giờ mới có
|
|
1579
|
+
|
|
1580
|
+
| Luật | Nó chặn điều gì |
|
|
1581
|
+
|---|---|
|
|
1582
|
+
| **Cấm 9 cụm từ mơ hồ** | *"hiển thị đúng"* · *"hoạt động bình thường"* · *"không lỗi"* → bắt viết giá trị đo được. Phép thử: **hai QC đọc có ra cùng một kết luận đỗ/trượt không?** |
|
|
1583
|
+
| **Khuôn TC + luật ATOMIC** | Mỗi TC một kết cục; mẫu đầy đủ; 5 bẫy khi đánh số lại hàng loạt |
|
|
1584
|
+
| **Cây quyết định chọn tầng** | ~12 ví dụ phân biệt: *"dropdown có placeholder"* = giao diện, *"dropdown hiện đúng N mục từ API"* = tích hợp |
|
|
1585
|
+
| **Từ điển hành động** | Một hành động **một** từ: Click (web) / Tap (mobile) / Enter (nhập) — cấm dùng Type, Input, Fill lẫn lộn |
|
|
1586
|
+
|
|
1587
|
+
Thêm **kiểm trùng lặp** — thứ framework trước đây **không có gì cả**. Nó quan trọng hơn sau khi
|
|
1588
|
+
bật chế độ tách tối đa: nhân một TC trùng lên 5 mảnh thì thành 5 TC trùng, và không ai đếm
|
|
1589
|
+
được nữa. Nên kiểm trùng phải chạy **trước** khi tách.
|
|
1590
|
+
|
|
1591
|
+
Và B11 vừa gom mọi UC vào **một thư mục** — nên **trùng chéo UC** giờ mới thực sự nhìn thấy
|
|
1592
|
+
được, và mới thực sự xảy ra. Hai UC của cùng tính năng rất hay dùng lại một luật nghiệp vụ;
|
|
1593
|
+
gặp thì trỏ trace về UC nguồn, **không viết lại**.
|
|
1594
|
+
|
|
1595
|
+
## Chín bản của một luật → một bản
|
|
1596
|
+
|
|
1597
|
+
Khối *"Format file TC"* trước đây **bị chép ở 9 kỹ năng** và câu chữ đã lệch nhau. Giờ một
|
|
1598
|
+
bản dùng chung, 9 kỹ năng trỏ về. **Cùng bệnh 5-bản mà B11 vừa chữa** — và lần này nó không
|
|
1599
|
+
tiết kiệm dòng, nó chỉ bỏ đi khả năng chín bản nói khác nhau.
|
|
1600
|
+
|
|
1601
|
+
Cũng bỏ chữ **"hoặc"**: hai kỹ năng từng ghi *"file riêng **hoặc** gộp trong file feature"*.
|
|
1602
|
+
Chữ "hoặc" nghĩa là AI tự chọn mỗi lần một kiểu — nên hai lần chạy cùng một tính năng có thể ra
|
|
1603
|
+
hai cấu trúc thư mục khác nhau.
|
|
1604
|
+
|
|
1605
|
+
## Hai lần bị máy bắt lỗi — và đó là điểm sáng
|
|
1606
|
+
|
|
1607
|
+
**Lần 1 — self-check chặn.** Tôi dồn phần `@trace.testid_attr` sang file dùng chung, làm lệnh
|
|
1608
|
+
không còn nhắc tag nữa. Rule R3 đỏ ngay: *"schema khai `qc-design-test` là consumer nhưng lệnh
|
|
1609
|
+
KHÔNG nhắc tới nó"*. Đúng — tôi gộp quá tay. Đã trả lại ở lệnh.
|
|
1610
|
+
|
|
1611
|
+
**Lần 2 — bộ test chặn.** `qc-scope.md` (B11) được 5 lệnh nạp, nên nó bị nướng 5 bản vào 5 file
|
|
1612
|
+
lệnh. Cộng thêm phần B12, tổng vượt ngưỡng: **1203 KB / ngưỡng 1200**. Framework đã có cơ chế
|
|
1613
|
+
cho đúng ca này (đọc-lúc-chạy thay vì nướng vào), chỉ là chưa dùng cho file đó. Chuyển xong:
|
|
1614
|
+
**1174 KB**.
|
|
1615
|
+
|
|
1616
|
+
> Cả hai lỗi đều là **lỗi của tôi**, và **cả hai đều do máy bắt, không phải do tôi đọc lại**.
|
|
1617
|
+
> Đây chính là lập luận cho mục còn treo 2b: thứ có máy canh thì không đi lệch.
|
|
1618
|
+
|
|
1619
|
+
## Bịt một điểm mù của chính bộ máy canh
|
|
1620
|
+
|
|
1621
|
+
Cập nhật bản đồ port xong, self-check **xanh** — nhưng `functional/api.md` nhận nội dung port mà
|
|
1622
|
+
**không đóng dấu nguồn gốc** nào. Đọc lại luật thì rõ vì sao: nó so dấu với bản đồ **khi file có
|
|
1623
|
+
khai**. File không khai gì thì nó im lặng.
|
|
1624
|
+
|
|
1625
|
+
Đã thêm một nhánh mới: **target đã port thì PHẢI khai** nguồn gốc, không chỉ "khai đúng nếu có
|
|
1626
|
+
khai". Và **thử cháy thật** cả hai góc (bỏ cả hai dòng · bỏ một dòng) — kêu đúng cả hai lần.
|
|
1627
|
+
|
|
1628
|
+
Mất dấu nguồn gốc ở file đích nghĩa là: bản đồ biết file này port từ đâu, còn **người mở file
|
|
1629
|
+
thì không** — và lần đồng bộ sau, mọi thứ thành đoán.
|
|
1630
|
+
|
|
1631
|
+
## Đã kiểm chứng
|
|
1632
|
+
|
|
1633
|
+
| Phép kiểm | Kết quả |
|
|
1634
|
+
|---|---|
|
|
1635
|
+
| Dựng lại toàn bộ | 41/41 ✅ |
|
|
1636
|
+
| Bộ kiểm tra nội bộ | sạch (giờ có **17** nhánh luật) ✅ |
|
|
1637
|
+
| Bộ test tự động | **199/199** ✅ |
|
|
1638
|
+
| Tổng kích thước lệnh | 1174 KB / ngưỡng 1200 ✅ |
|
|
1639
|
+
| Tên file cũ còn sót | **0** |
|
|
1640
|
+
| Lệnh "in bảng" trong kỹ năng thiết kế | **0** |
|
|
1641
|
+
| Dấu vết mẫu cũ trong nhánh API | **0** |
|
|
1642
|
+
| Khối kết quả mong đợi đúng 1 dòng | **13/13** |
|
|
1643
|
+
| Entry "chưa quyết" của trạm này | **21 → 0** |
|
|
1644
|
+
| Target đã port có đủ dấu nguồn gốc | **23/23** |
|
|
1645
|
+
| File mới tới đủ ba tầng (nguồn → đóng gói → đang chạy) | **13/13/13** |
|
|
1646
|
+
|
|
1647
|
+
## Cố ý chưa làm
|
|
1648
|
+
|
|
1649
|
+
| Việc | Vì sao |
|
|
1650
|
+
|---|---|
|
|
1651
|
+
| **Máy kiểm cấu trúc file test case** | Vẫn là năng lực mới, và vẫn chờ kết quả phép thử của trạm 1. Nhưng B12 làm nó **rẻ hơn hẳn**: cả hai luật mới đều **đếm được** — "0 ký tự `\|`" và "1 dòng mỗi kết quả". Nếu phép thử kết luận cần máy kiểm thì làm một lượt cho cả file gap và file test case |
|
|
1652
|
+
| **4 file tầng test của bản gốc** *(ui · e2e · integration · nfr — 935 dòng)* | Đã ghi `skipped` **kèm lý do**, không để "chưa quyết". Framework tách tầng UI thành 3 kỹ năng theo cây quyết định thay vì một file 341 dòng; phần khuôn TC và độ chính xác **đã tách ra dùng chung**. Còn lại là phần khai kiến trúc agent — framework không có. Đối chiếu nội dung từng tầng để dành |
|
|
1653
|
+
| **3 trạm còn lại** | Đi theo thứ tự dây chuyền. Trạm soát là trạm kế tiếp, và nó **tiêu thụ** đúng chuẩn B12 vừa đặt ra — làm sau là đúng thứ tự |
|
|
1654
|
+
|
|
1655
|
+
---
|
|
1656
|
+
|
|
1376
1657
|
# Tổng kết
|
|
1377
1658
|
|
|
1378
1659
|
| | Quyết định ở B7 | Bằng chứng | |
|
|
@@ -1415,9 +1696,9 @@ rồi, giờ bắt nó **so** với hai tài liệu kia"*. Không nạp thêm fi
|
|
|
1415
1696
|
|
|
1416
1697
|
# Các bước còn lại
|
|
1417
1698
|
|
|
1418
|
-
**
|
|
1699
|
+
**Mười ba bước B0–B12 đã xong.** Không còn bước nào đang chờ quyết định để bắt đầu.
|
|
1419
1700
|
|
|
1420
|
-
Nhưng có **
|
|
1701
|
+
Nhưng có **hai việc chưa validate** và **bốn món nợ** — xem §Còn treo ở cuối.
|
|
1421
1702
|
Mô tả kế hoạch gốc nằm ở [`qc-merge-plan.md`](qc-merge-plan.md).
|
|
1422
1703
|
|
|
1423
1704
|
> **Nhật ký này chỉ ghi bước đã hoàn thành** — mỗi bước xong mới nạp vào đây, không viết trước.
|
|
@@ -1429,7 +1710,7 @@ Mô tả kế hoạch gốc nằm ở [`qc-merge-plan.md`](qc-merge-plan.md).
|
|
|
1429
1710
|
Sau mỗi bước, toàn bộ hệ thống được dựng lại và kiểm tra tự động:
|
|
1430
1711
|
|
|
1431
1712
|
- 41/41 mẫu lệnh dựng thành công
|
|
1432
|
-
- Bộ kiểm tra nội bộ báo sạch
|
|
1713
|
+
- Bộ kiểm tra nội bộ báo sạch (17 nhánh luật sau B12)
|
|
1433
1714
|
- Nội dung mới đã lan đủ ba tầng: bản nguồn → bản đóng gói → bản đang chạy
|
|
1434
1715
|
- **9** dấu nguồn gốc đều được kiểm chứng khớp với file gốc thật
|
|
1435
1716
|
- Báo khói đã **thử cháy thật** cả 5 nhánh — không giao một cái chưa bao giờ kêu
|
|
@@ -1440,7 +1721,9 @@ Sau mỗi bước, toàn bộ hệ thống được dựng lại và kiểm tra
|
|
|
1440
1721
|
|
|
1441
1722
|
| # | Câu | Mức | Ai quyết |
|
|
1442
1723
|
|---|---|---|---|
|
|
1443
|
-
| **1** | Chạy thử trên một tính năng thật — **
|
|
1444
|
-
| **2** | Đưa nhật ký cho đội QC xem — họ là chủ sở hữu các kỹ năng này, và định dạng họ nhận đã đổi | 🟠 | bạn |
|
|
1724
|
+
| **1** | Chạy thử trên một tính năng thật — **mười ba bước, chưa một lần chạy**. Sau B11 phép thử này còn quan trọng hơn: nó là cách duy nhất biết bản cấp tính năng có bắt được mâu thuẫn chéo UC, và có theo đúng template hay không. Ước lượng chi phí của tôi đã sai 3 lần, đều ước thấp | 🔴 chưa validate | bạn (cần spec thật) |
|
|
1725
|
+
| **2** | Đưa nhật ký cho đội QC xem — họ là chủ sở hữu các kỹ năng này, và định dạng họ nhận đã đổi **hai lần** (B9 rồi B11) | 🟠 | bạn |
|
|
1726
|
+
| **2b** | **Máy kiểm cấu trúc file gap** — bệnh *"không theo template"* vẫn chưa có gì chặn bằng máy. Hiện chỉ có chỉ thị bằng văn xuôi, tức **cùng loại** với câu chỉ đường đã thất bại, chỉ mạnh hơn | 🟠 chưa chữa | bạn (sau khi chạy thử B11) |
|
|
1727
|
+
| **2c** | `Blocker` ↔ `Critical` — đổi trạm chạy test đọc `Critical` (một dòng) thay vì bắt đội QC đổi thói quen. Kết quả hôm trước cho thấy đổi từ ngữ của họ làm **tăng** khả năng lệch | 🟡 | bạn |
|
|
1445
1728
|
| 3 | 30 file không có bản gốc — framework tự sở hữu, gộp lại, hay ghép tay? | 🟡 nợ | bạn |
|
|
1446
1729
|
| 4 | Có lấy lại 38 dòng đã mất của phần "lập kế hoạch test" không? | 🟡 nợ | bạn |
|
|
@@ -149,7 +149,8 @@ mà bản đồ này tồn tại để chặn — và §5 để máy canh nó.
|
|
|
149
149
|
"adapt_rules": {
|
|
150
150
|
"path-placeholders": [
|
|
151
151
|
{ "from": "inputs/free-trial-spec/specs/", "to": "{paths.specs_dir}/" },
|
|
152
|
-
{ "from": "docs/<feature>/", "to": "{paths.qc_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" },
|
|
153
154
|
{ "from": "inputs/", "to": "{paths.specs_dir}/", "note": "khái niệm nguồn-evidence" }
|
|
154
155
|
],
|
|
155
156
|
"two-file-guard": { "kind": "append-block", "block": "…rào 'chỉ trả 2 file'…" },
|
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('');
|