v2.2.299~307: 아키텍처 수렴 + 채팅 정리 + 벤치마킹 강화 + Claude 구독 엔진

- v2.2.299 아키텍처 수렴: coreChat 통일(엔진 휴리스틱 3벌 제거), 기업 모드
  검색 오케스트레이터 승격, lib/execUtil(실행 래퍼 6곳·Python 탐지 3벌 단일화),
  lib/kstSchedule(워처 4개 nowInKst 통합), estimateTokens 통합, 설정 접근 규칙 명문화
- v2.2.300 채팅 화면 정리: LiveReasoningFilter(스트리밍 중 <think>/Harmony 추론
  토큰 단위 차단), 확신도·검토요청 footer 기본 숨김(계산·Reflection 은 유지)
- v2.2.301 문맥·의도 이해: [답변 전 이해 원칙] 상시 주입, 워크플로우 의도 브리핑,
  Report QA 루프(규칙 레지스트리+실측치+회귀 게이트, 블로그_v3 개념 이식)
- v2.2.302 /benchmark 비즈니스 렌즈(가격·수익·운영)+빌드 프롬프트 모드+QA 연계
- v2.2.303 handoff 모드(측정치 무손실 인수인계 문서)+/claude(Claude Code 터미널 위임)
- v2.2.304 이식성: 지식 경로 두뇌-상대 규약(pickWikiDir 상대 해석), 이사 체크리스트
- v2.2.305 Claude 구독 엔진: claude: 프로바이더(CLI 위임, 모델 드롭다운 자동 노출,
  coreChat 지원 — 워크플로우·QA도 구독 모델 가능)
- v2.2.306 Tone Guard: AI 상투어 금지 레지스트리(상담사 화법 실사례 8종+대조 예시)
- v2.2.307 /benchmark 레이아웃 골격(sectionRoles 결정론 분석, 롤링 배너 즉답),
  파트별 실패 격리, 합성 타임아웃 120→300초

