@relipa/ai-flow-kit 0.2.0-beta.2 → 0.2.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.
Files changed (42) hide show
  1. package/custom/rules/java/spring-boot-rules.md +209 -0
  2. package/custom/rules/javascript/nestjs-examples.md +41 -0
  3. package/custom/rules/javascript/nestjs-rules.md +42 -0
  4. package/custom/rules/javascript/nodejs-express-examples.md +35 -0
  5. package/custom/rules/javascript/nodejs-express-rules.md +49 -0
  6. package/custom/rules/javascript/reactjs-examples.md +380 -0
  7. package/custom/rules/javascript/reactjs-rules.md +173 -0
  8. package/custom/rules/php/php-examples.md +161 -0
  9. package/custom/rules/php/php-rules.md +127 -0
  10. package/custom/rules/python/python-django-examples.md +34 -0
  11. package/custom/rules/python/python-django-rules.md +48 -0
  12. package/custom/rules/python/python-examples.md +32 -0
  13. package/custom/rules/python/python-fastapi-examples.md +30 -0
  14. package/custom/rules/python/python-fastapi-rules.md +35 -0
  15. package/custom/rules/python/python-ml-examples.md +187 -0
  16. package/custom/rules/python/python-ml-rules.md +121 -0
  17. package/custom/rules/python/python-rules.md +58 -0
  18. package/custom/skills/ba-skills/skill-ba-qna-template-v1.md +4 -4
  19. package/custom/skills/ba-skills/skill-ba-qna-v1.md +6 -0
  20. package/custom/skills/create-system-requirement/SKILL.md +52 -16
  21. package/custom/skills/create-system-requirement/system-requirement-template-v1.md +128 -0
  22. package/custom/skills/impact-analysis/SKILL.md +106 -106
  23. package/custom/skills/ingest-data/SKILL.md +53 -5
  24. package/custom/skills/report-customer/SKILL.md +99 -99
  25. package/custom/templates/nestjs.md +5 -72
  26. package/custom/templates/nodejs-express.md +5 -73
  27. package/custom/templates/php-plain.md +5 -261
  28. package/custom/templates/php.md +5 -261
  29. package/custom/templates/python-django.md +5 -71
  30. package/custom/templates/python-fastapi.md +5 -54
  31. package/custom/templates/python-ml.md +1 -269
  32. package/custom/templates/python.md +5 -79
  33. package/custom/templates/reactjs.md +5 -492
  34. package/custom/templates/shared/create-testcase-workflow.md +30 -2
  35. package/custom/templates/shared/gate-workflow.md +5 -3
  36. package/custom/templates/shared/ml-gate-workflow.md +1 -0
  37. package/custom/templates/spring-boot.md +5 -224
  38. package/docs/common/CHANGELOG.md +20 -10
  39. package/package.json +1 -1
  40. package/scripts/init.js +143 -40
  41. package/scripts/link-resolver.js +60 -24
  42. package/scripts/ticket-writer.js +72 -3
@@ -197,24 +197,41 @@ async function fetchBacklogPriorities(domain, apiKey) {
197
197
  return httpsGet(`https://${domain}/api/v2/priorities?apiKey=${apiKey}`);
198
198
  }
199
199
 
200
+ /**
201
+ * Fetches a single Backlog issue — used to validate a PM-supplied "ticket cha có sẵn" before
202
+ * using it as `parentIssueId` for a batch of child issues (PM Workflow_v1.0.md Vấn đề 5, ticket cha).
203
+ * Returns the internal numeric `id` (needed for `parentIssueId`, which is NOT the issueKey).
204
+ */
205
+ async function getBacklogIssue(domain, apiKey, issueIdOrKey) {
206
+ const issue = await httpsGet(`https://${domain}/api/v2/issues/${issueIdOrKey}?apiKey=${apiKey}`);
207
+ return { id: issue.id, issueKey: issue.issueKey, projectId: issue.projectId, summary: issue.summary || '' };
208
+ }
209
+
200
210
  /**
201
211
  * Creates a Backlog issue. Requires an apiKey with WRITE permission — distinct from the
202
212
  * read-only key used elsewhere in this file for `ak fetch-links`/`ak use`.
203
213
  * `projectId`, `summary`, `issueTypeId`, `priorityId` are mandatory per Backlog's API.
214
+ *
215
+ * `parentIssueId` (optional) — Backlog's internal numeric issue `id` (not the `issueKey`) to
216
+ * link this as a child issue. ⚠️ NOT verified against a live space: requires the target project
217
+ * to have issue hierarchy ("Subtasking") enabled — if it isn't, Backlog rejects the request and
218
+ * the caller should surface the raw error rather than assume the link succeeded.
204
219
  */
