refactor: v2.2.195-201 — slashRouter god-file 해체 (–95%) + 인프라 5개 추출

아키텍처 감사 결과 HIGH 2건 + MED 2건 + LOW 1건 — 7 라운드 정리 시리즈.
기능 변경 없음, 순수 구조 정리.

**slashRouter.ts: 4,174 → 201줄 (–3,973, –95%)**
**agent.ts: 1,617 → 1,551줄 (–66, –4%)**

v2.2.195: eventSourcedStore + SystemPromptBlock registry
  - createEventStore<E>(opts) — 4 store (customers/hire/runway/feedback) I/O 240줄 중복 제거
  - _turnCtx 5 named string field → 1 Map<string, string> (새 verification block 추가 25곳→1곳)
  - buildAstraModeSystemPrompt: 5 ternary gate + 5 위치 → 1 for-loop join

v2.2.196: trackers cluster split
  - src/features/teamops/handlers/_shared.ts (fmtKrw/parseAmount/daysUntil/parseTaskOwner/stageEmoji/STAGE_ORDER/TERMINAL_STAGES)
  - src/features/teamops/handlers/trackers.ts (runway/customers/hire)
  - src/features/teamops/handlers/index.ts (barrel)
  - extension.ts 에 side-effect import (순환 import 회피)

v2.2.197: mtimeFileCache + PostAnswerHook registry
  - src/lib/mtimeFileCache.ts — createMtimeFileCache<T>(name, parse) (terminologyBlock + termValidator 2-cache invariant 자동화)
  - src/agent/postAnswerHooks/{types,index}.ts — Devil/SelfCheck/TermValidator 3 _maybeX method → 1 runPostAnswerHooks(ctx) loop
  - agent.ts –66줄

v2.2.198: dashboards cluster split
  - src/features/teamops/handlers/dashboards.ts (morning/evening/cohort/weekly)

v2.2.199: coordination + communication clusters split
  - src/features/teamops/handlers/coordination.ts (task/decisions/onesie/blocked/standup)
  - src/features/teamops/handlers/communication.ts (draft/feedback)
  - callLmSynthesis export 노출 (communication 이 사용)
  - 옛 parseTaskOwner local 정의 삭제 (_shared.ts 사용)

v2.2.200: system cluster split
  - src/features/system/handlers.ts (memory/glossary/help)

v2.2.201: datacollect cluster split + LLM 인프라 추출
  - src/features/datacollect/handlers.ts (research/benchmark/youtube/blog/wikify/meet)
  - src/features/datacollect/llm.ts (callLmSynthesis + repairKoreanGlitches + bridgeErrorRemedy)
  - slashRouter import 4개로 축소: vscode/logInfo/getBridgeBaseUrl/bridgeErrorRemedy

**최종 slashRouter (201줄):**
- REGISTRY Map + registerSlashCommand/listSlashCommands/isSlashCommand
- handleSlashCommand (dispatcher + 에러 처리)
- Webview interface + chunk helper
- getRecentSlashCommands ring buffer (actionability scoring 용)

**미래 부담 감소 metrics:**
- 새 슬래시 명령: god-file 끝에 함수 + register → 1 파일 + 1 register call
- 새 verification block: 5곳 편집 → 1 set call
- 새 event store: 60줄 boilerplate → createEventStore 한 줄
- 새 post-answer hook: 3 step → 1 push
- 새 mtime cache: Map + invariant 관리 → createMtimeFileCache 한 줄

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 11:55:22 +09:00
parent 15a34e0889
commit 7bec20620a
40 changed files with 4784 additions and 4545 deletions
+83
View File
@@ -0,0 +1,83 @@
/**
* Generic event-sourced store — append-only `.jsonl` 파일 1개를 읽고/쓰는 공통 기반.
*
* 배경: customers, hire, runway, feedback 4개 store 가 같은 패턴 4번 반복
* (getXFilePath / readX / appendX / countX) — byte-for-byte 중복 ~240줄.
* 한 곳에서 잡으면 BOM/인코딩 edge case 등 fix 도 한 번에 전파.
*
* 사용:
* const store = createEventStore<CustomerEvent>({
* relPath: '.astra/customers.jsonl',
* validate: (e) => typeof e.id === 'string' && typeof e.customerId === 'string',
* });
* store.read(); store.append(event); store.count(); store.getFilePath();
*
* 도메인별 로직 (computeStates 등) 은 그대로 도메인 파일에 남음 — 본 모듈은
* I/O 만 추상화.
*/
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
export interface EventStoreOptions<E> {
/** workspace root 기준 상대 경로. 예: '.astra/customers.jsonl'. */
relPath: string;
/** 파싱된 객체가 유효한 event 인지 판정. false 면 malformed 로 skip. */
validate: (e: unknown) => e is E;
}
export interface EventStore<E> {
getFilePath(): string | null;
read(): E[];
append(event: E): { ok: true; filePath: string } | { ok: false; error: string };
count(): number;
}
export function createEventStore<E>(opts: EventStoreOptions<E>): EventStore<E> {
function getFilePath(): string | null {
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) return null;
return path.join(folders[0].uri.fsPath, opts.relPath);
}
function read(): E[] {
const fp = getFilePath();
if (!fp || !fs.existsSync(fp)) return [];
const out: E[] = [];
let content = '';
try { content = fs.readFileSync(fp, 'utf-8'); } catch { return []; }
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const parsed = JSON.parse(trimmed);
if (opts.validate(parsed)) out.push(parsed);
} catch { /* skip malformed — append-only 라 손상 1줄이 전체 무력화하면 안 됨 */ }
}
return out;
}
function append(event: E): { ok: true; filePath: string } | { ok: false; error: string } {
const fp = getFilePath();
if (!fp) return { ok: false, error: '워크스페이스 폴더가 없어 저장 불가. VS Code 에서 폴더를 열어 주세요.' };
try {
fs.mkdirSync(path.dirname(fp), { recursive: true });
fs.appendFileSync(fp, JSON.stringify(event) + '\n', 'utf-8');
return { ok: true, filePath: fp };
} catch (e: any) {
return { ok: false, error: e?.message || String(e) };
}
}
function count(): number {
const fp = getFilePath();
if (!fp || !fs.existsSync(fp)) return 0;
try {
const content = fs.readFileSync(fp, 'utf-8');
return content.split('\n').filter((l) => l.trim()).length;
} catch { return 0; }
}
return { getFilePath, read, append, count };
}