검증: tsc 무오류 + jest 888 통과 + esbuild 정상

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 21:02:15 +09:00
co-authored by Claude Fable 5
parent 98d533f045
commit 47b3b9f93a
66 changed files with 2421 additions and 459 deletions
+119 -13
View File
@@ -15,10 +15,15 @@ import { promises as fsp } from 'fs';
import * as path from 'path';
import { registerSlashCommand, chunk, type Webview } from './slashRouter';
import { callLmSynthesis } from './llm';
import { getConfig } from '../../config';
import { logInfo } from '../../utils';
import { runReportQaLoop } from '../../agents/reportQa';
import { bridgeFetch, BRIDGE_API } from './bridgeClient';
import { collectSourceFiles } from './reviewFiles';
import { buildReviewFilePrompt, buildReviewReducePrompt } from './prompts/reviewPrompt';
import { type SynthesisPart, buildSynthesisPrompt } from './prompts/synthesisPrompt';
import { buildHandoffDoc } from './prompts/handoffDoc';
import { buildLayoutSkeleton } from './prompts/layoutSkeleton';
import {
type YoutubeAnalysisMode,
formatHms,
@@ -49,28 +54,64 @@ import {
// ───────────────────────────── /benchmark ─────────────────────────────
async function runBenchmark(arg: string, view: Webview | undefined): Promise<boolean> {
/**
* /benchmark 모드 → 실행할 합성 파트 목록.
* - full(기본): 디자인 3파트 + 비즈니스 렌즈 (1~4)
* - design: 기존 3파트만 (원본 복원 시방서)
* - biz: 비즈니스 렌즈만 (빠름 — 포지셔닝·가격·운영)
* - build: 풀코스 + 스텝별 빌드 프롬프트 (1~5)
*/
export type BenchmarkMode = 'full' | 'design' | 'biz' | 'build' | 'handoff';
export const BENCHMARK_MODE_PARTS: Record<BenchmarkMode, ReadonlyArray<1 | 2 | 3 | 4 | 5>> = {
full: [1, 2, 3, 4],
design: [1, 2, 3],
biz: [4],
build: [1, 2, 3, 4, 5],
handoff: [], // LLM 합성 없음 — 스캔 원시 데이터로 인수인계 문서를 결정론 생성
};
/** /benchmark 인자 파싱 — 순수 함수 (테스트 대상). */
export function parseBenchmarkArgs(arg: string): {
url: string; depth?: number; pages?: number; mode: BenchmarkMode; userContent: string;
} {
const tokens = arg.trim().split(/\s+/).filter(Boolean);
let url = '';
let depthArg: number | undefined;
let pagesArg: number | undefined;
let depth: number | undefined;
let pages: number | undefined;
let mode: BenchmarkMode = 'full';
const restParts: string[] = [];
const BARE_MODES = new Set<BenchmarkMode>(['full', 'design', 'biz', 'build', 'handoff']);
for (const t of tokens) {
const m = /^(depth|pages)=(\d+)$/i.exec(t);
const modeM = /^mode[:=](full|design|biz|build|handoff)$/i.exec(t);
if (m) {
if (m[1].toLowerCase() === 'depth') depthArg = Number(m[2]);
else pagesArg = Number(m[2]);
if (m[1].toLowerCase() === 'depth') depth = Number(m[2]);
else pages = Number(m[2]);
} else if (modeM) {
mode = modeM[1].toLowerCase() as BenchmarkMode;
} else if (BARE_MODES.has(t.toLowerCase() as BenchmarkMode)) {
mode = t.toLowerCase() as BenchmarkMode;
} else if (!url) {
url = t;
} else {
restParts.push(t);
}
}
return { url, depth, pages, mode, userContent: restParts.join(' ') };
}
async function runBenchmark(arg: string, view: Webview | undefined): Promise<boolean> {
const { url, depth: depthArg, pages: pagesArg, mode, userContent } = parseBenchmarkArgs(arg);
if (!url) {
chunk(view, `사용법: \`/benchmark <url> [depth=N] [pages=N] [보조 설명]\`\n예: \`/benchmark https://example.com depth=2 pages=12\`\n`);
chunk(view, [
'사용법: `/benchmark <url> [mode] [depth=N] [pages=N] [보조 설명]`',
'- mode: `full`(기본 — 디자인+비즈니스) · `design`(복원 시방서만) · `biz`(사업 분석만) · `build`(풀코스+빌드 프롬프트) · `handoff`(클라우드 개발용 인수인계 문서 — LLM 없이 즉시)',
'- 가격·수익 분석이 목적이면 `depth=2` 로 pricing 페이지까지 크롤 권장',
'예: `/benchmark https://interiorai.com build depth=2 pages=12 나는 부동산 쪽으로 변형하고 싶어`',
'',
].join('\n'));
return true;
}
const userContent = restParts.join(' ');
const cfg = vscode.workspace.getConfiguration('g1nation');
const crawlDepth = depthArg ?? (cfg.get<number>('datacollectCrawlDepth', 1) ?? 1);
@@ -126,23 +167,84 @@ async function runBenchmark(arg: string, view: Webview | undefined): Promise<boo
'```',
].join('\n');
// ── handoff 모드: LLM 합성을 건너뛰고 스캔 원시 데이터로 인수인계 문서를 즉시 생성.
// 소형 모델의 요약 병목을 통과시키지 않는다 (측정치 무손실 — handoffDoc.ts 참조).
if (mode === 'handoff') {
const handoffDoc = buildHandoffDoc(s, url, { crawlDepth, maxPages, userContent });
chunk(view, `📦 **개발 인수인계 문서 생성** (LLM 미개입 — 측정치 원본)\n\n`);
const hToday = new Date().toISOString().slice(0, 10);
let hHost = url;
try { hHost = new URL(/^https?:\/\//i.test(url) ? url : `https://${url}`).host; } catch { /* keep raw */ }
const hTitle = `웹벤치마크 핸드오프 ${hHost} ${hToday}`;
const savedH = await persistWikiDoc(hTitle, handoffDoc, { docType: 'benchmark-handoff', sources: [url] });
chunk(view, describeSave(savedH, '인수인계 문서'));
if (savedH.ok && savedH.path) {
chunk(view, [
'',
'**다음 단계 — 클라우드 모델로 개발:**',
`1. 파일을 열어 §1 "내 요구사항" 을 채우세요 (무엇으로 변형할지).`,
`2. Claude Code(구독 인증)로 바로 개발: \`/claude ${savedH.path} 파일을 읽고 명세대로 개발해줘\``,
` (또는 Antigravity 에 파일 내용을 붙여넣어도 됩니다)`,
'',
].join('\n'));
}
return true;
}
let finalReport: string;
if (looksEmpty) {
chunk(view, `(스캔이 비어 LLM 합성을 건너뜁니다.)\n\n`);
finalReport = rawReport;
} else {
const model = (cfg.get<string>('defaultModel', '') || 'gemma4:e2b').trim();
chunk(view, `🧪 **LLM 4-렌즈 합성** (3단계 · 모델 \`${model}\`)\n모델·하드웨어에 따라 수 분 걸릴 수 있습니다…\n`);
const partList = BENCHMARK_MODE_PARTS[mode];
const PART_LABELS: Record<number, string> = {
1: '4-렌즈 디자인', 2: 'IA·토큰', 3: '재구축 명세', 4: '비즈니스 렌즈', 5: '빌드 프롬프트',
};
chunk(view, `🧪 **LLM 합성** (mode \`${mode}\` · ${partList.length}단계 · 모델 \`${model}\`)\n모델·하드웨어에 따라 수 분 걸릴 수 있습니다…\n`);
try {
const parts: string[] = [];
for (const part of [1, 2, 3] as const) {
chunk(view, `\n · 합성 ${part}/3 진행 중…`);
let okParts = 0;
for (let i = 0; i < partList.length; i++) {
const part = partList[i];
chunk(view, `\n · ${i + 1}/${partList.length} ${PART_LABELS[part]} 합성 중…`);
const partT0 = Date.now();
const out = await callLmSynthesis(buildSynthesisPrompt(s, userContent, part));
if (!out) throw new Error(`LLM ${part}/3 응답이 비어 있습니다.`);
// [v2.2.307] 파트별 실패 격리 — 한 파트가 죽어도 완성된 파트를 버리지 않는다
// (실사례: 파트 2 타임아웃으로 105초 걸린 파트 1까지 폐기). 타임아웃도 300초로 —
// 입력 JSON 이 큰 파트는 e4b 급에서 120초를 넘긴다.
let out: string;
try {
out = await callLmSynthesis(buildSynthesisPrompt(s, userContent, part), undefined, { timeoutMs: 300_000 });
} catch (partErr: any) {
chunk(view, ` ✗ (${Math.round((Date.now() - partT0) / 1000)}s — ${partErr?.message || partErr})`);
parts.push(`> ⚠️ **${PART_LABELS[part]} 파트 합성 실패**: ${partErr?.message || String(partErr)} — 나머지 파트는 계속 진행됨.`);
continue;
}
if (!out) {
chunk(view, ` ✗ (빈 응답)`);
parts.push(`> ⚠️ **${PART_LABELS[part]} 파트 합성 실패**: 빈 응답 — 나머지 파트는 계속 진행됨.`);
continue;
}
okParts++;
// [Report QA] 비즈니스 렌즈는 날조 위험(임의 가격·매출)이 가장 큰 파트 —
// 채점→피드백 재작성→회귀 게이트를 통과시킨다. prompt 로 합성 프롬프트
// 자체(스캔 JSON 포함)를 넘겨 재작성이 원본 근거를 보게 한다.
if (part === 4 && getConfig().reportQaEnabled) {
chunk(view, ` (품질 검증 중…)`);
const qa = await runReportQaLoop({
draft: out,
prompt: buildSynthesisPrompt(s, userContent, 4),
intentBrief: null,
model,
onProgress: (msg: string) => chunk(view, ` · ${msg}`),
});
out = qa.finalDraft;
logInfo('[Benchmark] 비즈니스 렌즈 QA 완료.', { score: qa.score, revisions: qa.revisions });
}
parts.push(out);
chunk(view, ` ✓ (${Math.round((Date.now() - partT0) / 1000)}s)`);
}
if (okParts === 0) throw new Error('모든 합성 파트가 실패했습니다.');
finalReport = parts.join('\n\n---\n\n');
chunk(view, `\n\n`);
} catch (e: any) {
@@ -151,6 +253,10 @@ async function runBenchmark(arg: string, view: Webview | undefined): Promise<boo
}
}
// [v2.2.307] 레이아웃 골격 (결정론) — 합성 성공/실패와 무관하게 항상 포함.
// "배너/캐러셀이 있는가, 각 페이지가 어떤 블록 순서로 구성되는가"에 즉답.
finalReport = `${buildLayoutSkeleton(s)}\n\n---\n\n${finalReport}`;
chunk(view, finalReport + '\n\n');
try {
@@ -1129,7 +1235,7 @@ async function reduceReviewNotes(
// /research(NotebookLM Deep Research)는 v2.2.205 에서 제거 — NotebookLM 은 로컬
// Datacollect 앱 전용으로 분리(Chrome/Google 로그인 의존). ASTRA 백엔드는 NAS 경량
// Bridge 로 운영 가능해야 하므로 brower-auth 가 필요한 명령은 두지 않는다.
registerSlashCommand({ name: '/benchmark', description: 'Playwright 웹 벤치마크 + 4-렌즈 LLM 분석', handler: runBenchmark });
registerSlashCommand({ name: '/benchmark', description: '웹 벤치마크 — 디자인 4-렌즈 + 비즈니스 렌즈(가격·수익·운영) 분석, build 모드는 스텝별 빌드 프롬프트까지', handler: runBenchmark });
registerSlashCommand({ name: '/youtube', description: 'YouTube 단일 영상 또는 채널/플레이리스트 분석', handler: runYoutube });
registerSlashCommand({ name: '/blog', description: 'Blog Pipeline 안내 (Datacollect 별도 흐름)', handler: runBlog });
registerSlashCommand({ name: '/wikify', description: '웹 URL → P-Reinforce v3.0 위키 합성·저장', handler: runWikify });