205
- async function createBacklogIssue(domain, apiKeyWrite, { projectId, summary, description, issueTypeId, priorityId }) {
220
+ async function createBacklogIssue(domain, apiKeyWrite, { projectId, summary, description, issueTypeId, priorityId, parentIssueId }) {
221
+ const payload = {
222
+ projectId: String(projectId),
223
+ summary,
224
+ issueTypeId: String(issueTypeId),
225
+ priorityId: String(priorityId),
226
+ description: description || '',
227
+ };
228
+ if (parentIssueId) payload.parentIssueId = String(parentIssueId);
206
229
  const issue = await httpsPost(
207
230
  `https://${domain}/api/v2/issues?apiKey=${apiKeyWrite}`,
208
- {
209
- projectId: String(projectId),
210
- summary,
211
- issueTypeId: String(issueTypeId),
212
- priorityId: String(priorityId),
213
- description: description || '',
214
- },
231
+ payload,
215
232
  { bodyType: 'form' }
216
233
  );
217
- return { ticketId: issue.issueKey, url: `https://${domain}/view/${issue.issueKey}`, raw: issue };
234
+ return { ticketId: issue.issueKey, internalId: issue.id, url: `https://${domain}/view/${issue.issueKey}`, raw: issue };
218
235
  }
219
236
 
220
237
  function jiraAuth(email, token) {
@@ -262,25 +279,44 @@ async function fetchJiraComment(domain, email, token, ticketId, commentId) {
262
279
 
263
280
  // ── Jira WRITE — issue creation (PM Workflow_v1.0.md "Vấn đề 5") ────────────
264
281
 
282
+ /**
283
+ * Fetches a single Jira issue — used to validate a PM-supplied "ticket cha có sẵn" before
284
+ * using its key as `parent` for a batch of child issues.
285
+ */
286
+ async function getJiraIssue(domain, email, token, ticketId) {
287
+ const issue = await httpsGet(
288
+ `https://${domain}/rest/api/3/issue/${ticketId}`,
289
+ { Authorization: jiraAuth(email, token), Accept: 'application/json' }
290
+ );
291
+ const fields = issue.fields || {};
292
+ return { key: issue.key, projectKey: fields.project ? fields.project.key : null, summary: fields.summary || '' };
293
+ }
294
+
265
295
  /**
266
296
  * Creates a Jira issue. `apiTokenWrite`/`emailWrite` are distinct from the read-only
267
297
  * credentials used elsewhere in this file — same "separate WRITE key" policy as Backlog.
298
+ *
299
+ * `parentKey` (optional) — Jira issue key to link this as a child via `fields.parent`.
300
+ * ⚠️ NOT verified against a live instance: this field works directly for team-managed
301
+ * ("next-gen") projects; company-managed ("classic") projects may require the child's
302
+ * `issuetype` to be a dedicated `Subtask` type instead — if Jira rejects the request,
303
+ * surface the raw error rather than assume the link succeeded.
268
304
  */
269
- async function createJiraIssue(domain, emailWrite, apiTokenWrite, { projectKey, summary, description, issueTypeName }) {
305
+ async function createJiraIssue(domain, emailWrite, apiTokenWrite, { projectKey, summary, description, issueTypeName, parentKey }) {
306
+ const fields = {
307
+ project: { key: projectKey },
308
+ summary,
309
+ issuetype: { name: issueTypeName || 'Task' },
310
+ description: {
311
+ type: 'doc',
312
+ version: 1,
313
+ content: [{ type: 'paragraph', content: [{ type: 'text', text: description || '' }] }],
314
+ },
315
+ };
316
+ if (parentKey) fields.parent = { key: parentKey };
270
317
  const issue = await httpsPost(
271
318
  `https://${domain}/rest/api/3/issue`,
272
- {
273
- fields: {
274
- project: { key: projectKey },
275
- summary,
276
- issuetype: { name: issueTypeName || 'Task' },
277
- description: {
278
- type: 'doc',
279
- version: 1,
280
- content: [{ type: 'paragraph', content: [{ type: 'text', text: description || '' }] }],
281
- },
282
- },
283
- },
319
+ { fields },
284
320
  { headers: { Authorization: jiraAuth(emailWrite, apiTokenWrite), Accept: 'application/json' }, bodyType: 'json' }
285
321
  );
286
322
  return { ticketId: issue.key, url: `https://${domain}/browse/${issue.key}`, raw: issue };
@@ -365,6 +401,6 @@ module.exports = {
365
401
  fetchBacklogTicket, fetchBacklogComment,
366
402
  fetchBacklogDocument, fetchBacklogDocumentComment, fetchBacklogWiki,
367
403
  fetchJiraTicket, fetchJiraComment,
368
- fetchBacklogIssueTypes, fetchBacklogPriorities, createBacklogIssue,
369
- createJiraIssue,
404
+ fetchBacklogIssueTypes, fetchBacklogPriorities, createBacklogIssue, getBacklogIssue,
405
+ createJiraIssue, getJiraIssue,
370
406
  };
@@ -24,7 +24,9 @@ const {
24
24
  fetchBacklogIssueTypes,
25
25
  fetchBacklogPriorities,
26
26
  createBacklogIssue,
27
+ getBacklogIssue,
27
28
  createJiraIssue,
29
+ getJiraIssue,
28
30
  } = linkResolver;
29
31
 
30
32
  // Only these keys can be written via `ak credentials set` — dedicated adapter credentials
@@ -104,13 +106,23 @@ async function pickBacklogDefaults(domain, apiKeyRead, projectId) {
104
106
  * "target": "backlog" | "jira",
105
107
  * "projectId": "12345", // optional — Backlog only, overrides BACKLOG_DEFAULT_PROJECT_ID
106
108
  * "projectKey": "PROJ", // optional — Jira only, overrides JIRA_DEFAULT_PROJECT_KEY
109
+ * "parentTicket": { // optional — ingest-data SKILL.md § 5.6b "Ticket cha"
110
+ * "mode": "create" | "existing" | "none",
111
+ * "title": "...", // required when mode === "create"
112
+ * "description": "...", // optional when mode === "create"
113
+ * "existingId": "..." // required when mode === "existing" (Backlog key/id or Jira key)
114
+ * },
107
115
  * "tasks": [
108
- * { "type": "spec|coding|test|other", "title": "...", "description": "..." }
116
+ * { "type": "spec|system-requirement|impact-analysis|coding|test|execute-test|other", "title": "...", "description": "..." }
109
117
  * ]
110
118
  * }
111
119
  *
112
120
  * `description` is expected to already be fully composed by the AI per the ticket template in
113
121
  * PM Workflow_v1.0.md §6.1 (Nội dung task + Nguồn tham chiếu) — this command posts it as-is.
122
+ *
123
+ * When `parentTicket.mode` is "create" or "existing", every task in `tasks` is created as a
124
+ * child of that parent (Backlog `parentIssueId` / Jira `parent`) — see link-resolver.js for the
125
+ * caveat that this depends on the target project supporting issue hierarchy/subtasks.
114
126
  */
115
127
  async function createTicketsCommand(inputFile, options = {}) {
116
128
  const emit = (obj) => {
@@ -119,6 +131,7 @@ async function createTicketsCommand(inputFile, options = {}) {
119
131
  console.log(chalk.red(`✗ ${obj.message || obj.error}`));
120
132
  } else {
121
133
  console.log(chalk.green(`✓ ${obj.results.filter(r => r.ok).length}/${obj.results.length} ticket(s) created on ${obj.target}.`));
134
+ if (obj.parent) console.log(` ${chalk.cyan('↳ parent:')} ${obj.parent.ticketId} ${chalk.gray(obj.parent.url)}`);
122
135
  for (const r of obj.results) {
123
136
  console.log(r.ok ? ` ${chalk.green('✓')} ${r.ticketId} ${chalk.gray(r.title)}` : ` ${chalk.red('✗')} ${chalk.gray(r.title)} — ${r.error}`);
124
137
  }
@@ -173,6 +186,34 @@ async function createTicketsCommand(inputFile, options = {}) {
173
186
  return;
174
187
  }
175
188
 
189
+ const parentTicket = input.parentTicket || { mode: 'none' };
190
+ let parentRef = null;
191
+ if (parentTicket.mode === 'create') {
192
+ try {
193
+ const createdParent = await createBacklogIssue(write.domain, write.apiKeyWrite, {
194
+ projectId,
195
+ summary: parentTicket.title,
196
+ description: parentTicket.description || '',
197
+ issueTypeId: defaults.defaultIssueTypeId,
198
+ priorityId: defaults.defaultPriorityId,
199
+ });
200
+ parentRef = { ticketId: createdParent.ticketId, internalId: createdParent.internalId, url: createdParent.url };
201
+ } catch (err) {
202
+ emit({ ok: false, error: 'parent-create-failed', message: `Không tạo được ticket cha trên Backlog: ${err.message}` });
203
+ process.exitCode = 1;
204
+ return;
205
+ }
206
+ } else if (parentTicket.mode === 'existing') {
207
+ try {
208
+ const existing = await getBacklogIssue(write.domain, write.apiKeyRead, parentTicket.existingId);
209
+ parentRef = { ticketId: existing.issueKey, internalId: existing.id, url: `https://${write.domain}/view/${existing.issueKey}` };
210
+ } catch (err) {
211
+ emit({ ok: false, error: 'parent-not-found', message: `Không tìm thấy ticket cha "${parentTicket.existingId}" trên Backlog: ${err.message}` });
212
+ process.exitCode = 1;
213
+ return;
214
+ }
215
+ }
216
+
176
217
  const results = [];
177
218
  for (const task of tasks) {
178
219
  try {
@@ -182,13 +223,14 @@ async function createTicketsCommand(inputFile, options = {}) {
182
223
  description: task.description,
183
224
  issueTypeId: task.issueTypeId || defaults.defaultIssueTypeId,
184
225
  priorityId: task.priorityId || defaults.defaultPriorityId,
226
+ parentIssueId: parentRef ? parentRef.internalId : undefined,
185
227
  });
186
228
  results.push({ type: task.type, title: task.title, ok: true, ticketId: created.ticketId, url: created.url });
187
229
  } catch (err) {
188
230
  results.push({ type: task.type, title: task.title, ok: false, error: err.message });
189
231
  }
190
232
  }
191
- emit({ ok: true, target, projectId, results });
233
+ emit({ ok: true, target, projectId, parent: parentRef, results });
192
234
  return;
193
235
  }
194
236
 
@@ -206,6 +248,32 @@ async function createTicketsCommand(inputFile, options = {}) {
206
248
  return;
207
249
  }
208
250
 
251
+ const parentTicket = input.parentTicket || { mode: 'none' };
252
+ let parentRef = null;
253
+ if (parentTicket.mode === 'create') {
254
+ try {
255
+ const createdParent = await createJiraIssue(write.domain, write.emailWrite, write.apiTokenWrite, {
256
+ projectKey,
257
+ summary: parentTicket.title,
258
+ description: parentTicket.description || '',
259
+ });
260
+ parentRef = { ticketId: createdParent.ticketId, url: createdParent.url };
261
+ } catch (err) {
262
+ emit({ ok: false, error: 'parent-create-failed', message: `Không tạo được ticket cha trên Jira: ${err.message}` });
263
+ process.exitCode = 1;
264
+ return;
265
+ }
266
+ } else if (parentTicket.mode === 'existing') {
267
+ try {
268
+ const existing = await getJiraIssue(write.domain, write.emailWrite, write.apiTokenWrite, parentTicket.existingId);
269
+ parentRef = { ticketId: existing.key, url: `https://${write.domain}/browse/${existing.key}` };
270
+ } catch (err) {
271
+ emit({ ok: false, error: 'parent-not-found', message: `Không tìm thấy ticket cha "${parentTicket.existingId}" trên Jira: ${err.message}` });
272
+ process.exitCode = 1;
273
+ return;
274
+ }
275
+ }
276
+
209
277
  const results = [];
210
278
  for (const task of tasks) {
211
279
  try {
@@ -214,13 +282,14 @@ async function createTicketsCommand(inputFile, options = {}) {
214
282
  summary: task.title,
215
283
  description: task.description,
216
284
  issueTypeName: task.issueType || 'Task',
285
+ parentKey: parentRef ? parentRef.ticketId : undefined,
217
286
  });
218
287
  results.push({ type: task.type, title: task.title, ok: true, ticketId: created.ticketId, url: created.url });
219
288
  } catch (err) {
220
289
  results.push({ type: task.type, title: task.title, ok: false, error: err.message });
221
290
  }
222
291
  }
223
- emit({ ok: true, target, projectKey, results });
292
+ emit({ ok: true, target, projectKey, parent: parentRef, results });
224
293
  }
225
294
 
226
295
  /**