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>
97 lines
3.8 KiB
TypeScript
97 lines
3.8 KiB
TypeScript
/**
|
|
* Post-answer hook registry — 답변 완료 후 실행되는 부가 작업 모음.
|
|
*
|
|
* 새 hook 추가 = 1 객체 push. agent.ts 는 이 배열을 iterate 만 함.
|
|
*
|
|
* 현재 등록 순서 (v2.2.197):
|
|
* 1. devilRebuttal — Devil Agent 반박 카드 (비활성 시 silent skip)
|
|
* 2. postHocSelfCheck — 답변 검증 LLM 호출 (opt-in, 기본 OFF)
|
|
* 3. termValidator — 결정론적 글로서리 forbidden 검사 (기본 ON)
|
|
*/
|
|
|
|
import type { PostAnswerHook, PostAnswerHookContext } from './types';
|
|
import { maybeEmitDevilRebuttal as maybeEmitDevilRebuttalFn } from '../llm/devilRebuttal';
|
|
import { postHocSelfCheck, formatSelfCheckFooter, DEFAULT_SELF_CHECK_OPTIONS } from '../postHocSelfCheck';
|
|
import { validateTermUsage, formatTermValidatorFooter } from '../termValidator';
|
|
import { getConfig } from '../../config';
|
|
|
|
const devilRebuttalHook: PostAnswerHook = {
|
|
id: 'devil-rebuttal',
|
|
runAsync: true,
|
|
async run(ctx: PostAnswerHookContext): Promise<void> {
|
|
await maybeEmitDevilRebuttalFn(
|
|
{
|
|
getAbortSignal: ctx.getAbortSignal,
|
|
callNonStreaming: ctx.callNonStreaming,
|
|
// agent.ts 에서 vscode.Webview 를 통과시키므로 실런타임 호환. 타입 cast 로 hook 일반화.
|
|
getWebview: ctx.getWebview as any,
|
|
},
|
|
{
|
|
userPrompt: ctx.userPrompt,
|
|
assistantAnswer: ctx.assistantAnswer,
|
|
baseUrl: ctx.baseUrl,
|
|
modelName: ctx.modelName,
|
|
contextLength: ctx.contextLength,
|
|
engine: ctx.engine,
|
|
},
|
|
);
|
|
},
|
|
};
|
|
|
|
const postHocSelfCheckHook: PostAnswerHook = {
|
|
id: 'self-check',
|
|
runAsync: true,
|
|
async run(ctx: PostAnswerHookContext): Promise<void> {
|
|
const cfg = getConfig();
|
|
if (!cfg.selfCheckEnabled) return;
|
|
if (!ctx.userPrompt.trim() || !ctx.assistantAnswer.trim()) return;
|
|
const model = (cfg.selfCheckModel || '').trim() || cfg.defaultModel;
|
|
if (!model || !cfg.ollamaUrl) return;
|
|
|
|
const result = await postHocSelfCheck(ctx.userPrompt, ctx.assistantAnswer, ctx.selfCheckSources, {
|
|
ollamaUrl: cfg.ollamaUrl,
|
|
model,
|
|
timeoutMs: (cfg.selfCheckTimeoutSec ?? 6) * 1000,
|
|
excerptLength: DEFAULT_SELF_CHECK_OPTIONS.excerptLength,
|
|
maxSources: DEFAULT_SELF_CHECK_OPTIONS.maxSources,
|
|
});
|
|
const footer = formatSelfCheckFooter(result, model);
|
|
ctx.getWebview()?.postMessage({ type: 'streamChunk', value: footer });
|
|
},
|
|
};
|
|
|
|
const termValidatorHook: PostAnswerHook = {
|
|
id: 'term-validator',
|
|
runAsync: false,
|
|
run(ctx: PostAnswerHookContext): void {
|
|
const cfg = getConfig();
|
|
if (cfg.termValidatorEnabled === false) return;
|
|
if (!ctx.assistantAnswer || !ctx.assistantAnswer.trim()) return;
|
|
const result = validateTermUsage(ctx.assistantAnswer, cfg.glossaryPath || '.astra/glossary.md');
|
|
if (!result.ran || result.dictionarySize === 0) return;
|
|
const footer = formatTermValidatorFooter(result);
|
|
if (footer) ctx.getWebview()?.postMessage({ type: 'streamChunk', value: footer });
|
|
},
|
|
};
|
|
|
|
export const POST_ANSWER_HOOKS: PostAnswerHook[] = [
|
|
devilRebuttalHook,
|
|
postHocSelfCheckHook,
|
|
termValidatorHook,
|
|
];
|
|
|
|
/** 모든 hook 을 안전하게 실행 — 한 hook 의 throw 가 다른 hook 막지 않음. */
|
|
export function runPostAnswerHooks(ctx: PostAnswerHookContext): void {
|
|
for (const hook of POST_ANSWER_HOOKS) {
|
|
try {
|
|
if (hook.runAsync) {
|
|
void Promise.resolve(hook.run(ctx)).catch(() => { /* swallow */ });
|
|
} else {
|
|
hook.run(ctx);
|
|
}
|
|
} catch { /* hook never breaks the turn */ }
|
|
}
|
|
}
|
|
|
|
export type { PostAnswerHook, PostAnswerHookContext } from './types';
|