@educa-corp/fw 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # Có gì mới
2
+
3
+ > `fw install` in các mục của những version mới hơn bản đang cài trong dự án.
4
+ > Mỗi lần sửa lệnh, khuôn, bộ câu hỏi hay công cụ đều phải tăng version và thêm một mục ở đây. Test sẽ báo lỗi nếu version chưa có mục.
5
+
6
+ ## 0.6.0 — 2026-09-30
7
+
8
+ - Sẵn sàng publish lên npmjs với tên **`@educa-corp/fw`**. Cài lần đầu: `npx @educa-corp/fw install`.
9
+ - **`fw upgrade [version]`**: nâng cấp lên bản mới nhất, hoặc đúng một version (ví dụ `npx @educa-corp/fw upgrade 0.7.0`).
10
+ - `fw install` chép cả **`fw.js`** vào `.fw/core/bin/`. Hook và CI chạy bản này **offline**, luôn đúng version đã commit, không gọi `@latest`.
11
+ - `fw install` cảnh báo khi Python cài từ Microsoft Store (bản này thỉnh thoảng thoát lỗi mà không chạy script). Lệnh AI gặp lỗi mà không có thông báo nào thì chạy lại một lần.
12
+
13
+ ## 0.5.0 — 2026-09-30
14
+
15
+ - **Mã mục ổn định**: mỗi mục trong khuôn có mã ẩn cạnh tiêu đề, ví dụ `## 6. Ràng buộc <!-- sec:constraints -->`. Số và tên tiêu đề đổi tự do. Script và AI tìm mục theo mã.
16
+ - `spec_edit.py`: `section <file> <mã>` đọc đúng một mục · `upgrade <file>` gắn mã cho file theo khuôn cũ và liệt kê mục còn thiếu · chặn tạo file thiếu mục của khuôn · chặn sửa làm mất mã mục.
17
+ - Thay cho cách tìm mục theo tên ở 0.4.0: đổi tên tiêu đề sẽ làm cách đó không tìm thấy mục.
18
+
19
+ ## 0.4.0 — 2026-09-30
20
+
21
+ - Tầng sản phẩm (tham khảo BRD): thêm cột **Giai đoạn** (MVP, Phase 2…) cho bảng epic, mục **Ràng buộc** có mã `CON-01`…, trường duyệt `approved_by` / `approved_at`. Ba mục tuỳ chọn: Stakeholders, Giả định và phụ thuộc, Rủi ro. Ba mục này chỉ trích từ tài liệu PO đưa vào, không hỏi riêng.
22
+ - `/prd` đọc Ràng buộc của tầng sản phẩm. Rule bắt nguồn từ ràng buộc thì ghi `(nguồn: CON-xx)`.
23
+ - Duyệt tầng sản phẩm hoặc PRD phải ghi rõ người duyệt. `spec_edit` chặn nếu thiếu.
24
+ - Mục trong `product.md` giờ được tìm theo **tên**, không theo số thứ tự. File theo khuôn cũ được bổ sung mục và cột còn thiếu khi chạy lại `/product`.
25
+
26
+ ## 0.3.0 — 2026-09-30
27
+
28
+ - Lệnh mới `/prd`: `/prd <EPIC-ID>` tạo PRD từ epic đã `ready` · `/prd <EPIC-ID> change <mô tả>` thêm, sửa hoặc bỏ nội dung. Mã ổn định (`UC-001`, `UC-001-BR01`, `UC-001-AC01`), không bao giờ đánh số lại.
29
+ - `spec_edit.py`: `next-uc` (cấp mã UC kế tiếp) · `coverage` (kiểm mọi rule và tiêu chí của epic đều có trong PRD) · chặn đặt `ready` / `approved` khi còn 🤖 hoặc còn câu hỏi mở.
30
+ - Khuôn epic: rule và tiêu chí đánh số `R1.`, `AC1.` để `/prd` kiểm được không rơi mục nào.
31
+
32
+ ## 0.2.0 — 2026-09-30
33
+
34
+ - `spec_edit.py` có thêm `pending` (liệt kê các dòng còn 🤖) và `confirm <mã…>` (đổi 🤖 → ✅ theo mã, ví dụ `AC2 R8b`). AI không phải tự viết script cho hai việc này nữa.
35
+ - `/product`: tối đa 4 câu hỏi mỗi lượt, **tính cả câu phụ**.
36
+ - `/product`: khuôn tầng sản phẩm có thêm mục "Câu hỏi còn mở". File cũ được bổ sung mục này khi chạy tiếp.
37
+ - `/product`: có `business-dictionary.md` của framework cũ thì đề xuất gộp vào `glossary.md`.
38
+ - `/product`: chốt xong một checkpoint thì gợi ý `/clear` rồi chạy tiếp ở phiên mới, để giảm chi phí. Nếu lệnh được gọi trong một phiên đã có hội thoại khác thì nhắc `/clear` ngay từ đầu.
39
+ - `fw install` in ra version cũ → mới kèm danh sách thay đổi.
40
+
41
+ ## 0.1.0 — 2026-09-29
42
+
43
+ - Lệnh đầu tiên: `/product` (tầng sản phẩm và một epic).
44
+ - `fw install`: cài lệnh, bộ câu hỏi, khuôn; sao lưu file bạn đã sửa; tạo `.fw/config.yaml`.
45
+ - AI sửa file spec bằng `tools/spec_edit.py` (bắt buộc có Python 3.8+).
@@ -0,0 +1,16 @@
1
+ # .fw/config.yaml — cấu hình của dự án.
2
+ # Bạn được sửa file này. `fw install` chỉ tạo khi chưa có, không bao giờ ghi đè.
3
+
4
+ workspace: {{WORKSPACE}}
5
+
6
+ # Thư mục chứa mọi tài liệu (product, PRD, BDD, TDD, kiến trúc).
7
+ # Umbrella: trỏ vào repo spec, ví dụ: spec-repo/specs
8
+ specs: specs
9
+
10
+ # [jira] | [builtin] | [jira, builtin]
11
+ tracker: [builtin]
12
+
13
+ # Mỗi unit là một codebase build/deploy độc lập.
14
+ units: []
15
+ # - { name: web-phuhuynh, path: apps/parent-web, stack: react }
16
+ # - { name: api, path: services/api, stack: java-spring }
package/bin/fw.js ADDED
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+ const { spawnSync } = require('child_process');
8
+
9
+ const PACKAGE = '@educa-corp/fw';
10
+ const MANIFEST = '.fw/core/manifest.json';
11
+
12
+ // fw.js chạy ở hai nơi:
13
+ // - trong gói npm (có commands/, ref/…): cài được vào dự án;
14
+ // - bản đã chép vào dự án (.fw/core/bin/fw.js): để hook và CI chạy offline, đúng version đã commit.
15
+ // Bản này không có nguồn để cài; nâng cấp thì gọi gói npm qua `upgrade`.
16
+ const FW_ROOT = path.join(__dirname, '..');
17
+ const IS_PACKAGE = fs.existsSync(path.join(FW_ROOT, 'commands'));
18
+ const VERSION = IS_PACKAGE
19
+ ? require(path.join(FW_ROOT, 'package.json')).version
20
+ : JSON.parse(fs.readFileSync(path.join(FW_ROOT, 'manifest.json'), 'utf8')).version;
21
+
22
+ // Nguồn trong package → đích trong dự án.
23
+ const SOURCES = [
24
+ { from: 'commands', to: '.claude/commands' },
25
+ { from: 'ref', to: '.fw/core/ref' },
26
+ { from: 'templates', to: '.fw/core/templates' },
27
+ { from: 'tools', to: '.fw/core/tools' },
28
+ { from: 'bin/fw.js', to: '.fw/core/bin/fw.js' },
29
+ ];
30
+ // Mọi file gói npm phải mang theo để fw.js chạy được — test đối chiếu với `files` của package.json.
31
+ const RUNTIME_FILES = ['package.json', 'CHANGELOG.md', 'bin/config.template.yaml', ...SOURCES.map((s) => s.from)];
32
+ const CONFIG = '.fw/config.yaml';
33
+ const SETTINGS = '.claude/settings.json';
34
+
35
+ // Lệnh AI dùng để sửa file spec (xem tools/spec_edit.py). Cho phép sẵn để PO không bị hỏi quyền mỗi lần.
36
+ const SPEC_EDIT = '.fw/core/tools/spec_edit.py';
37
+ const PYTHON_CANDIDATES = [['python'], ['python3'], ['py', '-3']];
38
+ const ALLOW_RULES = PYTHON_CANDIDATES.map((c) => `Bash(${c.join(' ')} ${SPEC_EDIT}:*)`);
39
+ const MIN_PY_MINOR = 8;
40
+
41
+ const sha = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
42
+ const toPosix = (p) => p.split(path.sep).join('/');
43
+
44
+ function walk(dir) {
45
+ if (!fs.existsSync(dir)) return [];
46
+ if (fs.statSync(dir).isFile()) return [dir];
47
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
48
+ const p = path.join(dir, e.name);
49
+ if (e.isDirectory()) return walk(p);
50
+ return e.name === '.gitkeep' ? [] : [p];
51
+ });
52
+ }
53
+
54
+ function readManifest(target) {
55
+ const p = path.join(target, MANIFEST);
56
+ return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : { files: {} };
57
+ }
58
+
59
+ // Chép một file đè lên bản cũ; bản cũ mà người dùng đã sửa thì sao lưu trước.
60
+ function backup(target, rel, stamp) {
61
+ const dest = path.join(target, '.fw/backup', stamp, rel);
62
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
63
+ fs.copyFileSync(path.join(target, rel), dest);
64
+ }
65
+
66
+ // Tìm Python 3.8+. Trên Windows, `python` có thể là lối tắt của Microsoft Store: chạy được nhưng
67
+ // chỉ in lời mời cài — nên phải đọc số version, không chỉ xem lệnh có chạy hay không.
68
+ function findPython() {
69
+ for (const [cmd, ...args] of PYTHON_CANDIDATES) {
70
+ const r = spawnSync(cmd, [...args, '--version'], { encoding: 'utf8' });
71
+ const m = /Python 3\.(\d+)/.exec(`${r.stdout || ''}${r.stderr || ''}`);
72
+ if (r.status === 0 && m && Number(m[1]) >= MIN_PY_MINOR) {
73
+ const exe = spawnSync(cmd, [...args, '-c', 'import sys; print(sys.executable)'], { encoding: 'utf8' });
74
+ return { cmd: [cmd, ...args].join(' '), store: /WindowsApps/i.test(exe.stdout || '') };
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+
80
+ // Thêm luật cho phép vào .claude/settings.json, giữ nguyên mọi cấu hình khác của dự án.
81
+ function allowSpecEdit(target) {
82
+ const p = path.join(target, SETTINGS);
83
+ let settings = {};
84
+ if (fs.existsSync(p)) {
85
+ try {
86
+ settings = JSON.parse(fs.readFileSync(p, 'utf8'));
87
+ } catch {
88
+ return { error: `${SETTINGS} không phải JSON hợp lệ — không sửa. Tự thêm vào permissions.allow: ${ALLOW_RULES.join(', ')}` };
89
+ }
90
+ }
91
+ settings.permissions = settings.permissions || {};
92
+ const allow = (settings.permissions.allow = settings.permissions.allow || []);
93
+ const added = ALLOW_RULES.filter((r) => !allow.includes(r));
94
+ if (!added.length) return { added };
95
+ allow.push(...added);
96
+ fs.mkdirSync(path.dirname(p), { recursive: true });
97
+ fs.writeFileSync(p, JSON.stringify(settings, null, 2) + '\n');
98
+ return { added };
99
+ }
100
+
101
+ function install(target) {
102
+ const old = readManifest(target);
103
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
104
+ const next = {};
105
+ const report = { added: [], updated: [], backedUp: [], removed: [], kept: [] };
106
+
107
+ for (const { from, to } of SOURCES) {
108
+ const srcDir = path.join(FW_ROOT, from);
109
+ for (const src of walk(srcDir)) {
110
+ const rel = toPosix(path.join(to, path.relative(srcDir, src)));
111
+ const dest = path.join(target, rel);
112
+ const content = fs.readFileSync(src);
113
+ next[rel] = sha(content);
114
+
115
+ if (fs.existsSync(dest)) {
116
+ const current = sha(fs.readFileSync(dest));
117
+ if (current === next[rel]) continue;
118
+ // Không có trong manifest (file sẵn của dự án) hoặc lệch manifest (người dùng đã sửa).
119
+ if (old.files[rel] !== current) {
120
+ backup(target, rel, stamp);
121
+ report.backedUp.push(rel);
122
+ }
123
+ report.updated.push(rel);
124
+ } else {
125
+ report.added.push(rel);
126
+ }
127
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
128
+ fs.writeFileSync(dest, content);
129
+ }
130
+ }
131
+
132
+ // Gỡ file framework đã bỏ — chỉ khi còn nguyên bản; người dùng đã sửa thì giữ lại.
133
+ for (const [rel, hash] of Object.entries(old.files)) {
134
+ if (next[rel]) continue;
135
+ const dest = path.join(target, rel);
136
+ if (!fs.existsSync(dest)) continue;
137
+ if (sha(fs.readFileSync(dest)) === hash) {
138
+ fs.unlinkSync(dest);
139
+ report.removed.push(rel);
140
+ } else {
141
+ report.kept.push(rel);
142
+ }
143
+ }
144
+
145
+ fs.mkdirSync(path.join(target, '.fw/core'), { recursive: true });
146
+ fs.writeFileSync(
147
+ path.join(target, MANIFEST),
148
+ JSON.stringify({ version: VERSION, files: next }, null, 2) + '\n'
149
+ );
150
+
151
+ const configPath = path.join(target, CONFIG);
152
+ const configCreated = !fs.existsSync(configPath);
153
+ if (configCreated) {
154
+ const tpl = fs.readFileSync(path.join(__dirname, 'config.template.yaml'), 'utf8');
155
+ fs.writeFileSync(configPath, tpl.replace('{{WORKSPACE}}', path.basename(path.resolve(target))));
156
+ }
157
+
158
+ return {
159
+ ...report,
160
+ configCreated,
161
+ stamp,
162
+ fromVersion: old.version || null,
163
+ changes: changesSince(old.version),
164
+ python: findPython(),
165
+ permissions: allowSpecEdit(target),
166
+ };
167
+ }
168
+
169
+ const cmpVersion = (a, b) => {
170
+ const [x, y] = [a, b].map((v) => v.split('.').map(Number));
171
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i];
172
+ return 0;
173
+ };
174
+
175
+ // Các mục CHANGELOG của những version mới hơn bản đang cài. Cài mới (chưa có bản nào) thì không in.
176
+ function changesSince(fromVersion) {
177
+ if (!fromVersion || cmpVersion(fromVersion, VERSION) >= 0) return [];
178
+ const text = fs.readFileSync(path.join(FW_ROOT, 'CHANGELOG.md'), 'utf8');
179
+ return text
180
+ .split(/^## /m)
181
+ .slice(1)
182
+ .map((block) => ({ version: block.split(/\s/)[0], body: block }))
183
+ .filter((c) => /^\d+\.\d+\.\d+$/.test(c.version) && cmpVersion(c.version, fromVersion) > 0)
184
+ .map((c) => c.body.split('\n').filter((l) => l.startsWith('- ')));
185
+ }
186
+
187
+ function printReport(r) {
188
+ const upgrade = r.fromVersion && r.fromVersion !== VERSION;
189
+ console.log(upgrade ? `fw ${r.fromVersion} → ${VERSION} — đã nâng cấp` : `fw ${VERSION} — đã cài`);
190
+ if (r.changes.length) {
191
+ console.log(' Có gì mới:');
192
+ r.changes.flat().forEach((l) => console.log(` ${l}`));
193
+ }
194
+ console.log(` thêm ${r.added.length} · cập nhật ${r.updated.length} · gỡ ${r.removed.length}`);
195
+ if (r.configCreated) console.log(` tạo ${CONFIG} — hãy mở file này và khai specs, tracker, units`);
196
+ if (r.permissions.error) console.log(` ⚠ ${r.permissions.error}`);
197
+ else if (r.permissions.added.length) console.log(` thêm ${r.permissions.added.length} luật cho phép vào ${SETTINGS} (AI sửa spec không phải hỏi quyền)`);
198
+ if (r.python) {
199
+ console.log(` Python: ${r.python.cmd}`);
200
+ if (r.python.store) {
201
+ console.log(' ⚠ Python này cài từ Microsoft Store. Khi đo trên máy phát triển framework, bản Store thỉnh thoảng');
202
+ console.log(' thoát lỗi mà không chạy script (~6% khi nhiều tiến trình cùng lúc). Lệnh AI tự chạy lại một lần,');
203
+ console.log(' nhưng nên cài bản python.org: winget install Python.Python.3.12');
204
+ }
205
+ } else {
206
+ console.log('');
207
+ console.log(` ❌ KHÔNG TÌM THẤY PYTHON 3.${MIN_PY_MINOR}+ — các lệnh AI sẽ dừng ngay cho tới khi cài.`);
208
+ console.log(' Windows : winget install Python.Python.3.12 (hoặc tải ở https://www.python.org/downloads/)');
209
+ console.log(' macOS : brew install python');
210
+ console.log(` Sau khi cài, mở terminal mới và chạy lại: npx ${PACKAGE} install`);
211
+ process.exitCode = 1;
212
+ }
213
+ if (r.backedUp.length) {
214
+ console.log(` ⚠ ${r.backedUp.length} file bạn đã sửa bị ghi đè, bản cũ ở .fw/backup/${r.stamp}/:`);
215
+ r.backedUp.forEach((f) => console.log(` - ${f}`));
216
+ }
217
+ if (r.kept.length) {
218
+ console.log(` ⚠ ${r.kept.length} file framework đã bỏ nhưng bạn có sửa, nên được giữ lại:`);
219
+ r.kept.forEach((f) => console.log(` - ${f}`));
220
+ }
221
+ }
222
+
223
+ // Nâng cấp = chạy `install` của gói npm ở đúng version cần lên. Đang chạy đúng gói đó rồi thì cài luôn,
224
+ // không tải lại lần nữa.
225
+ function upgradeCommand(want, target) {
226
+ const version = want || 'latest';
227
+ if (IS_PACKAGE && (version === VERSION || (!want && require.main === module && isNpxLatest()))) return null;
228
+ return ['npx', '-y', `${PACKAGE}@${version}`, 'install', '--target', target];
229
+ }
230
+
231
+ // Chạy qua `npx @educa-corp/fw upgrade` (không ghi version) thì npx vừa tải bản mới nhất rồi.
232
+ const isNpxLatest = () => /[\\/]_npx[\\/]/.test(__dirname);
233
+
234
+ function upgrade(want, target, dryRun) {
235
+ const cmd = upgradeCommand(want, target);
236
+ if (!cmd) return printReport(install(target));
237
+ console.log(`Chạy: ${cmd.join(' ')}`);
238
+ if (dryRun) return;
239
+ const r = spawnSync(cmd[0], cmd.slice(1), { stdio: 'inherit', shell: process.platform === 'win32' });
240
+ process.exitCode = r.status === null ? 1 : r.status;
241
+ }
242
+
243
+ const HELP = `fw ${VERSION}${IS_PACKAGE ? '' : ' (bản đã cài trong dự án)'}
244
+
245
+ Cách dùng:
246
+ npx ${PACKAGE} install [--target <thư mục>] Cài framework vào dự án (lần đầu)
247
+ npx ${PACKAGE} upgrade [version] Nâng cấp lên bản mới nhất, hoặc đúng version (vd 0.7.0)
248
+ fw --version In phiên bản`;
249
+
250
+ function argValue(rest, flag) {
251
+ const i = rest.indexOf(flag);
252
+ return i >= 0 && rest[i + 1] ? rest[i + 1] : null;
253
+ }
254
+
255
+ function main(argv) {
256
+ const [cmd, ...rest] = argv;
257
+ const target = argValue(rest, '--target') || process.cwd();
258
+ if (cmd === '--version' || cmd === '-v') return console.log(VERSION);
259
+ if (cmd === 'install') {
260
+ if (!IS_PACKAGE) {
261
+ console.log(`Đây là bản fw đã cài trong dự án (${VERSION}), không mang theo nguồn để cài.`);
262
+ console.log(`Nâng cấp: npx ${PACKAGE} upgrade`);
263
+ process.exitCode = 1;
264
+ return;
265
+ }
266
+ return printReport(install(target));
267
+ }
268
+ if (cmd === 'upgrade') {
269
+ const want = rest.find((a) => /^\d+\.\d+\.\d+$/.test(a)) || null;
270
+ return upgrade(want, target, rest.includes('--dry-run'));
271
+ }
272
+ console.log(HELP);
273
+ if (cmd && cmd !== 'help' && cmd !== '--help') process.exitCode = 1;
274
+ }
275
+
276
+ if (require.main === module) main(process.argv.slice(2));
277
+
278
+ module.exports = { install, upgradeCommand, RUNTIME_FILES, SOURCES, PACKAGE };
@@ -0,0 +1,62 @@
1
+ ---
2
+ description: Viết PRD chính thức từ epic đã làm rõ (EPIC-ID), hoặc đổi PRD đã có (EPIC-ID change)
3
+ argument-hint: "<EPIC-ID> [change <mô tả thay đổi>] [--force]"
4
+ ---
5
+
6
+ # /prd
7
+
8
+ Mục đích: biến epic đã làm rõ (`/product`) thành **PRD chính thức**, gồm các use case, rule và tiêu chí chấp nhận có **mã ổn định**. Sau đó PRD là nguồn cho BDD, TDD và code.
9
+
10
+ - `/prd <EPIC-ID>`: tạo PRD từ epic `ready`.
11
+ - `/prd <EPIC-ID> change <mô tả>`: thêm, sửa hoặc bỏ nội dung trong PRD đã có.
12
+
13
+ Tham số: `$ARGUMENTS`
14
+
15
+ ## Bước 1 — Nạp context
16
+
17
+ Nếu **trước lệnh này** phiên đã có hội thoại khác, dòng đầu tiên in: *"💡 Phiên này đã dài. Nên `/clear` rồi gọi lại lệnh để rẻ hơn, vì mọi thứ đã chốt đều nằm trong file."*
18
+
19
+ 1. Chạy **một** lệnh Bash: `python .fw/core/tools/spec_edit.py --check && cat .fw/config.yaml`
20
+ - Lỗi không tìm thấy python: thử `python3`, rồi `py -3`, và dùng lệnh chạy được cho cả phiên. Cả ba đều lỗi thì **DỪNG NGAY** và báo: *"❌ Máy chưa có Python 3.8+. Cài Python (Windows: `winget install Python.Python.3.12`), mở terminal mới rồi chạy lại lệnh."*
21
+ - Không có `.fw/config.yaml`: dừng và báo *"Chưa cài framework. Chạy `npx @educa-corp/fw install` ở thư mục gốc dự án."*
22
+ 2. Tìm file epic: `{specs}/product/epics/{EPIC-ID}-*.md`. Đọc frontmatter để lấy `slug`, `domain`, `status`.
23
+ 3. File PRD: `D = {specs}/{domain}/{slug}/prd.md`.
24
+ 4. Đọc `{specs}/product/glossary.md` nếu có, và `SE section {specs}/product/product.md constraints`. Viết đúng thuật ngữ. PRD không được đi ngược ràng buộc nào. Rule bắt nguồn từ ràng buộc thì ghi `(nguồn: CON-01)`. `SE` báo không có mục đó (khuôn cũ): coi như chưa có ràng buộc, và nhắc một dòng *"Chạy `/product` để bổ sung Ràng buộc và Giai đoạn cho tầng sản phẩm."*
25
+
26
+ ## Bước 2 — Chọn chế độ
27
+
28
+ | Tình huống | Làm gì |
29
+ |---|---|
30
+ | Không có `change`, chưa có `D`, epic `ready` | Chế độ **tạo**: đọc `.fw/core/ref/prd/new.md` |
31
+ | Không có `change`, chưa có `D`, epic **chưa** `ready` | Dừng: *"EP-xx chưa làm rõ xong. Chạy `/product EP-xx` trước."* Có `--force` thì làm tiếp, nhưng in cảnh báo, ghi vào mục 4 của PRD dòng *"Tạo khi epic chưa ready (--force)"*, và không cho `approved` khi epic còn câu hỏi mở |
32
+ | Không có `change`, **đã có** `D` | Dừng: *"PRD đã có. Muốn đổi thì chạy `/prd EP-xx change <mô tả>`."* Không ghi đè |
33
+ | Có `change`, đã có `D` | Chế độ **đổi**: đọc `.fw/core/ref/prd/change.md` |
34
+ | Có `change`, chưa có `D` | Dừng: *"Chưa có PRD. Chạy `/prd EP-xx` trước."* |
35
+
36
+ ## Bước 3 — Luật chung
37
+
38
+ 1. **Mã ổn định.** `UC-NNN` đánh số trên toàn sản phẩm. `UC-NNN-BRnn`, `UC-NNN-ACnn` đánh số trong từng UC. Mã đã cấp thì **không bao giờ đổi, không dùng lại**. Thêm mới thì lấy số kế tiếp trong UC đó. Mục bỏ đi thì giữ dòng, gạch ngang nội dung, và ghi *"Đã bỏ (v…)"*.
39
+ 2. **Nguồn.** Mọi rule và tiêu chí lấy từ epic phải ghi `(nguồn: R3, AC1)`, đúng mã của epic.
40
+ 3. **Dấu.** Nội dung chuyển nguyên ý từ mục `✅` của epic thì giữ `✅`. Chỗ nào AI **tự thêm, tách hoặc suy ra** thì gắn `🤖`. Chỉ PO mới đổi được `🤖` thành `✅`.
41
+ 4. **Ngôn ngữ nghiệp vụ.** Không nói API, bảng dữ liệu hay framework.
42
+ 5. **Tối đa 4 câu hỏi mỗi lượt, tính cả câu phụ.**
43
+ 6. **Sửa file: CHỈ dùng `spec_edit.py`** (gọi là `SE`). Không dùng Edit/Write, không tự viết script:
44
+ - Tạo: `SE create <file> <<'EOF'` … `EOF` · Sửa: `SE edit <file> <<'EOF'` `[{"old": "…", "new": "…"}]` `EOF` (gom mọi chỗ sửa của một lượt vào một lần gọi)
45
+ - Frontmatter: `SE set <file> version=1.1 status=draft updated=YYYY-MM-DD`
46
+ - `SE pending <file>` · `SE confirm <file> UC-003-BR02 UC-003-AC01` · `SE next-uc {specs}` · `SE coverage <epic> <prd>`
47
+ - Đọc **một mục**: `SE section <file> <mã>`. Mã mục là `<!-- sec:… -->` ở tiêu đề (vd `constraints`, `usecases`, `open`). **Luôn tìm mục theo mã**, không theo số hay tên tiêu đề, không tự cắt file bằng `sed`/`grep`. Khi sửa, giữ nguyên `<!-- sec:… -->`.
48
+ - `SE` báo lỗi thì **không có gì được ghi**. Đọc lại file, sửa lệnh rồi chạy lại. Không bỏ qua lỗi. Lỗi mà **không in dòng nào** là Python chưa kịp chạy: chạy lại đúng lệnh đó một lần.
49
+ 7. **Duyệt.** `status=approved` chỉ đặt được khi không còn `🤖`, `open_questions=0`, và có người duyệt. Hỏi PO *"Ai duyệt PRD này?"* (mặc định là PO của epic), rồi đặt trong **một** lệnh: `SE set D status=approved approved_by=<tên> approved_at=YYYY-MM-DD`. `SE` tự chặn nếu chưa đủ. Không tìm cách lách.
50
+
51
+ ## Bước 4 — Kết thúc
52
+
53
+ In ra đúng khối sau:
54
+
55
+ ```
56
+ ---
57
+ Trạng thái : {✅ Đã duyệt v{version} | 🟡 Nháp v{version} — còn {n} mục 🤖, {k} câu hỏi mở}
58
+ Đã ghi : {D} {· epic → handed-off · product.md nếu có}
59
+ Kiểm : coverage {đủ | thiếu …} · {số UC} UC · {số BR} rule · {số AC} tiêu chí
60
+ Luồng : Product → [PRD ◀ bạn ở đây] → BDD → TDD · Design-spec → Code → Test → QC
61
+ Bước tiếp : {/bdd UC-xxx | trả lời / xác nhận các mục trên | `/clear` rồi `/prd {id}` để tiếp}
62
+ ```
@@ -0,0 +1,92 @@
1
+ ---
2
+ description: Làm rõ yêu cầu — tầng sản phẩm (không tham số) hoặc một epic (có tham số)
3
+ argument-hint: "[EPIC-ID | tên tính năng]"
4
+ ---
5
+
6
+ # /product
7
+
8
+ Mục đích: **hỏi ngược lại PO** để làm rõ những gì PO biết mà chưa viết ra, **trước** khi viết PRD. Giá trị của lệnh nằm ở **câu hỏi**, không nằm ở việc chép lại tài liệu.
9
+
10
+ - `/product`: tầng **sản phẩm**, gồm tầm nhìn, nhóm người dùng, danh sách epic.
11
+ - `/product <EPIC-ID | tên>`: làm rõ yêu cầu cho **một epic**.
12
+
13
+ Tham số: `$ARGUMENTS`
14
+
15
+ ## Bước 1 — Nạp context (chỉ những gì cần)
16
+
17
+ Nếu **trước lệnh này** phiên đã có hội thoại khác (một lệnh khác, hoặc một lần `/product` trước), thì dòng đầu tiên in ra: *"💡 Phiên này đã dài. Nên `/clear` rồi gọi lại lệnh để rẻ hơn, vì mọi thứ đã chốt đều nằm trong file."* Sau đó vẫn làm tiếp bình thường.
18
+
19
+ 1. Chạy **một** lệnh Bash: `python .fw/core/tools/spec_edit.py --check && cat .fw/config.yaml`
20
+ - Lỗi "không tìm thấy python": thử lại lần lượt với `python3`, rồi `py -3`, và dùng lệnh chạy được cho cả phiên. Cả ba đều lỗi thì **DỪNG NGAY**, không hỏi gì thêm, và báo: *"❌ Máy chưa có Python 3.8+. Cài Python (Windows: `winget install Python.Python.3.12`), mở terminal mới rồi chạy lại lệnh."*
21
+ - Không có `.fw/config.yaml`: dừng và báo *"Chưa cài framework vào dự án này. Chạy `npx @educa-corp/fw install` ở thư mục gốc dự án."*
22
+ - Lấy `specs` (thư mục spec) và `tracker` từ config.
23
+ 2. Đặt `P = {specs}/product`.
24
+ 3. Chế độ epic: đọc thêm `SE section P/product.md actors`, `… epics`, `… constraints` (gộp trong một lệnh Bash) và `P/glossary.md` nếu có. Epic không được đi ngược một ràng buộc `CON-xx`. PO muốn làm vậy thì hỏi PO có sửa ràng buộc ở tầng sản phẩm không.
25
+ **Không** đọc PRD khác, trừ khi PO nhắc tới một tính năng liên quan.
26
+ 4. **Bảng thuật ngữ cũ.** Nếu `{specs}/domain-knowledge/business-dictionary.md` (của framework cũ) có **dòng thuật ngữ thật** (không chỉ là khuôn trống), thì ở lượt hỏi đầu, đề xuất gộp các thuật ngữ đó vào `P/glossary.md`. PO đồng ý thì gộp, sau đó hỏi PO xoá file cũ hay giữ. Mục tiêu là dự án chỉ còn **một** bảng thuật ngữ.
27
+
28
+ ## Bước 2 — Xác định file đích
29
+
30
+ | Chế độ | File đích | Khuôn |
31
+ |---|---|---|
32
+ | Sản phẩm | `P/product.md` | `.fw/core/templates/product.md` |
33
+ | Epic | `P/epics/{id}-{slug}.md` | `.fw/core/templates/product-epic.md` |
34
+
35
+ Xác định `id` và `slug` của epic:
36
+ - `$ARGUMENTS` khớp một mã trong bảng epic của `product.md` thì dùng mã đó.
37
+ - `$ARGUMENTS` là tên tính năng: tìm trong bảng epic. Không thấy thì hỏi PO mã epic (Jira key, hoặc để AI đánh `EP-xx` nếu `tracker` chỉ có `builtin`).
38
+ - `slug` = kebab-case của tên, chỉ gồm `a-z 0-9 -`. Chỉ sinh **một lần** ở đây. PRD, BDD, TDD của epic dùng lại nguyên văn.
39
+ - Epic mà chưa có `product.md` thì vẫn chạy được, nhưng báo trước một dòng: *"Chưa có tầng sản phẩm, nhóm người dùng sẽ không được chuẩn hoá."*
40
+
41
+ **File đích đã tồn tại** thì đọc `.fw/core/ref/product/resume.md` và làm theo đó. Không ghi đè.
42
+
43
+ ## Bước 3 — Luật khi hỏi (áp cho mọi checkpoint)
44
+
45
+ 1. **AI trích, PO xác nhận.** PO dán tài liệu vào thì đó là **nguyên liệu**, không phải câu trả lời thay cho các bước.
46
+ - Mục mà tài liệu đã có: trình bản trích với dấu `🤖`, rồi hỏi *"Đúng chưa, cần sửa gì?"*
47
+ - Mục mà tài liệu chưa có: hỏi mới.
48
+ - Chỉ khi PO xác nhận thì mới đổi `🤖` thành `✅`. **Không bao giờ tự đổi thay PO.**
49
+ 2. **Không bỏ checkpoint**, kể cả khi tài liệu rất dày.
50
+ 3. **Tối đa 4 câu hỏi mỗi lượt, tính cả câu phụ.** Mọi chỗ cần PO trả lời hoặc xác nhận đều được **đánh số** trong 4 câu đó, không kèm thêm "còn vài điểm…" ở ngoài. Còn câu thì để lượt sau.
51
+ 4. **Chỉ nói ngôn ngữ nghiệp vụ.** Không hỏi và không đề xuất API, bảng dữ liệu hay framework.
52
+ 5. Gặp **thuật ngữ mới** (lặp từ 2 lần trở lên, chưa có trong `glossary.md`) thì đưa vào lượt hỏi kế tiếp: nghĩa là gì, có thêm vào `glossary.md` không.
53
+
54
+ ## Bước 4 — Đi qua các checkpoint
55
+
56
+ Đọc bộ câu hỏi, và **chỉ đọc phần của checkpoint đang làm**:
57
+ - Sản phẩm: `.fw/core/ref/product/product-level.md` (2 checkpoint)
58
+ - Epic: `.fw/core/ref/product/epic.md` (3 checkpoint)
59
+
60
+ Trước checkpoint 1 của epic, AI tự điền mục **Bối cảnh hệ thống** (nhóm người dùng liên quan, tính năng đã có liên quan, thuật ngữ mới). Mục này không cần PO.
61
+
62
+ **Sửa file: CHỈ dùng `spec_edit.py`** (gọi là `SE` bên dưới). Không dùng Edit/Write, không tự viết script:
63
+ - Tạo file mới: `SE create <file> <<'EOF'` … nội dung … `EOF`
64
+ - Sửa nội dung: `SE edit <file> <<'EOF'` `[{"old": "…", "new": "…"}]` `EOF`. Gom mọi chỗ sửa của một lượt vào **một** lần gọi. Đoạn lặp giống nhau thì thêm `"all": true`.
65
+ - Frontmatter: `SE set <file> checkpoint=1 open_questions=3 updated=YYYY-MM-DD`
66
+ - Xem các mục còn chờ PO: `SE pending <file>`. PO xác nhận các mục có mã: `SE confirm <file> AC2 AC3 R8b`
67
+ - Đọc một mục: `SE section <file> <mã>` (mã là `<!-- sec:… -->` ở tiêu đề). **Luôn tìm mục theo mã**, không theo số hay tên. Khi sửa, giữ nguyên `<!-- sec:… -->`.
68
+ - `SE` báo lỗi thì **không có gì được ghi**. Đọc lại file, sửa lệnh rồi chạy lại. Không bỏ qua lỗi. Lỗi mà **không in dòng nào** là Python chưa kịp chạy: chạy lại đúng lệnh đó một lần.
69
+
70
+ **Sau mỗi checkpoint** mà PO đã chốt:
71
+ 1. **Ghi file ngay.** Không đợi tới cuối, để lần sau chạy tiếp được.
72
+ 2. `SE set` các trường `checkpoint`, `open_questions` (đếm số dòng trong mục `sec:open` của file, **không** đếm câu vừa hỏi trong lượt), `updated`.
73
+ 3. Checkpoint chỉ được tính là chốt khi **mọi mục của nó mang `✅`**. Còn mục `🤖` thì không tăng `checkpoint`.
74
+
75
+ PO muốn dừng giữa chừng thì ghi file với trạng thái hiện tại (câu chưa trả lời đưa vào mục `sec:open`) rồi báo: *"Đã lưu. Chạy lại `/product {id}` để tiếp tục."*
76
+
77
+ **Mở phiên mới sau mỗi checkpoint.** Mọi thứ đã chốt đều nằm trong file, nên phiên mới không mất gì, và ngữ cảnh quay về mức ban đầu thay vì phình dần. Vì vậy ngay sau khi một checkpoint được chốt (còn checkpoint tiếp theo), khối kết thúc ghi *Bước tiếp* là: `/clear` rồi `/product {id}`.
78
+
79
+ ## Bước 5 — Kết thúc
80
+
81
+ - Epic đủ điều kiện `ready` (xem cuối `epic.md`): đặt `status: ready`, và cập nhật cột **Trạng thái** của epic đó trong `P/product.md` thành `sẵn sàng PRD`.
82
+ - Glossary: PO đồng ý thêm thuật ngữ nào thì ghi vào `P/glossary.md` (tạo file nếu chưa có), dạng bảng `| Thuật ngữ | Nghĩa | Tên tiếng Anh |`.
83
+
84
+ In ra đúng khối sau:
85
+
86
+ ```
87
+ ---
88
+ Trạng thái : {✅ Sẵn sàng | 🟡 Đang làm rõ — checkpoint {n}/{tổng}, còn {k} câu hỏi mở}
89
+ Đã ghi : {đường dẫn file} {· glossary.md nếu có sửa}
90
+ Luồng : [Product ◀ bạn ở đây] → PRD → BDD → TDD · Design-spec → Code → Test → QC
91
+ Bước tiếp : {/prd {id} | vừa chốt checkpoint: `/clear` rồi `/product {id}` (phiên mới, rẻ hơn) | đang giữa checkpoint: trả lời các câu trên}
92
+ ```
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@educa-corp/fw",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.6.0",
4
+ "description": "Framework làm việc với Claude Code cho phòng PTPM",
5
+ "bin": {
6
+ "fw": "bin/fw.js"
7
+ },
8
+ "scripts": {
9
+ "test": "node --test \"test/*.test.js\"",
10
+ "release:check": "node scripts/release-check.js",
11
+ "pack:check": "npm pack --dry-run",
12
+ "prepublishOnly": "npm test && node scripts/release-check.js",
13
+ "pub": "npm publish --access public"
14
+ },
15
+ "files": [
16
+ "bin/",
17
+ "commands/",
18
+ "ref/",
19
+ "templates/",
20
+ "tools/",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "keywords": [
27
+ "claude-code",
28
+ "spec-driven",
29
+ "workflow"
30
+ ],
31
+ "author": "duclm2 <duclm2@edupia.vn>",
32
+ "license": "MIT",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }
@@ -0,0 +1,32 @@
1
+ # /prd — chế độ đổi
2
+
3
+ Dùng cho **mọi** thay đổi: thêm, sửa, bỏ. Mã không bao giờ đánh số lại, nên không cần tách riêng "thêm" với "sửa".
4
+
5
+ ## Lượt 1 — Kế hoạch thay đổi
6
+
7
+ 1. Đọc `D`. Lấy mô tả thay đổi từ `$ARGUMENTS`. Không có mô tả thì hỏi PO *"Bạn muốn đổi gì?"* rồi dừng.
8
+ 2. Xác định các mục bị ảnh hưởng. Mô tả thay đổi có thể kéo theo mục khác (một rule đổi thì tiêu chí liên quan cũng phải đổi), nên phải tìm cả những mục đó.
9
+ 3. Cấp mã cho mục mới:
10
+ - Rule hoặc tiêu chí mới trong UC có sẵn: số lớn nhất **đang có trong UC đó**, kể cả dòng đã bỏ, cộng một.
11
+ - UC mới: `SE next-uc {specs}`.
12
+ 4. Trình kế hoạch:
13
+
14
+ | Mã | Loại | Hiện tại | Sẽ thành |
15
+ |---|---|---|---|
16
+ | UC-003-BR02 | sửa | Khoá sau 5 lần sai | Khoá sau 3 lần sai |
17
+ | UC-003-AC05 | thêm | — | Khi sai 3 lần thì … |
18
+ | UC-003-BR04 | bỏ | … | ~~…~~ Đã bỏ (v1.1) |
19
+
20
+ Nếu thư mục `{specs}/{domain}/{slug}/bdd/` đã có file của các UC bị ảnh hưởng thì thêm một dòng: *"⚠ UC-003 đã có BDD. Sau khi đổi PRD cần chạy lại `/bdd UC-003`."*
21
+ Kèm tối đa 3 câu hỏi, chỉ khi thay đổi còn mơ hồ.
22
+
23
+ ## Lượt 2 — Áp thay đổi
24
+
25
+ Khi PO đồng ý kế hoạch:
26
+
27
+ 1. **Một** lần `SE edit D` cho mọi chỗ sửa:
28
+ - Mục sửa và mục thêm mang `✅`, vì PO vừa duyệt kế hoạch.
29
+ - Mục bỏ: giữ dòng, gạch ngang nội dung, ghi *"Đã bỏ (v{mới})"*.
30
+ - Thêm dòng vào **5. Lịch sử thay đổi**, dạng *"v1.1 — UC-003: khoá sau 3 lần sai (BR02 sửa, AC05 thêm, BR04 bỏ)"*.
31
+ 2. Tăng version phụ (1.0 → 1.1): `SE set D version=1.1 updated=…`. PRD đang `approved` thì đặt luôn `status=draft approved_by=— approved_at=—`.
32
+ 3. Báo lại cho PO. Không in lại cả PRD, chỉ tóm tắt những gì đã đổi. Nếu không còn `🤖` và không còn câu hỏi mở thì hỏi: *"Duyệt lại bản v1.1 không?"* PO đồng ý thì duyệt theo luật 7 của lệnh. Bản mới cần người duyệt mới, nên đặt lại `approved_by` và `approved_at`.
package/ref/prd/new.md ADDED
@@ -0,0 +1,39 @@
1
+ # /prd — chế độ tạo
2
+
3
+ Chỉ **2 lượt** hỏi-đáp, vì epic đã được làm rõ kỹ. Không hỏi lại những gì epic đã `✅`.
4
+
5
+ ## Lượt 1 — Chia use case
6
+
7
+ 1. Đọc **toàn bộ** file epic.
8
+ 2. Chạy `SE next-uc {specs}` để lấy mã UC đầu tiên. Các UC tiếp theo tăng dần từ mã đó.
9
+ 3. Đề xuất cách chia UC.
10
+ - **Một UC = một mục tiêu của một actor**, xong trong một lần tương tác. Ví dụ "Đổi và quên mật khẩu", không phải "Quản lý bảo mật".
11
+ - Mỗi UC nên có khoảng 2–8 rule. Nhiều hơn thì cân nhắc tách, ít hơn một thì cân nhắc gộp.
12
+ - **Mọi `R…` và `AC…` của epic phải thuộc ít nhất một UC.** Một rule áp cho nhiều UC thì ghi ở tất cả các UC đó.
13
+ 4. Trình cho PO dạng bảng:
14
+
15
+ | UC | Tên | Actor | Lấy từ epic |
16
+ |---|---|---|---|
17
+ | UC-001 | … | ACT-01 | R8, R9, R10, AC4, AC5 |
18
+
19
+ Kèm tối đa 3 câu hỏi khác, chỉ khi cần. Ví dụ: một rule nên đặt vào UC nào, hoặc hai UC có nên gộp không. Không có gì cần hỏi thì chỉ hỏi *"Cách chia này được chưa?"*.
20
+
21
+ ## Lượt 2 — Viết PRD
22
+
23
+ Khi PO đồng ý cách chia UC:
24
+
25
+ 1. Tạo `D` bằng **một** lần `SE create`, theo khuôn `.fw/core/templates/prd.md`:
26
+ - **1. Tổng quan:** chuyển từ checkpoint 1 của epic. Ghi các ràng buộc `CON-xx` áp cho epic này.
27
+ - **2. Use case:** mỗi UC gồm điều kiện trước, kết quả sau, luồng chính (lấy các bước liên quan ở checkpoint 2 của epic), bảng rule, tiêu chí.
28
+ - **Cột Rule:** một rule mỗi dòng, dạng *"Hệ thống PHẢI / KHÔNG ĐƯỢC …"*.
29
+ - **Cột Logic:** rẽ nhánh, công thức, thông báo khi lỗi. Lấy từ edge case và nhật ký làm rõ của epic.
30
+ - **Tiêu chí:** dạng *"Khi … thì …"*, mô tả **kết quả nhìn thấy được**, không mô tả cơ chế.
31
+ - **3. Màn hình:** chuyển từ "Màn hình chính" của epic, kèm cột UC.
32
+ - **4. Câu hỏi còn mở:** chỉ những gì phát sinh khi viết PRD.
33
+ - **5. Lịch sử thay đổi:** dòng `1.0`.
34
+ 2. Chạy `SE coverage <epic> D`. **Thiếu mục nào thì bổ sung vào UC phù hợp rồi chạy lại cho tới khi đủ.** Mục mà PO muốn bỏ thì hỏi PO. Không được tự bỏ.
35
+ 3. Epic giờ chỉ còn là lịch sử:
36
+ - `SE set <epic> status=handed-off`
37
+ - Trong `{specs}/product/product.md`, sửa cột Trạng thái của epic thành `đã có PRD`.
38
+ 4. Chạy `SE pending D`. **Chỉ trình các mục `🤖`**, không in lại cả PRD. Mỗi dòng gồm mã và nội dung ngắn gọn. Kèm câu hỏi mở nếu có. Nhắc PO: *"Mở file để đọc toàn bộ. Trả lời, hoặc xác nhận theo mã, ví dụ 'OK UC-003-BR04'."*
39
+ 5. Khi PO xác nhận: `SE confirm D <mã…>`. Không còn `🤖` và không còn câu hỏi mở thì duyệt theo luật 7 của lệnh.