Buổi 16: Jira Integration + QA Knowledge Base
Mục Tiêu Buổi Học
Sinh viên sẽ xây dựng hệ thống quản lý issue lifecycle từ GitHub → Jira + QA Knowledge Base tự động compile từ CONTINUITY.md, giúp AI ngày càng thông minh qua feedback loop.
Phần 1: Lý Thuyết (20 phút)
Issue Lifecycle Automation
TaskFlow QA có 3 trạng thái issue:
┌──────────────────┐
│ Auto-Created │ (Session 14: AI tạo từ test failure)
│ GitHub Issue │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Auto-Reviewed │ (Session 15: AI review trên PR)
│ + Commented │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Synced to │ (Session 16: Jira + Knowledge Base)
│ Jira + Memory │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Resolved │ Next test cycle starts
└──────────────────┘cm-continuity: Dual Brain Architecture
QA AI hiệu quả nhờ 2 kiểu memory:
1. Master Brain — Global Patterns
# CONTINUITY.md — Master Brain
## Common Bug Patterns (từ tất cả projects)
### Pattern 1: Email Validation
- Root Cause: No regex or regex incomplete (forgot .+@.+\..+)
- Appeared in: UserAPI (3 times), AuthService (2 times)
- Fix Template: Use validator library, unit tests for edge cases
- Learning: Always use library, never write custom regex
### Pattern 2: SQLite NULL Handling
- Root Cause: Forgot .notNull() constraint or != null check
- Appeared in: TaskTable (5 times), UserTable (2 times)
- Fix Template: Add .notNull() to schema, return default value
- Learning: SQLite default is NULL, must explicit handle
### Pattern 3: Async/Await Timing
- Root Cause: Missing await before promise-returning function
- Appeared in: API routes (7 times), Tests (4 times)
- Fix Template: Always await db.prepare().run(), add eslint rule
- Learning: Node 18+ strict rules, catch unhandled promise rejectionsBenefit: Khi bug tương tự xuất hiện lần thứ 8, AI lookup từ master brain thay vì re-analyze.
2. Project Brain — Project-Specific
## CONTINUITY-TASKFLOW.md — Project Brain
### TaskFlow Bug Patterns
#### Bug 1: POST /api/users — Email Validation Missing
- Date: 2024-01-15
- Commit: abc123
- Error: TypeError: Cannot read property 'domain' of null
- Root Cause: validateEmail() checks length but not format
- Fix: import validator; use validator.isEmail(email)
- Test Added: test/api/users-validation.test.js
- Related: Pattern 1 from Master Brain
#### Bug 2: GET /api/tasks/:id — SQLite NULL Return
- Date: 2024-01-20
- Commit: def456
- Error: ReferenceError: Cannot read property 'title' of undefined
- Root Cause: Task.findById() returns undefined for deleted tasks
- Fix: Return { id, title: null } instead
- Test Added: test/api/tasks-deleted-task.test.js
- Related: Pattern 2 from Master BrainBenefit: QA AI llearns project quirks (e.g., "TaskFlow always needs notNull on title field").
Why AI-Native QA Improves Over Time
Scenario A: Without Knowledge Base
Week 1: Found email validation bug #1
- Analyze: 30 min + 3,000 tokens
Week 3: Found similar email bug #2
- Analyze: 30 min + 3,000 tokens (NO learning!)
Week 5: Found third email bug #3
- Analyze: 30 min + 3,000 tokens (STILL no learning!)
Total: 90 min + 9,000 tokens 🔴Scenario B: With Knowledge Base (CONTINUITY.md)
Week 1: Found email validation bug #1
- Analyze: 30 min + 3,000 tokens
- Save to CONTINUITY.md: "Pattern: Email validation always needs library"
Week 3: Found similar email bug #2
- Lookup CONTINUITY.md: 1 min + 50 tokens ✓
- Match with Pattern 1: "Use validator library"
- Fix immediately
Week 5: Found third email bug #3
- Lookup CONTINUITY.md: 1 min + 50 tokens ✓
- Match with Pattern 1: "Use validator library"
- Fix immediately
Total: 32 min + 3,100 tokens 🟢
Savings: 58 min + 5,900 tokens (62% reduction!)cm-notebooklm: Content Generation
CONTINUITY.md cung cấp raw QA knowledge. NotebookLM convert thành multiple formats:
Format 1: Podcast (Audio)
Episode 1: "Email Validation: The Most Common Bug"
- 15 min audio
- Covers Pattern 1
- Real examples from TaskFlow
- Actionable adviceFormat 2: Study Guide (PDF)
# QA Study Guide for TaskFlow
## Chapter 1: Email Validation Bugs
- What goes wrong?
- How to test?
- Fix checklist
- Real TaskFlow codeFormat 3: Flashcards (Quizlet)
Q: What's the most common bug in TaskFlow API?
A: Email validation missing format check. Use validator.isEmail()
Q: How to handle NULL in SQLite TaskFlow schema?
A: Add .notNull() constraint, return default value { id, title: null }Phần 2: Thực Hành (60 phút)
Task 1: Viết scripts/jira-sync.js
Sync issue từ GitHub → Jira (hoặc GitHub Projects nếu không có Jira)
Tạo file: scripts/jira-sync.js
const https = require('https');
const fs = require('fs');
/**
* Jira Sync for TaskFlow QA
* Syncs GitHub issues to Jira
*
* Usage: node scripts/jira-sync.js
*
* Environment vars:
* - JIRA_HOST: jira.company.com
* - JIRA_USER: qa@company.com
* - JIRA_API_TOKEN: xxxx (from Jira account)
* - GITHUB_TOKEN: xxxx
* - PROJECT_KEY: TASKFLOW (or TK)
*/
class JiraSync {
constructor(config = {}) {
this.jiraHost = process.env.JIRA_HOST || 'jira.company.com';
this.jiraUser = process.env.JIRA_USER;
this.jiraToken = process.env.JIRA_API_TOKEN;
this.githubToken = process.env.GITHUB_TOKEN;
this.githubOwner = process.env.GITHUB_OWNER || 'your-org';
this.githubRepo = process.env.GITHUB_REPO || 'taskflow';
this.projectKey = process.env.PROJECT_KEY || 'TASKFLOW';
this.syncMap = {}; // GitHub issue ID → Jira issue key
}
/**
* Get GitHub issues labeled 'bug' or 'test-failure'
*/
async getGitHubIssues() {
return this.makeRequest({
hostname: 'api.github.com',
path: `/repos/${this.githubOwner}/${this.githubRepo}/issues?labels=bug,test-failure&state=open`,
method: 'GET',
headers: {
'Authorization': `token ${this.githubToken}`,
'Accept': 'application/vnd.github.v3+json'
}
});
}
/**
* Create issue in Jira
*/
async createJiraIssue(githubIssue) {
const issueBody = {
fields: {
project: { key: this.projectKey },
summary: githubIssue.title,
description: {
version: 3,
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: `Original GitHub Issue: #${githubIssue.number}\n\n${githubIssue.body}`
}
]
}
]
},
issuetype: { name: 'Bug' },
labels: githubIssue.labels.map(label => label.name),
priority: this.mapPriority(githubIssue),
customfield_10000: githubIssue.html_url // Custom field: GitHub URL
}
};
return this.makeRequest({
hostname: this.jiraHost,
path: '/rest/api/3/issues',
method: 'POST',
headers: {
'Authorization': `Basic ${Buffer.from(`${this.jiraUser}:${this.jiraToken}`).toString('base64')}`,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(issueBody)
});
}
/**
* Map GitHub issue to Jira priority
*/
mapPriority(githubIssue) {
const body = githubIssue.body.toLowerCase();
if (body.includes('🔴 critical') || body.includes('critical')) {
return { name: 'Highest' };
}
if (body.includes('🟠 high') || body.includes('high')) {
return { name: 'High' };
}
if (body.includes('🟡 medium') || body.includes('medium')) {
return { name: 'Medium' };
}
return { name: 'Low' };
}
/**
* Generic HTTP request helper
*/
makeRequest(options) {
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
if (data) {
resolve(JSON.parse(data));
} else {
resolve({ status: res.statusCode });
}
} catch (e) {
reject(new Error(`Failed to parse response: ${e.message}`));
}
});
});
req.on('error', reject);
if (options.body) {
req.write(options.body);
}
req.end();
});
}
/**
* Main sync workflow
*/
async run() {
try {
console.log('🔄 Jira Sync started...\n');
// Step 1: Get GitHub issues
console.log('Step 1: Fetching GitHub issues...');
const githubIssues = await this.getGitHubIssues();
console.log(`✅ Found ${githubIssues.length} open issues\n`);
if (githubIssues.length === 0) {
console.log('No issues to sync.');
return;
}
// Step 2: Create in Jira
console.log('Step 2: Syncing to Jira...');
let synced = 0;
let failed = 0;
for (const issue of githubIssues) {
try {
const jiraIssue = await this.createJiraIssue(issue);
console.log(`✅ Created: ${jiraIssue.key} ← #${issue.number}`);
this.syncMap[issue.number] = jiraIssue.key;
synced++;
} catch (error) {
console.error(`❌ Failed to sync #${issue.number}: ${error.message}`);
failed++;
}
}
// Step 3: Save sync map
console.log('\nStep 3: Saving sync map...');
fs.writeFileSync(
'sync-map.json',
JSON.stringify(this.syncMap, null, 2)
);
console.log(`✅ Saved sync map\n`);
console.log(`\n📊 Summary:`);
console.log(` Synced: ${synced}`);
console.log(` Failed: ${failed}`);
console.log(` Total: ${synced + failed}`);
} catch (error) {
console.error('\n❌ ERROR:', error.message);
process.exit(1);
}
}
}
// CLI
if (require.main === module) {
const sync = new JiraSync();
sync.run();
}
module.exports = JiraSync;Task 2: Compile QA Knowledge Base từ CONTINUITY.md
Tạo file: scripts/compile-knowledge-base.js
const fs = require('fs');
const path = require('path');
const marked = require('marked'); // npm install marked
/**
* Compile QA Knowledge Base
* Reads CONTINUITY.md + CONTINUITY-TASKFLOW.md
* Generates lessons.md for uploading to NotebookLM
*/
class KnowledgeBaseCompiler {
constructor() {
this.masterBrain = this.readFile('CONTINUITY.md');
this.projectBrain = this.readFile('CONTINUITY-TASKFLOW.md');
}
readFile(filename) {
const filepath = path.join(process.cwd(), filename);
if (!fs.existsSync(filepath)) {
console.warn(`⚠️ File not found: ${filename}`);
return '';
}
return fs.readFileSync(filepath, 'utf-8');
}
/**
* Extract bug patterns from CONTINUITY.md
*/
extractPatterns() {
const patterns = [];
const lines = this.masterBrain.split('\n');
let currentPattern = null;
lines.forEach((line, idx) => {
if (line.match(/^### Pattern \d+:/)) {
currentPattern = {
title: line.replace('### ', ''),
rootCause: '',
appeared: '',
fixTemplate: '',
learning: ''
};
patterns.push(currentPattern);
} else if (currentPattern) {
if (line.includes('Root Cause:')) {
currentPattern.rootCause = line.replace('- Root Cause:', '').trim();
} else if (line.includes('Appeared in:')) {
currentPattern.appeared = line.replace('- Appeared in:', '').trim();
} else if (line.includes('Fix Template:')) {
currentPattern.fixTemplate = line.replace('- Fix Template:', '').trim();
} else if (line.includes('Learning:')) {
currentPattern.learning = line.replace('- Learning:', '').trim();
}
}
});
return patterns;
}
/**
* Extract project-specific bugs
*/
extractProjectBugs() {
const bugs = [];
const lines = this.projectBrain.split('\n');
let currentBug = null;
lines.forEach((line) => {
if (line.match(/^#### Bug \d+:/)) {
currentBug = {
title: line.replace('#### ', ''),
date: '',
commit: '',
error: '',
rootCause: '',
fix: '',
test: '',
related: ''
};
bugs.push(currentBug);
} else if (currentBug) {
if (line.includes('Date:')) currentBug.date = line.replace('- Date:', '').trim();
if (line.includes('Commit:')) currentBug.commit = line.replace('- Commit:', '').trim();
if (line.includes('Error:')) currentBug.error = line.replace('- Error:', '').trim();
if (line.includes('Root Cause:')) currentBug.rootCause = line.replace('- Root Cause:', '').trim();
if (line.includes('Fix:')) currentBug.fix = line.replace('- Fix:', '').trim();
if (line.includes('Test Added:')) currentBug.test = line.replace('- Test Added:', '').trim();
if (line.includes('Related:')) currentBug.related = line.replace('- Related:', '').trim();
}
});
return bugs;
}
/**
* Generate comprehensive lessons.md
*/
generateLessons() {
const patterns = this.extractPatterns();
const bugs = this.extractProjectBugs();
let lessons = `# TaskFlow QA Knowledge Base
**Generated from**: CONTINUITY.md + CONTINUITY-TASKFLOW.md
**Purpose**: Train AI reviewer and QA team on recurring patterns
**Use**: Upload to NotebookLM for podcast/study guide generation
---
## Table of Contents
1. [Common Bug Patterns](#common-patterns) — ${patterns.length} patterns
2. [TaskFlow-Specific Bugs](#taskflow-bugs) — ${bugs.length} bugs
3. [Learning Timeline](#timeline) — When bugs appeared
4. [Quick Reference](#quick-ref) — Fix checklist
---
## Common Bug Patterns
These patterns appear across multiple projects. When new bug found, check these first.
`;
patterns.forEach((pattern, idx) => {
lessons += `
### Pattern ${idx + 1}: ${pattern.title}
**Root Cause**: ${pattern.rootCause}
**Where It Appears**: ${pattern.appeared}
**How to Fix**: ${pattern.fixTemplate}
**Key Learning**: ${pattern.learning}
---
`;
});
lessons += `
## TaskFlow-Specific Bugs
Real bugs found in TaskFlow codebase with solutions.
`;
bugs.forEach((bug, idx) => {
lessons += `
### Bug ${idx + 1}: ${bug.title}
**Found**: ${bug.date} (Commit: \`${bug.commit}\`)
**Error Message**: \`${bug.error}\`
**Root Cause**: ${bug.rootCause}
**Solution Applied**: ${bug.fix}
**Test Coverage**: ${bug.test}
**Related Pattern**: ${bug.related}
---
`;
});
lessons += `
## Learning Timeline
When did we discover each pattern?
\`\`\`
${bugs.map(b => `${b.date} — ${b.title}`).join('\n')}
\`\`\`
---
## Quick Reference: Fix Checklist
Use this when reviewing new issues:
### Email Validation (Pattern 1)
- [ ] Using validator.isEmail() or similar library?
- [ ] Testing edge cases (subdomains, +alias, long domains)?
- [ ] Returning 400 with clear error message?
### SQLite NULL (Pattern 2)
- [ ] Schema has .notNull() for required fields?
- [ ] Queries handle NULL gracefully?
- [ ] Tests check NULL edge case?
### Async/Await (Pattern 3)
- [ ] All promise-returning functions have await?
- [ ] Error handling with try/catch?
- [ ] ESLint rule: no-floating-promises enabled?
---
## For NotebookLM
**How to use this file**:
1. Copy content of this file
2. Go to https://notebooklm.google.com
3. Create new notebook
4. Paste this content
5. Click "Generate" → Choose:
- **Study Guide**: For QA training material
- **Podcast**: For team listening (15-20 min episodes)
- **Flashcards**: For quick memorization
**Expected outputs**:
- 📚 Study Guide: 20-30 pages, structured lessons
- 🎙️ Podcast: 2-3 episodes, conversational tone, real examples
- 🎯 Flashcards: 50+ Q&A pairs for Quizlet
---
## Stats
- **Master Brain Patterns**: ${patterns.length}
- **Project Brain Bugs**: ${bugs.length}
- **AI Learning Curve**: Improves 10% per new pattern documented
- **Token Savings**: 50 tokens (lookup) vs 3,000 tokens (re-analyze)
`;
return lessons;
}
/**
* Main workflow
*/
run() {
console.log('📚 Knowledge Base Compiler started...\n');
console.log('Step 1: Reading Master Brain...');
console.log(`✅ Read CONTINUITY.md (${this.masterBrain.length} chars)\n`);
console.log('Step 2: Reading Project Brain...');
console.log(`✅ Read CONTINUITY-TASKFLOW.md (${this.projectBrain.length} chars)\n`);
console.log('Step 3: Extracting patterns and bugs...');
const patterns = this.extractPatterns();
const bugs = this.extractProjectBugs();
console.log(`✅ Found ${patterns.length} patterns, ${bugs.length} bugs\n`);
console.log('Step 4: Generating lessons.md...');
const lessons = this.generateLessons();
fs.writeFileSync('lessons.md', lessons);
console.log(`✅ Saved lessons.md (${lessons.length} chars)\n`);
console.log('✅ Complete!\n');
console.log('Next steps:');
console.log('1. Copy lessons.md content');
console.log('2. Go to https://notebooklm.google.com');
console.log('3. Create notebook and paste content');
console.log('4. Generate: Study Guide, Podcast, Flashcards');
}
}
// CLI
if (require.main === module) {
const compiler = new KnowledgeBaseCompiler();
compiler.run();
}
module.exports = KnowledgeBaseCompiler;Task 3: Sample CONTINUITY Files
Create example knowledge base files:
File: CONTINUITY.md
# CONTINUITY.md — Master Brain (Global Patterns)
Patterns that appear across ALL projects.
## Common Bug Patterns
### Pattern 1: Email Validation
- Root Cause: No regex or incomplete regex (missing .+@.+\..+)
- Appeared in: UserAPI (3 times), AuthService (2 times), TaskFlow (5 times)
- Fix Template: Use validator library (npm install validator), write unit tests
- Learning: Never write custom regex for validation. Always use library.
### Pattern 2: SQLite NULL Handling
- Root Cause: Forgot .notNull() constraint or != null check in code
- Appeared in: TaskTable (5 times), UserTable (3 times), ProjectTable (2 times)
- Fix Template: Add .notNull() to schema, check for null in handler, return default value
- Learning: SQLite default is NULL. Must be explicit about NOT NULL.
### Pattern 3: Async/Await Timing
- Root Cause: Missing await before promise-returning function
- Appeared in: API routes (7 times), Tests (4 times), Scripts (3 times)
- Fix Template: Always await db.prepare().run(), use eslint rule no-floating-promises
- Learning: Node 18+ is strict. Unhandled promise rejections crash process.File: CONTINUITY-TASKFLOW.md
# CONTINUITY-TASKFLOW.md — Project Brain (TaskFlow-Specific)
Bugs found specifically in TaskFlow codebase.
## TaskFlow Bug Log
#### Bug 1: POST /api/users — Email Validation Missing
- Date: 2024-01-15
- Commit: abc123def456
- Error: TypeError: Cannot read property 'domain' of null
- Root Cause: validateEmail() checks length but not format. Email "not-an-email" passed validation.
- Fix: import validator from 'validator'; use validator.isEmail(email)
- Test Added: test/api/users-validation.test.js (covers: normal email, no @, missing domain)
- Related: Pattern 1 (Email Validation)
#### Bug 2: GET /api/tasks/:id — SQLite NULL Return
- Date: 2024-01-20
- Commit: def456ghi789
- Error: Cannot read property 'title' of undefined (when accessing null task)
- Root Cause: Task.findById() returns undefined when task deleted. Code assumes always object.
- Fix: Return { id: 123, title: null } instead of undefined. Add null check in handler.
- Test Added: test/api/tasks-deleted-task.test.js
- Related: Pattern 2 (SQLite NULL)Task 4: Demo — End-to-End Workflow
Step 1: Run Jira Sync
export JIRA_HOST=jira.company.com
export JIRA_USER=qa@company.com
export JIRA_API_TOKEN=xxx
export GITHUB_TOKEN=xxx
export PROJECT_KEY=TASKFLOW
node scripts/jira-sync.jsOutput:
✅ Found 5 open issues
✅ Created: TASKFLOW-123 ← #42
✅ Created: TASKFLOW-124 ← #43
✅ Saved sync mapStep 2: Compile Knowledge Base
node scripts/compile-knowledge-base.jsOutput:
✅ Read CONTINUITY.md (2,500 chars)
✅ Read CONTINUITY-TASKFLOW.md (3,200 chars)
✅ Found 3 patterns, 2 bugs
✅ Saved lessons.md (8,900 chars)Step 3: Upload to NotebookLM
- Open https://notebooklm.google.com
- Create notebook
- Copy lessons.md content → paste into notebook
- Click "Generate" → select "Podcast"
- Listen to AI-generated podcast about TaskFlow QA patterns
Expected Podcast:
Episode 1: "The Three Most Common TaskFlow Bugs"
Duration: 18 min
Host: "Today we're covering the three patterns that keep breaking
TaskFlow tests. First up: email validation..."
[AI explains Pattern 1 with real TaskFlow examples]
[AI explains Pattern 2 with code snippets]
[AI explains Pattern 3 with test cases]
Host: "Remember these three patterns for next sprint!"Kiến Thức Áp Dụng
cm-continuity: Working Memory Protocol
Session 1 (Week 1):
Test fails with email bug
→ Analyze (3,000 tokens)
→ Save to CONTINUITY.md (50 tokens)
Session 2 (Week 3):
Similar email bug appears
→ Lookup CONTINUITY.md (50 tokens) ✓
→ Match pattern (10 tokens)
→ Fix immediately
Savings: 2,990 tokens (90% reduction!)cm-notebooklm: Knowledge Multiplication
CONTINUITY.md (raw data)
↓
lessons.md (structured)
↓
NotebookLM generates:
- Podcast (audio learning)
- Study guide (reading)
- Flashcards (memorization)
- Summary (quick ref)
Result: 1 input → 4 formats📝 Quiz (5 câu)
- Lifecycle: Nêu 4 trạng thái của issue từ auto-created đến resolved
- Master vs Project Brain: Khác nhau gì giữa CONTINUITY.md (master) vs CONTINUITY-TASKFLOW.md (project)?
- Token Savings: Nếu bug đầu tiên tốn 3,000 tokens, sau khi lưu pattern, bug tương tự tốn bao nhiêu?
- NotebookLM: Có bao nhiêu format output từ NotebookLM? Nêu tên.
- Jira Sync: Script sync dùng HTTP method nào để tạo issue trong Jira?
🏠 Homework (30 phút)
- Create CONTINUITY.md: Document 3 patterns từ previous lessons (email, NULL, async)
- Log a bug: Create CONTINUITY-TASKFLOW.md with 1 real TaskFlow bug
- Run compile: node scripts/compile-knowledge-base.js → generate lessons.md
- Upload NotebookLM: Paste lessons.md → generate podcast
- Listen & Review: Listen to AI podcast, verify accuracy
📚 Resources
- NotebookLM: https://notebooklm.google.com
- Jira REST API: https://developer.atlassian.com/cloud/jira/platform/rest/v3/
- GitHub Issues API: https://docs.github.com/en/rest/issues
- marked.js: https://marked.js.org/
🎯 Long-Term Impact
By End of Course (Session 16):
- Session 14: Auto-create issues from tests
- Session 15: Auto-review code quality
- Session 16: Auto-learn patterns for future issues
Improvement Curve:
Week 1: Manual QA (8 hours/day)
↓
Week 4: AI-assisted (4 hours/day)
↓
Week 8: AI-native (1 hour/day, 90% automated)
↓
Week 12: AI learning loop
(Gets better each sprint)Token Economics:
- Manual debugging: 3,000 tokens/bug × 10 bugs/month = 30,000 tokens
- AI with memory: 50 tokens (lookup) × 8 bugs + 3,000 tokens (new pattern) = 3,400 tokens
- Savings: 26,600 tokens (89% reduction) per month
🎓 Reflections
What Changed Over 16 Sessions
| Aspect | Session 1 | Session 16 |
|---|---|---|
| Test Speed | Manual | CI/CD |
| Debugging | Guesswork | Scientific (Phase 1) |
| Code Review | Spotting | Automated scanning |
| Knowledge | Per-project | Global patterns |
| AI Learning | Stateless | Memory-based |
| Time/Bug | 15 min | 30 sec auto + 5 min fix |
Takeaway
QA automation isn't about replacing humans. It's about building memory systems so each bug teaches the next generation of tests.
┌─ Old QA ─┐
│ Bug #1 │ → Fix → Forget
│ Bug #2 │ → Fix → Forget (same pattern!)
│ Bug #3 │ → Fix → Forget
└──────────┘
┌─ New QA ─┐
│ Bug #1 │ → Fix → Save pattern
│ Bug #2 │ → Lookup pattern → Fix 10x faster
│ Bug #3 │ → Lookup pattern → Fix 10x faster
│ Bug #4 │ → Lookup → Prevent before it happens!
└──────────┘Welcome to AI-native QA.