aiwf
Version:
AI Workflow Framework for Claude Code with multi-language support (Korean/English)
502 lines (422 loc) • 12.9 kB
JavaScript
/**
* 컨텍스트 실시간 업데이트 관리자
* 페르소나 컨텍스트 파일의 변경을 감지하고 실시간으로 반영하는 모듈
*/
const fs = require('fs');
const path = require('path');
const EventEmitter = require('events');
const chokidar = require('chokidar');
const ContextRuleParser = require('./context-rule-parser');
class ContextUpdateManager extends EventEmitter {
constructor() {
super();
this.contextParser = new ContextRuleParser();
this.watchers = new Map();
this.updateQueue = [];
this.isProcessing = false;
this.pollingInterval = null;
this.lastUpdateTimes = new Map();
}
/**
* 컨텍스트 파일 감시 시작
* @param {Array} personas - 감시할 페르소나 목록
* @param {Object} options - 감시 옵션
*/
startWatching(personas = ['architect', 'frontend', 'backend', 'data_analyst', 'security'], options = {}) {
const watchOptions = {
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 1000,
pollInterval: 100
},
...options
};
personas.forEach(persona => {
const contextPath = path.join(
process.cwd(),
'.aiwf',
'07_AI_PERSONAS',
persona,
'context_rules.md'
);
try {
// 파일이 존재하는지 확인
if (!fs.existsSync(contextPath)) {
this.createDefaultContextFile(persona, contextPath);
}
// Chokidar 감시자 생성
const watcher = chokidar.watch(contextPath, watchOptions);
watcher
.on('change', () => this.handleFileChange(persona, contextPath))
.on('add', () => this.handleFileAdd(persona, contextPath))
.on('unlink', () => this.handleFileRemove(persona, contextPath))
.on('error', error => this.handleWatchError(persona, error));
this.watchers.set(persona, watcher);
console.log(`컨텍스트 감시 시작: ${persona}`);
} catch (error) {
console.error(`감시 설정 실패 (${persona}):`, error);
// 폴백: 폴링 방식으로 전환
this.startPolling(persona, contextPath);
}
});
// 이벤트 리스너 설정
this.setupEventHandlers();
}
/**
* 기본 컨텍스트 파일 생성
* @param {string} persona - 페르소나 이름
* @param {string} filePath - 파일 경로
*/
createDefaultContextFile(persona, filePath) {
const defaultContexts = {
architect: `---
persona_name: architect
analysis_approach: 시스템 설계 중심
design_principles:
- 확장성
- 유지보수성
- 성능
communication_style: 구조적이고 논리적
---
# 아키텍트 페르소나 컨텍스트
## 핵심 역할
시스템 전체 구조를 설계하고 기술적 결정을 내리는 역할을 수행합니다.
## 분석 접근법
- 전체 시스템 관점에서 문제 분석
- 컴포넌트 간 상호작용 고려
- 장기적 확장성과 유지보수성 중시
## 소통 스타일
- 다이어그램과 구조도를 활용한 설명
- 기술적 트레이드오프 명확히 제시
- 단계별 구현 계획 제공`,
frontend: `---
persona_name: frontend
analysis_approach: UI/UX 중심
design_principles:
- 사용성
- 접근성
- 반응성
communication_style: 시각적이고 직관적
---
# 프론트엔드 페르소나 컨텍스트
## 핵심 역할
사용자 인터페이스와 경험을 설계하고 구현하는 역할을 수행합니다.
## 분석 접근법
- 사용자 관점에서 문제 접근
- 시각적 요소와 인터랙션 중시
- 다양한 디바이스와 브라우저 고려
## 소통 스타일
- 목업과 프로토타입 활용
- 사용자 시나리오 기반 설명
- 직관적이고 이해하기 쉬운 표현`,
backend: `---
persona_name: backend
analysis_approach: API 및 데이터 처리 중심
design_principles:
- 보안
- 효율성
- 확장성
communication_style: 기술적이고 정확함
---
# 백엔드 페르소나 컨텍스트
## 핵심 역할
서버 사이드 로직과 데이터 처리를 담당하는 역할을 수행합니다.
## 분석 접근법
- 데이터 흐름과 처리 로직 중심
- 성능과 보안 최적화 고려
- API 설계와 데이터베이스 구조 중시
## 소통 스타일
- API 문서와 데이터 스키마 제공
- 기술적 세부사항 명확히 전달
- 성능 지표와 보안 고려사항 강조`,
data_analyst: `---
persona_name: data_analyst
analysis_approach: 데이터 기반 의사결정
design_principles:
- 정확성
- 통찰력
- 시각화
communication_style: 분석적이고 통계적
---
# 데이터 분석가 페르소나 컨텍스트
## 핵심 역할
데이터를 분석하여 인사이트를 도출하고 의사결정을 지원하는 역할을 수행합니다.
## 분석 접근법
- 정량적 데이터 분석 우선
- 통계적 방법론 적용
- 시각화를 통한 인사이트 전달
## 소통 스타일
- 차트와 그래프 활용
- 데이터 기반 근거 제시
- 객관적이고 중립적인 관점`,
security: `---
persona_name: security
analysis_approach: 보안 취약점 중심
design_principles:
- 보안성
- 무결성
- 기밀성
communication_style: 신중하고 방어적
---
# 보안 페르소나 컨텍스트
## 핵심 역할
시스템의 보안 취약점을 식별하고 보호 방안을 제시하는 역할을 수행합니다.
## 분석 접근법
- 위협 모델링과 취약점 분석
- 보안 모범 사례 적용
- 컴플라이언스 요구사항 고려
## 소통 스타일
- 위험도 평가와 우선순위 제시
- 구체적인 보안 대책 제안
- 예방적 접근과 지속적 모니터링 강조`
};
const content = defaultContexts[persona] || `---
persona_name: ${persona}
analysis_approach: 일반적 접근
design_principles:
- 품질
- 효율성
communication_style: 표준적
---
# ${persona} 페르소나 컨텍스트
기본 페르소나 컨텍스트입니다.`;
// 디렉토리 생성
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// 파일 생성
fs.writeFileSync(filePath, content, 'utf-8');
console.log(`기본 컨텍스트 파일 생성: ${persona}`);
}
/**
* 파일 변경 처리
* @param {string} persona - 페르소나 이름
* @param {string} filePath - 파일 경로
*/
async handleFileChange(persona, filePath) {
console.log(`컨텍스트 변경 감지: ${persona}`);
// 업데이트 큐에 추가
this.updateQueue.push({
type: 'change',
persona,
filePath,
timestamp: new Date().toISOString()
});
// 큐 처리
this.processUpdateQueue();
}
/**
* 파일 추가 처리
* @param {string} persona - 페르소나 이름
* @param {string} filePath - 파일 경로
*/
handleFileAdd(persona, filePath) {
console.log(`컨텍스트 파일 추가: ${persona}`);
this.updateQueue.push({
type: 'add',
persona,
filePath,
timestamp: new Date().toISOString()
});
this.processUpdateQueue();
}
/**
* 파일 제거 처리
* @param {string} persona - 페르소나 이름
* @param {string} filePath - 파일 경로
*/
handleFileRemove(persona, filePath) {
console.log(`컨텍스트 파일 제거: ${persona}`);
this.updateQueue.push({
type: 'remove',
persona,
filePath,
timestamp: new Date().toISOString()
});
this.processUpdateQueue();
}
/**
* 감시 오류 처리
* @param {string} persona - 페르소나 이름
* @param {Error} error - 오류 객체
*/
handleWatchError(persona, error) {
console.error(`감시 오류 (${persona}):`, error);
// 폴링으로 전환
const contextPath = path.join(
process.cwd(),
'.aiwf',
'07_AI_PERSONAS',
persona,
'context_rules.md'
);
this.startPolling(persona, contextPath);
}
/**
* 폴링 방식으로 파일 감시
* @param {string} persona - 페르소나 이름
* @param {string} filePath - 파일 경로
*/
startPolling(persona, filePath) {
const pollInterval = 5000; // 5초마다 확인
const checkFile = async () => {
try {
const stats = await fs.promises.stat(filePath);
const lastModified = stats.mtimeMs;
const previousModified = this.lastUpdateTimes.get(persona);
if (previousModified && lastModified !== previousModified) {
this.handleFileChange(persona, filePath);
}
this.lastUpdateTimes.set(persona, lastModified);
} catch (error) {
// 파일이 없으면 생성
if (error.code === 'ENOENT') {
this.createDefaultContextFile(persona, filePath);
}
}
};
// 기존 인터벌 정리
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
// 새 인터벌 설정
this.pollingInterval = setInterval(checkFile, pollInterval);
console.log(`폴링 모드로 전환: ${persona}`);
}
/**
* 업데이트 큐 처리
*/
async processUpdateQueue() {
if (this.isProcessing || this.updateQueue.length === 0) {
return;
}
this.isProcessing = true;
while (this.updateQueue.length > 0) {
const update = this.updateQueue.shift();
try {
await this.processUpdate(update);
} catch (error) {
console.error('업데이트 처리 오류:', error);
this.emit('error', { update, error });
}
}
this.isProcessing = false;
}
/**
* 개별 업데이트 처리
* @param {Object} update - 업데이트 정보
*/
async processUpdate(update) {
const { type, persona, filePath, timestamp } = update;
switch (type) {
case 'change':
case 'add':
// 컨텍스트 다시 파싱
const newContext = await this.contextParser.parseContextRules(persona);
// 캐시 무효화
this.contextParser.clearCache();
// 이벤트 발생
this.emit('contextUpdated', {
persona,
context: newContext,
updateType: type,
timestamp
});
console.log(`컨텍스트 업데이트 완료: ${persona}`);
break;
case 'remove':
// 기본 컨텍스트로 복원
this.createDefaultContextFile(persona, filePath);
this.emit('contextRemoved', {
persona,
timestamp
});
break;
}
}
/**
* 이벤트 핸들러 설정
*/
setupEventHandlers() {
// 컨텍스트 업데이트 시 로깅
this.on('contextUpdated', (data) => {
console.log(`[이벤트] 컨텍스트 업데이트: ${data.persona} (${data.updateType})`);
});
// 오류 발생 시 로깅
this.on('error', (data) => {
console.error(`[이벤트] 오류 발생:`, data.error);
});
}
/**
* 모든 감시 중지
*/
stopWatching() {
// Chokidar 감시자 정리
this.watchers.forEach((watcher, persona) => {
watcher.close();
console.log(`감시 중지: ${persona}`);
});
this.watchers.clear();
// 폴링 인터벌 정리
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
this.pollingInterval = null;
}
// 큐 초기화
this.updateQueue = [];
this.isProcessing = false;
}
/**
* 특정 페르소나 감시 중지
* @param {string} persona - 페르소나 이름
*/
stopWatchingPersona(persona) {
const watcher = this.watchers.get(persona);
if (watcher) {
watcher.close();
this.watchers.delete(persona);
console.log(`감시 중지: ${persona}`);
}
}
/**
* 감시 상태 확인
* @returns {Object} 감시 상태 정보
*/
getWatchStatus() {
const activeWatchers = Array.from(this.watchers.keys());
const queueLength = this.updateQueue.length;
const isPolling = this.pollingInterval !== null;
return {
activeWatchers,
watcherCount: activeWatchers.length,
queueLength,
isProcessing: this.isProcessing,
isPolling,
lastUpdateTimes: Object.fromEntries(this.lastUpdateTimes)
};
}
/**
* 강제 업데이트 트리거
* @param {string} persona - 페르소나 이름
*/
forceUpdate(persona) {
const contextPath = path.join(
process.cwd(),
'.aiwf',
'07_AI_PERSONAS',
persona,
'context_rules.md'
);
this.handleFileChange(persona, contextPath);
}
/**
* 모든 페르소나 강제 업데이트
*/
forceUpdateAll() {
const personas = Array.from(this.watchers.keys());
personas.forEach(persona => this.forceUpdate(persona));
}
}
module.exports = ContextUpdateManager;