7bec20620a
아키텍처 감사 결과 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>
126 lines
4.5 KiB
TypeScript
126 lines
4.5 KiB
TypeScript
/**
|
|
* 채용 파이프라인 트래커.
|
|
*
|
|
* 4인 → 5인 이상 확장 시점에 후보자가 여러 명, 여러 역할(개발/기획/디자인) 로
|
|
* 들어오기 시작 — 노션·스프레드시트·이메일에 흩어진 정보를 한 명령으로 본다.
|
|
*
|
|
* Event-sourced (customersStore 와 동일 패턴) — append-only 이벤트 로그를
|
|
* 재생해 후보자별 현재 단계 + 노트 누적 도출.
|
|
*
|
|
* 위치: `<workspace>/.astra/hire.jsonl`. 사람 직접 편집 가능.
|
|
* 민감 정보(이름, 연봉, 거절 사유) 포함 — 로컬 only.
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { createEventStore } from '../_shared/eventSourcedStore';
|
|
|
|
const STORE_REL_PATH = '.astra/hire.jsonl';
|
|
|
|
export type HireEventType = 'add' | 'stage' | 'note' | 'offer' | 'reject' | 'decline' | 'hire';
|
|
|
|
/**
|
|
* 기본 파이프라인 단계. 사용자가 다른 단계명 지정 가능 — 그냥 문자열로 저장.
|
|
* 표시 순서·정렬용으로 알려진 단계는 가중치 부여.
|
|
*/
|
|
export const KNOWN_STAGES = ['inbox', 'screened', 'interview', 'final', 'offer', 'accepted', 'hired', 'rejected', 'declined'] as const;
|
|
export type KnownStage = typeof KNOWN_STAGES[number];
|
|
|
|
export interface HireEvent {
|
|
id: string;
|
|
timestamp: string;
|
|
candidateId: string;
|
|
candidateName: string;
|
|
role: string;
|
|
type: HireEventType;
|
|
/** stage 전환 시 새 단계. add 시 시작 단계 (기본 'inbox'). */
|
|
stage?: string;
|
|
/** offer 의 연봉 — KRW. */
|
|
salary?: number;
|
|
/** 입사 예정일 / 거절·이탈 사유. */
|
|
memo?: string;
|
|
}
|
|
|
|
export interface CandidateState {
|
|
candidateId: string;
|
|
candidateName: string;
|
|
role: string;
|
|
stage: string;
|
|
salary?: number;
|
|
addedAt: string;
|
|
lastEventAt: string;
|
|
eventCount: number;
|
|
notes: { timestamp: string; type: HireEventType; memo: string }[];
|
|
}
|
|
|
|
const _store = createEventStore<HireEvent>({
|
|
relPath: STORE_REL_PATH,
|
|
validate: (e): e is HireEvent => !!e
|
|
&& typeof (e as any).id === 'string'
|
|
&& typeof (e as any).candidateId === 'string'
|
|
&& typeof (e as any).type === 'string',
|
|
});
|
|
|
|
export const getHireFilePath = _store.getFilePath;
|
|
export const readHireEvents = _store.read;
|
|
export const appendHireEvent = _store.append;
|
|
|
|
export function candidateIdFromName(name: string): string {
|
|
return name.toLowerCase().trim().replace(/\s+/g, '-').replace(/[^\w가-힣-]/g, '');
|
|
}
|
|
|
|
export function computeCandidateStates(): Map<string, CandidateState> {
|
|
const events = readHireEvents().slice().sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
const states = new Map<string, CandidateState>();
|
|
for (const e of events) {
|
|
let s = states.get(e.candidateId);
|
|
if (!s) {
|
|
s = {
|
|
candidateId: e.candidateId,
|
|
candidateName: e.candidateName,
|
|
role: e.role || '',
|
|
stage: e.stage || 'inbox',
|
|
addedAt: e.timestamp,
|
|
lastEventAt: e.timestamp,
|
|
eventCount: 0,
|
|
notes: [],
|
|
};
|
|
states.set(e.candidateId, s);
|
|
}
|
|
s.candidateName = e.candidateName || s.candidateName;
|
|
s.role = e.role || s.role;
|
|
s.lastEventAt = e.timestamp;
|
|
s.eventCount += 1;
|
|
|
|
switch (e.type) {
|
|
case 'add':
|
|
s.stage = e.stage || s.stage || 'inbox';
|
|
break;
|
|
case 'stage':
|
|
if (e.stage) s.stage = e.stage;
|
|
break;
|
|
case 'offer':
|
|
s.stage = 'offer';
|
|
if (e.salary !== undefined) s.salary = e.salary;
|
|
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'offer', memo: e.memo });
|
|
break;
|
|
case 'hire':
|
|
s.stage = 'hired';
|
|
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'hire', memo: e.memo });
|
|
break;
|
|
case 'reject':
|
|
s.stage = 'rejected';
|
|
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'reject', memo: e.memo });
|
|
break;
|
|
case 'decline':
|
|
s.stage = 'declined';
|
|
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'decline', memo: e.memo });
|
|
break;
|
|
case 'note':
|
|
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'note', memo: e.memo });
|
|
break;
|
|
}
|
|
}
|
|
return states;
|
|
}
|