@relipa/ai-flow-kit 0.2.0-beta.0 → 0.2.0-beta.1
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/aiflow.js +56 -0
- package/custom/rules/output-language.md +36 -0
- package/custom/skills/automation-testing/SKILL.md +1 -1
- package/custom/skills/ba-skills/skill-ba-build-business-rules-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-initial-analysis-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-mermaid-flowchart-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-qna-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-ui-prototype-v1.md +2 -0
- package/custom/skills/ba-skills/skill-ba-write-uc-spec-v1.md +2 -0
- package/custom/skills/coverage-check/SKILL.md +2 -0
- package/custom/skills/create-system-requirement/SKILL.md +11 -7
- package/custom/skills/deploy-model/SKILL.md +2 -0
- package/custom/skills/design-experiment/SKILL.md +2 -0
- package/custom/skills/evaluate-model/SKILL.md +2 -0
- package/custom/skills/evidence-aggregation/SKILL.md +2 -0
- package/custom/skills/execute-flow/SKILL.md +2 -0
- package/custom/skills/explore-data/SKILL.md +2 -0
- package/custom/skills/frame-ml-problem/SKILL.md +2 -0
- package/custom/skills/gate-review/SKILL.md +2 -0
- package/custom/skills/generate-spec/SKILL.md +2 -0
- package/custom/skills/generate-test-report/SKILL.md +2 -0
- package/custom/skills/generate-testcase/SKILL.md +2 -0
- package/custom/skills/impact-analysis/SKILL.md +2 -0
- package/custom/skills/improve-algorithm/SKILL.md +2 -0
- package/custom/skills/ingest-data/SKILL.md +116 -2
- package/custom/skills/log-bug/SKILL.md +2 -0
- package/custom/skills/pr-impact-analysis/SKILL.md +2 -0
- package/custom/skills/read-study-requirement/SKILL.md +13 -7
- package/custom/skills/report-customer/SKILL.md +6 -2
- package/custom/skills/retest-orchestration/SKILL.md +2 -0
- package/custom/skills/review-plan/SKILL.md +2 -0
- package/custom/skills/script-sync/SKILL.md +1 -1
- package/custom/skills/test-analysis/SKILL.md +2 -0
- package/custom/skills/test-skills/rules/qa-writing-standards.md +2 -2
- package/custom/skills/train-model/SKILL.md +2 -0
- package/custom/templates/shared/create-testcase-workflow.md +32 -1
- package/custom/templates/shared/gate-workflow.md +15 -0
- package/docs/common/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/scripts/create-score-excel.js +4 -111
- package/scripts/init.js +24 -1
- package/scripts/link-resolver.js +93 -0
- package/scripts/ticket-writer.js +247 -0
- package/scripts/use.js +141 -0
|
@@ -143,6 +143,8 @@ Create file `AK-Docs/04.Coding/04.Reviews/[functionId]/[ticketId].md`:
|
|
|
143
143
|
**Breaking changes:** None / [description if any]
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
Output language: auto-detect from the ticket/task input — see `custom/rules/output-language.md` (Vietnamese input → Vietnamese output; otherwise English).
|
|
147
|
+
|
|
146
148
|
### Step 2.5: Retrospect + propose memory drafts
|
|
147
149
|
|
|
148
150
|
Synthesize what was learned this task. **Priority order — human corrections first:** if this is a repeat pass through Gate 4 (the developer already sent "BUG: ..." at least once this task), the developer's own correction is the single highest-value lesson — scan back through this session for it and draft it even if you already drafted something for the earlier BUG at Step 4 (dedup will catch an exact repeat; don't skip capturing it out of caution). Only after that, add anything else genuinely new: architecture facts discovered, decisions made and why. For each candidate, create a **local draft** (no approval needed yet — `_pending/` is local-only, doc `docs/common/Memory-Architecture-v1.0.md`):
|
|
@@ -205,4 +205,4 @@ Trả về danh sách kết quả cho `execute-flow`:
|
|
|
205
205
|
- ❌ **Không overwrite** test block có hash match — skip hoàn toàn
|
|
206
206
|
- ✅ Ưu tiên locator: `getByRole` > `getByLabel` > `getByPlaceholder` > `getByText` > `getByTestId` > CSS
|
|
207
207
|
- ✅ Không tạo lại `BasePage.ts` / `fixtures/test.ts` nếu đã tồn tại
|
|
208
|
-
- ✅
|
|
208
|
+
- ✅ Test names/comments theo ngôn ngữ của TC input (xem `custom/rules/output-language.md`) — TC tiếng Việt → tiếng Việt, TC tiếng Anh → tiếng Anh
|
|
@@ -107,6 +107,8 @@ Dựa trên ticket/description, phân tích:
|
|
|
107
107
|
|
|
108
108
|
Tạo `test-plan/test-analysis.md`:
|
|
109
109
|
|
|
110
|
+
Ngôn ngữ output: auto-detect theo ngôn ngữ của ticket/testcase input — xem `custom/rules/output-language.md` và `custom/skills/test-skills/rules/qa-writing-standards.md` (input tiếng Việt → output tiếng Việt; ngược lại mặc định tiếng Anh; tiêu đề cột bảng vẫn giữ tiếng Anh).
|
|
111
|
+
|
|
110
112
|
```markdown
|
|
111
113
|
# Test Analysis: [Ticket ID / Feature Name]
|
|
112
114
|
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
## 1. Ngôn ngữ & văn phong
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
- **Tiêu đề cột của bảng (checklist, testcase) viết bằng tiếng Anh** theo quy ước team
|
|
11
|
+
- Ngôn ngữ nội dung mô tả/diễn giải **auto-detect theo ngôn ngữ của ticket/input** — xem `custom/rules/output-language.md`: input tiếng Việt → viết tiếng Việt (mặc định trước đây); input tiếng Anh → viết tiếng Anh.
|
|
12
|
+
- **Tiêu đề cột của bảng (checklist, testcase) luôn viết bằng tiếng Anh** theo quy ước team, bất kể ngôn ngữ nội dung ô; **nội dung trong ô theo ngôn ngữ đã xác định ở trên**.
|
|
13
13
|
- Thuật ngữ kỹ thuật/test có thể giữ tiếng Anh trong ngoặc khi cần (vd: kiểm thử biên (boundary)).
|
|
14
14
|
- Viết ngắn gọn, rõ ràng, không lan man. Ưu tiên gạch đầu dòng và bảng.
|
|
15
15
|
|
|
@@ -49,6 +49,8 @@ Log the following to MLflow or wandb for every run:
|
|
|
49
49
|
|
|
50
50
|
Do not leave runs unnamed or untracked.
|
|
51
51
|
|
|
52
|
+
Output language: auto-detect from the ticket/task input — see `custom/rules/output-language.md` (Vietnamese input → Vietnamese output; otherwise English).
|
|
53
|
+
|
|
52
54
|
### 5. Checkpointing
|
|
53
55
|
|
|
54
56
|
Save the best checkpoint by the primary validation metric (not the final epoch). Store the config file alongside the artifact so the checkpoint is self-contained. Use the model registry (MLflow Model Registry or equivalent) when available.
|
|
@@ -90,7 +90,7 @@ Ví dụ: `functionId = AD06` → thư mục gốc là `03.Testing/`
|
|
|
90
90
|
```
|
|
91
91
|
(AI-Artifacts/ = 03.Testing/07.AI-Artifacts/ — viết tắt cho ngắn gọn)
|
|
92
92
|
|
|
93
|
-
[Input: SRS / Jira / Backlog / Spec file] → Pre-flight: BẮT BUỘC xác định functionId
|
|
93
|
+
[Input: UC Spec + System Requirement (bắt buộc, cảnh báo nếu thiếu) / SRS / Jira / Backlog / Spec file] → Pre-flight: BẮT BUỘC xác định functionId
|
|
94
94
|
↓
|
|
95
95
|
Gate 1: Phân tích yêu cầu & Đánh giá rủi ro → AI-Artifacts/[functionId]/
|
|
96
96
|
↓ APPROVED
|
|
@@ -169,8 +169,37 @@ Trước khi ghi bất kỳ file nào vào `03.Testing/`, đảm bảo AK-Docs
|
|
|
169
169
|
|
|
170
170
|
> ❌ Không tự thêm `--yes` khi chưa thấy QA gõ xác nhận rõ ràng trong hội thoại.
|
|
171
171
|
|
|
172
|
+
#### Bước 0.6: Kiểm tra UC Spec & System Requirement (input bắt buộc, cảnh báo non-blocking)
|
|
173
|
+
|
|
174
|
+
**Mục tiêu:** Test Case phải được thiết kế dựa trên cả UC Spec (yêu cầu nghiệp vụ do BA chốt) **và** System Requirement (bản dịch kỹ thuật cho Dev — Validation Rule, Exception/Error Handling, Acceptance Test) — thiếu một trong hai, TC dễ bỏ sót case mà Dev đã implement hoặc case BA đã đặc tả.
|
|
175
|
+
|
|
176
|
+
1. Tìm UC Spec: `AK-Docs/02.BA-Specs/04.UC-Specs/[functionId]/UC-Spec_v{N}.md` (version cao nhất, không phải bản archive).
|
|
177
|
+
2. Tìm System Requirement: `AK-Docs/02.BA-Specs/00.Requirements/[functionId]/System-Requirement_v*.md` (version cao nhất).
|
|
178
|
+
3. Nếu tìm thấy System Requirement, đọc 2 header của nó: `UC-Spec-Version` (phải khớp version UC Spec tìm được ở bước 1) và `Status` (phải là `✅ Approved`).
|
|
179
|
+
|
|
180
|
+
Nếu **bất kỳ** điều kiện sau không thỏa mãn → hiển thị cảnh báo tương ứng, **không dừng/không cancel Gate 1** — vẫn tiếp tục với dữ liệu hiện có:
|
|
181
|
+
|
|
182
|
+
| Tình huống | Cảnh báo hiển thị |
|
|
183
|
+
|---|---|
|
|
184
|
+
| Không tìm thấy UC Spec | `⚠️ CẢNH BÁO: Không tìm thấy UC Spec cho [functionId] — Test Case sẽ chỉ dựa trên System Requirement (nếu có) + input khác, thiếu ngữ cảnh nghiệp vụ gốc.` |
|
|
185
|
+
| Không tìm thấy System Requirement | `⚠️ CẢNH BÁO: Không tìm thấy System Requirement cho [functionId] — Test Case sẽ chỉ dựa trên UC Spec, có thể thiếu Validation Rule/Exception Handling/Acceptance Test mà Dev đã chốt riêng.` |
|
|
186
|
+
| Có System Requirement nhưng `UC-Spec-Version` không khớp UC Spec hiện tại | `⚠️ CẢNH BÁO: System Requirement đang trace theo UC Spec v[X], nhưng UC Spec hiện tại là v[Y] — nội dung có thể lỗi thời.` |
|
|
187
|
+
| Có System Requirement, khớp version, nhưng `Status` ≠ `✅ Approved` | `⚠️ CẢNH BÁO: System Requirement cho [functionId] chưa Approved (Status: [giá trị hiện tại]) — nội dung có thể còn thay đổi.` |
|
|
188
|
+
|
|
189
|
+
→ Với mọi cảnh báo trên: ghi lại tình huống này vào `[functionId]_01_Requirement_Analysis_QA.md` (mục Assumptions, Bước 4) để QA/Dev biết rõ giới hạn của phân tích — không chỉ hiển thị rồi bỏ qua.
|
|
190
|
+
|
|
191
|
+
Nếu tất cả điều kiện thỏa mãn → thông báo ngắn rồi tiếp tục:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
✓ UC Spec: v[N]
|
|
195
|
+
✓ System Requirement: v[N] (✅ Approved, khớp UC Spec v[N])
|
|
196
|
+
→ Tiếp tục Gate 1...
|
|
197
|
+
```
|
|
198
|
+
|
|
172
199
|
#### Bước 1: Đọc và tổng hợp đầu vào
|
|
173
200
|
- Đọc `.aiflow/context/current.json` — tiêu đề, mô tả, acceptance criteria, liên kết tài liệu
|
|
201
|
+
- Đọc `UC-Spec_v{N}.md` (tìm thấy ở Bước 0.6) — yêu cầu nghiệp vụ gốc: Main/Alternative/Exception Flow, Business Rules (BR-NNN), UI Components
|
|
202
|
+
- Nếu tìm thấy `System-Requirement_v{N}.md` ở Bước 0.6 → đọc toàn bộ, dùng làm input kỹ thuật bổ sung cho Bước 2 (Phân tích yêu cầu): đối chiếu Functional/Non-Functional Requirements (`FR-*`, `NFR-*`), Validation Rules (`VR-*`), Exception & Error Handling (`ER-*`), Acceptance Test Scenarios (`AT-*`) — TC ở Gate 3 phải bao phủ cả các `AT-*`/`ER-*` này, không chỉ Business Rules trong UC Spec
|
|
174
203
|
- Nếu description có URL → chạy `ak fetch-links <url>` để tải nội dung
|
|
175
204
|
- Nếu có `supplementaryContext[]` → đọc từng item (SRS file, Figma link, API spec, spec MD file)
|
|
176
205
|
- Nếu có file yêu cầu thô được chỉ định → đọc file đó
|
|
@@ -227,6 +256,7 @@ Trước khi ghi bất kỳ file nào vào `03.Testing/`, đảm bảo AK-Docs
|
|
|
227
256
|
|
|
228
257
|
**Definition of Done:**
|
|
229
258
|
- [ ] `functionId` đã được xác định, thư mục đầu ra đã sẵn sàng
|
|
259
|
+
- [ ] Đã chạy Bước 0.6 (kiểm tra UC Spec + System Requirement); nếu có cảnh báo, đã ghi vào `01_Requirement_Analysis_QA.md`
|
|
230
260
|
- [ ] File `01_Requirement_Analysis_Result.md` có đủ 16 mục + Risk Matrix + Test Scope
|
|
231
261
|
- [ ] Toàn bộ Critical/Major đã được hỏi trực tiếp (từng câu một) và có câu trả lời từ QA — không còn nghi vấn bỏ ngỏ
|
|
232
262
|
- [ ] File `01_Requirement_Analysis_QA.md` đã tạo nếu có issue, ghi lại đầy đủ câu hỏi + câu trả lời (Bước 2b)
|
|
@@ -530,6 +560,7 @@ Sau khi Gate 4 đã APPROVED (bộ Test Case hoàn thành):
|
|
|
530
560
|
- ❌ **KHÔNG** tiến gate tiếp theo khi còn issue Critical hoặc Major chưa giải quyết
|
|
531
561
|
- ✅ **BẮT BUỘC** chạy Bước 0 ở Gate 1 — xác nhận `functionId` và thư mục đầu ra trước khi làm bất cứ điều gì
|
|
532
562
|
- ✅ **BẮT BUỘC** chạy Bước 0.5 ở Gate 1 — đảm bảo AK-Docs đang ở branch `feature/[functionId]/[taskId]` trước khi ghi file đầu tiên
|
|
563
|
+
- ✅ **BẮT BUỘC** chạy Bước 0.6 ở Gate 1 — kiểm tra UC Spec + System Requirement (tồn tại, khớp version, Approved); thiếu/lệch → cảnh báo yellow **không chặn Gate**, ghi vào QA artifact
|
|
533
564
|
- ✅ **BẮT BUỘC** đọc skill từ `.claude/skills/test-skills/` — không suy luận từ bộ nhớ
|
|
534
565
|
- ✅ **BẮT BUỘC** invoke `gate-review` cuối mỗi gate và chờ APPROVED
|
|
535
566
|
- ✅ **BẮT BUỘC** tuân thủ 100% `testcase-template.md` ở Gate 3 và Gate 4
|
|
@@ -719,3 +719,18 @@ Bugs logged: [N] | Skipped: [N]
|
|
|
719
719
|
7. Display: `GATE 2 DONE: Entry ghi vào AK-Docs/01.QnA/[file] — MR: [url hoặc "tạo thủ công"]`
|
|
720
720
|
|
|
721
721
|
> **Telemetry:** Run `ak gate 2 start --ticket [ticket-id]` when starting. Run `ak gate 2 approved --ticket [ticket-id]` when Gate 2 completes.
|
|
722
|
+
|
|
723
|
+
### GATE 3 — Sinh Task & Tạo Ticket (auto, chạy ngay sau Gate 2 xong)
|
|
724
|
+
|
|
725
|
+
> Implements "Vấn đề 5" trong `docs/internal/PM Workflow_v1.0.md`. **Không chờ APPROVED để bắt đầu** — chạy tự động ngay sau Gate 2 Bước 7, không cần lệnh riêng và không cần chờ PM merge MR (merge là việc độc lập của PM). `INVOKE`: `ingest-data` skill § 5 cho toàn bộ logic phân tích/đề xuất — mục này chỉ nêu gate mechanics.
|
|
726
|
+
|
|
727
|
+
1. Đọc lại entry vừa ghi + điều tra source/docs liên quan (skill § 5.1).
|
|
728
|
+
2. Quyết định content này có cần task không (skill § 5.2). Nếu KHÔNG cần task → hiển thị thông báo tương ứng và **kết thúc luôn, không chạy `ak gate 3 start`, không có Gate 3 Display**.
|
|
729
|
+
3. Nếu CẦN task → chạy `ak gate 3 start --ticket [ticket-id]`, rồi đề xuất danh sách task (skill § 5.3), cho PM sửa/thêm/xoá (skill § 5.4), lặp tới khi PM hài lòng.
|
|
730
|
+
4. Điểm dừng xác nhận trước khi tạo ticket (skill § 5.5) — ❌ không tự thêm `--yes`, phải có xác nhận rõ ràng trong hội thoại.
|
|
731
|
+
5. Xác định project đích trên Backlog/Jira (skill § 5.6, dùng `ak backlog-projects`/`ak jira-projects` + `ak backlog-set-default-project`/`ak jira-set-default-project`).
|
|
732
|
+
6. Tạo ticket bằng `ak tasks create-tickets <file>.json --json` (skill § 5.7). Nếu thiếu WRITE credentials, hỏi PM nhập ngay trong chat rồi `ak credentials set <field> "<value>"` và thử lại đúng 1 lần.
|
|
733
|
+
7. Ghi ngược link ticket vào entry gốc + hiển thị tổng kết (skill § 5.8).
|
|
734
|
+
8. Run: `ak gate 3 approved --ticket [ticket-id]` để đóng task.
|
|
735
|
+
|
|
736
|
+
> **Telemetry:** Run `ak gate 3 start --ticket [ticket-id]` chỉ khi Bước 2 kết luận CẦN task. Run `ak gate 3 approved --ticket [ticket-id]` khi Gate 3 hoàn tất.
|
package/docs/common/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2026-08-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`ingest-data` gained Gate 3 — auto-triggered task proposal & real ticket creation on Backlog/Jira, closing the gap after Gate 2 where the flow used to just stop once an entry landed in `AK-Docs/01.QnA/`.** Implements "Vấn đề 5" in `docs/internal/PM Workflow_v1.0.md`, decided to run **automatically** right after Gate 2 (not on-demand as the doc's earlier tentative proposal had it):
|
|
15
|
+
- AI re-reads the entry just ingested plus related source code/docs to judge impact, then decides whether it needs a task at all — pure-reference content (FYI, status update) ends the gate with no task proposed, no ticket created.
|
|
16
|
+
- When action is needed, proposes a task list (`type`: spec/coding/test/other, `title`, `track`, `description` built from the PM Workflow §6.1 template — spec-type tasks additionally require a PM-supplied "Nguồn tham chiếu" link, never inferred). The user can add/edit/remove tasks directly during review — not just approve/reject the whole list — before the single explicit confirmation point ("Danh sách task như trên — đồng ý tạo trên Backlog/Jira?", never auto-`--yes`'d).
|
|
17
|
+
- New `scripts/ticket-writer.js` — `ak tasks create-tickets <file>.json [--json]` posts the approved list as real Backlog/Jira issues. `scripts/link-resolver.js` gained the write-side API calls it needed (`createBacklogIssue`, `createJiraIssue`, `fetchBacklogIssueTypes`, `fetchBacklogPriorities`) plus a generic `httpsPost` helper (form-encoded for Backlog, JSON for Jira) — the existing `httpsGet`-only file had no POST capability before this.
|
|
18
|
+
- **Deliberately separate WRITE credentials** (`BACKLOG_API_KEY_WRITE` / `JIRA_API_TOKEN_WRITE`) from the read-only keys `ak fetch-links`/`ak use` already rely on, per the PM Workflow doc's policy of never letting the read key create tickets. When missing, `ak tasks create-tickets` returns a structured `{"error":"missing-write-credentials", field, message}` instead of failing silently — the AI surfaces this in chat, asks the user to paste the key, saves it via the new `ak credentials set <key> <value>` (allowlisted to the keys this feature introduced — dedicated adapter credentials still go through `ak init --adapter`), and retries once.
|
|
19
|
+
- Project-id resolution reuses the existing `BACKLOG_DEFAULT_PROJECT_ID`/`ak backlog-projects`/`ak backlog-set-default-project` pattern; added the Jira equivalents (`ak jira-projects`, `ak jira-set-default-project`, `fetchJiraProjects` in `scripts/use.js`) so both adapters check local config before ever prompting.
|
|
20
|
+
- Approved tickets get their link written back into the originating `Meetings-Log`/`QnA-Log`/`Confirmations-Log` entry (`→ Tasks created: TICKET-101 (coding), ...`), keeping the traceability chain from PM Workflow §6 intact end-to-end.
|
|
21
|
+
- **Still open, tracked in the PM Workflow doc's Vấn đề 5 implementation note:** no fixed feedback→task-type rule table (AI still judges per-context); new tickets don't auto-attach `functionId`/labels (Dev/BA/QA still assign on `ak use TICKET-XXX`); Backlog issueType/priority default to the project's first issueType and a "Normal"-named priority unless overridden explicitly in the task JSON.
|
|
22
|
+
|
|
10
23
|
## [0.2.0] - 2026-08-04
|
|
11
24
|
|
|
12
25
|
### Added
|
package/package.json
CHANGED
|
@@ -24,25 +24,6 @@ function norm(cmd) {
|
|
|
24
24
|
return ALIAS_MAP[c] || c;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// ── Version helpers ─────────────────────────────────────────────────────────
|
|
28
|
-
const CURRENT_VERSION = '0.1.6';
|
|
29
|
-
|
|
30
|
-
function parseVersion(v) {
|
|
31
|
-
if (!v || v === '-' || v === '' || v.toLowerCase() === 'unknown') return null;
|
|
32
|
-
const core = v.split('-')[0]; // strip prerelease suffix, e.g. 0.1.5-beta.1 -> 0.1.5
|
|
33
|
-
const parts = core.split('.').map(n => parseInt(n, 10));
|
|
34
|
-
if (parts.some(n => Number.isNaN(n))) return null;
|
|
35
|
-
return parts;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function compareVersion(a, b) {
|
|
39
|
-
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
40
|
-
const diff = (a[i] || 0) - (b[i] || 0);
|
|
41
|
-
if (diff !== 0) return diff;
|
|
42
|
-
}
|
|
43
|
-
return 0;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
27
|
// ── Parse log file ─────────────────────────────────────────────────────────
|
|
47
28
|
function parseLog(filePath) {
|
|
48
29
|
const lines = fs.readFileSync(filePath, 'utf-8').split(/\r?\n/);
|
|
@@ -58,19 +39,17 @@ function parseLog(filePath) {
|
|
|
58
39
|
if (!user || user === '-' || user === '') continue;
|
|
59
40
|
|
|
60
41
|
const timestamp = cols[0] || '';
|
|
61
|
-
const version = cols[5] || '';
|
|
62
42
|
const command = cols[6] || '';
|
|
63
43
|
const ticket = cols[9] || '';
|
|
64
44
|
|
|
65
45
|
const date = timestamp.split(' ')[0]; // dd/MM/yyyy
|
|
66
46
|
|
|
67
47
|
if (!users[user]) {
|
|
68
|
-
users[user] = { email: user, days: new Set(), events: 0, rawCmds: new Set(), gateCmds: []
|
|
48
|
+
users[user] = { email: user, days: new Set(), events: 0, rawCmds: new Set(), gateCmds: [] };
|
|
69
49
|
}
|
|
70
50
|
const u = users[user];
|
|
71
51
|
u.events++;
|
|
72
52
|
if (date) u.days.add(date);
|
|
73
|
-
if (version.trim()) u.versions.add(version.trim());
|
|
74
53
|
|
|
75
54
|
const cmd = norm(command);
|
|
76
55
|
if (cmd) u.rawCmds.add(cmd);
|
|
@@ -151,19 +130,6 @@ const SCORE_LABELS = [
|
|
|
151
130
|
'Sử dụng thường xuyên workflow',
|
|
152
131
|
];
|
|
153
132
|
|
|
154
|
-
// ── Version status for a user ──────────────────────────────────────────────
|
|
155
|
-
function getVersionStatus(u) {
|
|
156
|
-
const all = [...u.versions];
|
|
157
|
-
const parsed = all.map(v => ({ raw: v, parts: parseVersion(v) })).filter(v => v.parts);
|
|
158
|
-
if (parsed.length === 0) {
|
|
159
|
-
return { status: all.length ? 'unknown' : 'none', latest: null, all };
|
|
160
|
-
}
|
|
161
|
-
parsed.sort((a, b) => compareVersion(a.parts, b.parts));
|
|
162
|
-
const latest = parsed[parsed.length - 1];
|
|
163
|
-
const isOutdated = compareVersion(latest.parts, parseVersion(CURRENT_VERSION)) < 0;
|
|
164
|
-
return { status: isOutdated ? 'outdated' : 'ok', latest: latest.raw, all };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
133
|
// ── Build summary row for each user ───────────────────────────────────────
|
|
168
134
|
function buildRow(email, u) {
|
|
169
135
|
const { events, days, rawCmds, gateCmds } = u;
|
|
@@ -232,7 +198,6 @@ function buildRow(email, u) {
|
|
|
232
198
|
impactLabel: '',
|
|
233
199
|
totalScore: score,
|
|
234
200
|
notes,
|
|
235
|
-
versionStatus: getVersionStatus(u),
|
|
236
201
|
};
|
|
237
202
|
}
|
|
238
203
|
|
|
@@ -331,78 +296,11 @@ function writeExcel(rows, workbook, sheetLabel, fromLabel, toLabel) {
|
|
|
331
296
|
return sheet;
|
|
332
297
|
}
|
|
333
298
|
|
|
334
|
-
// ── Version warning sheet ───────────────────────────────────────────────────
|
|
335
|
-
function writeVersionSheet(workbook, rows) {
|
|
336
|
-
const sheet = workbook.addWorksheet('Version Warning');
|
|
337
|
-
sheet.columns = [
|
|
338
|
-
{ key: 'no', width: 5 },
|
|
339
|
-
{ key: 'email', width: 30 },
|
|
340
|
-
{ key: 'status', width: 16 },
|
|
341
|
-
{ key: 'latest', width: 16 },
|
|
342
|
-
{ key: 'allVersions', width: 30 },
|
|
343
|
-
{ key: 'note', width: 50 },
|
|
344
|
-
];
|
|
345
|
-
|
|
346
|
-
const border = {
|
|
347
|
-
top: { style: 'thin' }, left: { style: 'thin' },
|
|
348
|
-
bottom: { style: 'thin' }, right: { style: 'thin' },
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
sheet.addRow([`Cảnh báo phiên bản — Yêu cầu phiên bản hiện tại: ${CURRENT_VERSION}`]);
|
|
352
|
-
sheet.mergeCells('A1:F1');
|
|
353
|
-
const title = sheet.getCell('A1');
|
|
354
|
-
title.font = { bold: true, size: 13, color: { argb: 'FF1F3864' } };
|
|
355
|
-
title.alignment = { vertical: 'middle', horizontal: 'center' };
|
|
356
|
-
title.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD6DCE4' } };
|
|
357
|
-
sheet.getRow(1).height = 28;
|
|
358
|
-
|
|
359
|
-
const HEADERS = ['#', 'Email', 'Trạng thái', 'Phiên bản mới nhất', 'Các phiên bản dùng trong tuần', 'Ghi chú'];
|
|
360
|
-
const hRow = sheet.addRow(HEADERS);
|
|
361
|
-
hRow.height = 20;
|
|
362
|
-
hRow.eachCell(cell => {
|
|
363
|
-
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4F81BD' } };
|
|
364
|
-
cell.font = { bold: true, color: { argb: 'FFFFFFFF' }, size: 11 };
|
|
365
|
-
cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
|
366
|
-
cell.border = border;
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
const warnings = rows.filter(row => row.versionStatus.status === 'outdated' || row.versionStatus.status === 'unknown');
|
|
370
|
-
|
|
371
|
-
const STATUS_LABEL = { outdated: 'CŨ HƠN', unknown: 'KHÔNG XÁC ĐỊNH', ok: 'OK', none: 'Không có dữ liệu' };
|
|
372
|
-
const STATUS_COLOR = { outdated: 'FFFFC7CE', unknown: 'FFFFEB9C' };
|
|
373
|
-
|
|
374
|
-
warnings.forEach((row, idx) => {
|
|
375
|
-
const vs = row.versionStatus;
|
|
376
|
-
const note = vs.status === 'outdated'
|
|
377
|
-
? `Đang dùng ${vs.latest}, chưa cập nhật lên ${CURRENT_VERSION}`
|
|
378
|
-
: 'Không đọc được phiên bản (giá trị "-" hoặc "unknown") — cần kiểm tra lại';
|
|
379
|
-
const r = sheet.addRow([
|
|
380
|
-
idx + 1, row.email, STATUS_LABEL[vs.status], vs.latest || '-', vs.all.join(', ') || '-', note,
|
|
381
|
-
]);
|
|
382
|
-
r.height = 20;
|
|
383
|
-
const fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: STATUS_COLOR[vs.status] || 'FFFFFFFF' } };
|
|
384
|
-
r.eachCell({ includeEmpty: true }, (cell, col) => {
|
|
385
|
-
cell.border = border;
|
|
386
|
-
cell.fill = fill;
|
|
387
|
-
cell.alignment = (col <= 4)
|
|
388
|
-
? { vertical: 'middle', horizontal: 'center' }
|
|
389
|
-
: { vertical: 'middle', horizontal: 'left', wrapText: false };
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
sheet.views = [{ state: 'frozen', ySplit: 2 }];
|
|
394
|
-
sheet.autoFilter = { from: 'A2', to: 'F2' };
|
|
395
|
-
|
|
396
|
-
return warnings;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
299
|
async function writeExcelFile(rows, outputPath, sheetLabel, fromLabel, toLabel) {
|
|
400
300
|
const workbook = new ExcelJS.Workbook();
|
|
401
301
|
writeExcel(rows, workbook, sheetLabel, fromLabel, toLabel);
|
|
402
|
-
const warnings = writeVersionSheet(workbook, rows);
|
|
403
302
|
await workbook.xlsx.writeFile(outputPath);
|
|
404
|
-
console.log(`Written: ${outputPath} (${rows.length} users
|
|
405
|
-
return warnings;
|
|
303
|
+
console.log(`Written: ${outputPath} (${rows.length} users)`);
|
|
406
304
|
}
|
|
407
305
|
|
|
408
306
|
// ── Canonical user list (fixed order) ─────────────────────────────────────
|
|
@@ -437,7 +335,7 @@ const LOG_EMAIL_ALIAS = {
|
|
|
437
335
|
};
|
|
438
336
|
|
|
439
337
|
const EMPTY_USER = (email) => ({
|
|
440
|
-
email, events: 0, days: new Set(), rawCmds: new Set(), gateCmds: [],
|
|
338
|
+
email, events: 0, days: new Set(), rawCmds: new Set(), gateCmds: [],
|
|
441
339
|
});
|
|
442
340
|
|
|
443
341
|
// ── Main ───────────────────────────────────────────────────────────────────
|
|
@@ -471,7 +369,6 @@ const EMPTY_USER = (email) => ({
|
|
|
471
369
|
u.events += data.events;
|
|
472
370
|
data.days.forEach(d => u.days.add(d));
|
|
473
371
|
data.rawCmds.forEach(c => u.rawCmds.add(c));
|
|
474
|
-
data.versions.forEach(v => u.versions.add(v));
|
|
475
372
|
u.gateCmds.push(...data.gateCmds);
|
|
476
373
|
}
|
|
477
374
|
}
|
|
@@ -482,9 +379,5 @@ const EMPTY_USER = (email) => ({
|
|
|
482
379
|
return buildRow(email, u);
|
|
483
380
|
});
|
|
484
381
|
|
|
485
|
-
|
|
486
|
-
if (warnings.length) {
|
|
487
|
-
console.log(`\n⚠️ Version warnings (< ${CURRENT_VERSION} or unknown):`);
|
|
488
|
-
warnings.forEach(w => console.log(` - ${w.email}: ${w.versionStatus.latest || 'unknown'} (${w.versionStatus.status})`));
|
|
489
|
-
}
|
|
382
|
+
await writeExcelFile(rows, outputFile, sheetLabel, fromLabel, toLabel);
|
|
490
383
|
})();
|
package/scripts/init.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require('fs-extra');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
|
-
const { input, checkbox, confirm } = require('@inquirer/prompts');
|
|
5
|
+
const { input, checkbox, confirm, select } = require('@inquirer/prompts');
|
|
6
6
|
const { syncDocsRepos } = require('./docs-repo');
|
|
7
7
|
const memoryStore = require('./memory-store');
|
|
8
8
|
|
|
@@ -937,6 +937,29 @@ async function setupAdapter(projectDir, adapter) {
|
|
|
937
937
|
console.log(chalk.green(`✓ Credentials verified successfully`));
|
|
938
938
|
}
|
|
939
939
|
|
|
940
|
+
// Backlog: let the user pick a default project for tasks created manually (no ticket to
|
|
941
|
+
// derive one from otherwise) — kept for later use, e.g. syncing a local task up to Backlog.
|
|
942
|
+
if (adapter === 'backlog' && verifyResult.ok) {
|
|
943
|
+
const spaceKey = credentials.BACKLOG_SPACE_KEY;
|
|
944
|
+
const domain = spaceKey.includes('.') ? spaceKey : `${spaceKey}.backlog.com`;
|
|
945
|
+
try {
|
|
946
|
+
const { fetchBacklogProjects } = require('./use');
|
|
947
|
+
const projects = await fetchBacklogProjects(domain, credentials.BACKLOG_API_KEY);
|
|
948
|
+
if (projects.length > 0) {
|
|
949
|
+
const projectId = await select({
|
|
950
|
+
message: 'Default Backlog project for tasks created manually (no ticket):',
|
|
951
|
+
choices: projects.map((p) => ({ name: `${p.projectKey} ${p.name}`, value: p.id })),
|
|
952
|
+
});
|
|
953
|
+
const picked = projects.find((p) => p.id === projectId);
|
|
954
|
+
credentials.BACKLOG_DEFAULT_PROJECT_ID = picked.id;
|
|
955
|
+
credentials.BACKLOG_DEFAULT_PROJECT_KEY = picked.projectKey;
|
|
956
|
+
console.log(chalk.green(`✓ Default project set to ${picked.projectKey}.`));
|
|
957
|
+
}
|
|
958
|
+
} catch (err) {
|
|
959
|
+
console.log(chalk.gray(` (Could not fetch Backlog projects to pick a default: ${err.message} — skipping.)`));
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
940
963
|
const claudeConfigDir = path.join(
|
|
941
964
|
process.env.APPDATA ||
|
|
942
965
|
(process.platform === 'darwin'
|
package/scripts/link-resolver.js
CHANGED
|
@@ -78,6 +78,41 @@ function httpsGet(url, headers) {
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Generic POST — `bodyType` picks the wire format:
|
|
82
|
+
// 'form' → application/x-www-form-urlencoded (Backlog's write API)
|
|
83
|
+
// 'json' → application/json (Jira's write API)
|
|
84
|
+
function httpsPost(url, payload, { headers = {}, bodyType = 'json' } = {}) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const body = bodyType === 'form'
|
|
87
|
+
? new URLSearchParams(payload).toString()
|
|
88
|
+
: JSON.stringify(payload);
|
|
89
|
+
const reqHeaders = {
|
|
90
|
+
...headers,
|
|
91
|
+
'Content-Type': bodyType === 'form' ? 'application/x-www-form-urlencoded' : 'application/json',
|
|
92
|
+
'Content-Length': Buffer.byteLength(body),
|
|
93
|
+
};
|
|
94
|
+
const { hostname, pathname, search } = new URL(url);
|
|
95
|
+
const req = https.request(
|
|
96
|
+
{ hostname, path: pathname + search, method: 'POST', headers: reqHeaders },
|
|
97
|
+
(res) => {
|
|
98
|
+
let data = '';
|
|
99
|
+
res.on('data', chunk => (data += chunk));
|
|
100
|
+
res.on('end', () => {
|
|
101
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
102
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.substring(0, 500)}`));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try { resolve(data ? JSON.parse(data) : {}); }
|
|
106
|
+
catch (e) { reject(new Error('Invalid JSON from API')); }
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
req.on('error', reject);
|
|
111
|
+
req.write(body);
|
|
112
|
+
req.end();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
81
116
|
async function fetchBacklogTicket(domain, apiKey, ticketId) {
|
|
82
117
|
const issue = await httpsGet(`https://${domain}/api/v2/issues/${ticketId}?apiKey=${apiKey}`);
|
|
83
118
|
return {
|
|
@@ -148,6 +183,36 @@ async function fetchBacklogWiki(domain, apiKey, wikiId) {
|
|
|
148
183
|
};
|
|
149
184
|
}
|
|
150
185
|
|
|
186
|
+
// ── Backlog WRITE — issue creation (PM Workflow_v1.0.md "Vấn đề 5") ──────────
|
|
187
|
+
|
|
188
|
+
async function fetchBacklogIssueTypes(domain, apiKey, projectIdOrKey) {
|
|
189
|
+
return httpsGet(`https://${domain}/api/v2/projects/${projectIdOrKey}/issueTypes?apiKey=${apiKey}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function fetchBacklogPriorities(domain, apiKey) {
|
|
193
|
+
return httpsGet(`https://${domain}/api/v2/priorities?apiKey=${apiKey}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Creates a Backlog issue. Requires an apiKey with WRITE permission — distinct from the
|
|
198
|
+
* read-only key used elsewhere in this file for `ak fetch-links`/`ak use`.
|
|
199
|
+
* `projectId`, `summary`, `issueTypeId`, `priorityId` are mandatory per Backlog's API.
|
|
200
|
+
*/
|
|
201
|
+
async function createBacklogIssue(domain, apiKeyWrite, { projectId, summary, description, issueTypeId, priorityId }) {
|
|
202
|
+
const issue = await httpsPost(
|
|
203
|
+
`https://${domain}/api/v2/issues?apiKey=${apiKeyWrite}`,
|
|
204
|
+
{
|
|
205
|
+
projectId: String(projectId),
|
|
206
|
+
summary,
|
|
207
|
+
issueTypeId: String(issueTypeId),
|
|
208
|
+
priorityId: String(priorityId),
|
|
209
|
+
description: description || '',
|
|
210
|
+
},
|
|
211
|
+
{ bodyType: 'form' }
|
|
212
|
+
);
|
|
213
|
+
return { ticketId: issue.issueKey, url: `https://${domain}/view/${issue.issueKey}`, raw: issue };
|
|
214
|
+
}
|
|
215
|
+
|
|
151
216
|
function jiraAuth(email, token) {
|
|
152
217
|
return `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`;
|
|
153
218
|
}
|
|
@@ -191,6 +256,32 @@ async function fetchJiraComment(domain, email, token, ticketId, commentId) {
|
|
|
191
256
|
};
|
|
192
257
|
}
|
|
193
258
|
|
|
259
|
+
// ── Jira WRITE — issue creation (PM Workflow_v1.0.md "Vấn đề 5") ────────────
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Creates a Jira issue. `apiTokenWrite`/`emailWrite` are distinct from the read-only
|
|
263
|
+
* credentials used elsewhere in this file — same "separate WRITE key" policy as Backlog.
|
|
264
|
+
*/
|
|
265
|
+
async function createJiraIssue(domain, emailWrite, apiTokenWrite, { projectKey, summary, description, issueTypeName }) {
|
|
266
|
+
const issue = await httpsPost(
|
|
267
|
+
`https://${domain}/rest/api/3/issue`,
|
|
268
|
+
{
|
|
269
|
+
fields: {
|
|
270
|
+
project: { key: projectKey },
|
|
271
|
+
summary,
|
|
272
|
+
issuetype: { name: issueTypeName || 'Task' },
|
|
273
|
+
description: {
|
|
274
|
+
type: 'doc',
|
|
275
|
+
version: 1,
|
|
276
|
+
content: [{ type: 'paragraph', content: [{ type: 'text', text: description || '' }] }],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{ headers: { Authorization: jiraAuth(emailWrite, apiTokenWrite), Accept: 'application/json' }, bodyType: 'json' }
|
|
281
|
+
);
|
|
282
|
+
return { ticketId: issue.key, url: `https://${domain}/browse/${issue.key}`, raw: issue };
|
|
283
|
+
}
|
|
284
|
+
|
|
194
285
|
function linkDedupeKey(classified) {
|
|
195
286
|
if (classified.kind === 'document') return 'doc:' + classified.documentId + (classified.commentId ? '#' + classified.commentId : '');
|
|
196
287
|
if (classified.kind === 'wiki') return 'wiki:' + classified.wikiId;
|
|
@@ -270,4 +361,6 @@ module.exports = {
|
|
|
270
361
|
fetchBacklogTicket, fetchBacklogComment,
|
|
271
362
|
fetchBacklogDocument, fetchBacklogDocumentComment, fetchBacklogWiki,
|
|
272
363
|
fetchJiraTicket, fetchJiraComment,
|
|
364
|
+
fetchBacklogIssueTypes, fetchBacklogPriorities, createBacklogIssue,
|
|
365
|
+
createJiraIssue,
|
|
273
366
|
};
|