990ea0ae5f
4인 팀 운영 슬래시 (v2.2.173~189):
- 일과 리듬: /morning, /evening, /weekly, /standup
- 트래커 (event-sourced .astra/*.jsonl): /runway, /customers, /hire
- 작업·결정: /task, /blocked, /onesie, /decisions
- 외부 출력: /draft, /feedback
- 분석: /cohort (MoM 추세)
ASTRA 추론·검색 엔진 (v2.2.183~192):
- v2.2.183 Conflict Surface — scoring.conflictSeverity 를 [CONFLICT WARNINGS] 블록으로
서피스 + 교차-문서 발산(Jaccard) 감지
- v2.2.184 Chain-of-Verification — [VERIFICATION CHECKLIST] 답변 작성 전 그라운딩 자기 점검
(instructional, strictMode 옵션)
- v2.2.185 Actionability Scoring — 최근 슬래시 명령 + 열린 파일 신호로 검색 결과 재가중
- v2.2.186 Temporal Markers + Distillation Loop — LongTerm/Episodic 만료 필터 +
30일+ stale episode → LongTerm 'episode-digest' 승급 (수동 /memory distill + 세션 종료 자동)
- v2.2.187 Hierarchical Context Window + LLM Semantic Re-rank — 3-level 추상도 매칭
+ 토큰 예산 통과 후 LLM 1회로 의도-부합 재정렬 (opt-in)
- v2.2.190 Intent Clarification + Citation Trace — 모호 차원 감지 시 역질문 우선
+ 답변 끝 사용 출처 한 줄 정리
- v2.2.191 Post-hoc Self-Check — 답변 완료 후 별도 LLM 호출 1회로 답함/그라운딩/모순 평가,
footer 한 줄로 표시 (opt-in, semantic re-rank 와 같은 안전 fallback 패턴)
- v2.2.192 Terminology Dictionary — .astra/glossary.md 사용자 편집 파일 + Term Check
지침 통합 + /glossary init/path/reload
- v2.2.193 /help — 카테고리별 명령 목록 + 6종 verification 블록 현재 on/off
신규 모듈:
- src/retrieval/{conflictBlock,coveBlock,actionabilityScoring,hierarchicalLevel,
semanticRerank,intentClarification,citationTrace,terminologyBlock}.ts
- src/memory/distillation.ts + types.ts 에 expiresAt/promoted/episode-digest 추가
- src/agent/postHocSelfCheck.ts
- src/features/{customers,feedback,hire,runway}/*.ts (event-sourced stores)
ASTRA 검증 5종 자동 주입 (buildAstraModeSystemPrompt, casual 모드 제외):
[INTENT CLARIFICATION GUIDANCE] (답변 시작 전) → [TERMINOLOGY DICTIONARY] +
[CONFLICT WARNINGS] + [VERIFICATION CHECKLIST] (작성 중) → [CITATION TRACE] (끝)
+ 6번째: Post-hoc Self-Check footer (답변 완료 후, opt-in)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
136 lines
5.8 KiB
TypeScript
136 lines
5.8 KiB
TypeScript
/**
|
||
* Hierarchical Context Window — 질의·문서의 *추상도 레벨* 매칭으로 검색 노이즈 감소.
|
||
*
|
||
* 사용자 제안: "사용자가 '배포해줘' 라고 하면 L1(실행) 우선, '전략 검토' 라고 하면
|
||
* L3(전략) 우선". 같은 키워드 매치 점수여도 추상도가 안 맞으면 noise.
|
||
*
|
||
* v1 — 3-level 휴리스틱 (LLM 호출 없음, 결정적):
|
||
* - `concrete` — 코드, 로그, 디버그, 실행 명령
|
||
* - `operational` — 작업, 일정, 운영 절차, 회의록 (기본/기본값)
|
||
* - `strategic` — 전략, 비전, 의사결정 근거, 아키텍처 방향
|
||
*
|
||
* 매칭 정책:
|
||
* - 같은 레벨 → 보너스 (× 1.15)
|
||
* - 인접 레벨 (concrete↔operational, operational↔strategic) → 변화 없음
|
||
* - 양 끝 mismatch (concrete↔strategic) → 페널티 (× 0.7)
|
||
*
|
||
* 약한 시그널 — TF-IDF dominant 유지, 동점 깨기 역할. 검색 결과를 *제외* 하지 않음.
|
||
*/
|
||
|
||
import { RetrievalChunk } from './types';
|
||
|
||
export type AbstractionLevel = 'concrete' | 'operational' | 'strategic';
|
||
|
||
const QUERY_STRATEGIC_KEYWORDS = [
|
||
'전략', '방향', '비전', '미션', '목표', '의사결정', '아키텍처', '설계 방향',
|
||
'왜 이렇게', '왜 그렇게', '뭐가 맞', '어떤 게 좋', '어떻게 가야', '어떤 방향',
|
||
'판단', '결정', '관점', '평가', '검토',
|
||
'strategy', 'vision', 'mission', 'roadmap', 'direction', 'goal',
|
||
'why', 'rationale', 'pros and cons', 'tradeoff', 'evaluate',
|
||
];
|
||
|
||
const QUERY_CONCRETE_KEYWORDS = [
|
||
'코드', '함수', '버그', '에러', '로그', '실행', '명령어', '스크립트', '디버그',
|
||
'고쳐', '수정', '리팩토링', '리팩터', '커밋', '머지', '배포해', '돌려',
|
||
'에러 메시지', '스택 트레이스', 'syntax', 'compile',
|
||
'code', 'function', 'bug', 'error', 'log', 'execute', 'command', 'script',
|
||
'debug', 'fix', 'refactor', 'commit', 'merge', 'deploy', 'run',
|
||
];
|
||
|
||
const FOLDER_STRATEGIC_HINTS = ['strategy', 'vision', 'mission', 'roadmap', 'decision', 'principle', '전략', '비전'];
|
||
const FOLDER_OPERATIONAL_HINTS = ['playbook', 'runbook', 'operation', 'process', 'sop', '운영', '절차', 'meeting', '회의'];
|
||
const FOLDER_CONCRETE_HINTS = ['code', 'snippet', 'log', 'debug', 'fix', 'patch', '디버그', 'commit'];
|
||
|
||
const TITLE_STRATEGIC_HINTS = ['strategy', 'vision', 'rationale', 'direction', 'decision', 'plan', '전략', '계획', '방향', '결정', '평가'];
|
||
const TITLE_CONCRETE_HINTS = ['fix', 'bug', 'error', 'log', 'script', 'command', '버그', '에러', '로그', '커밋'];
|
||
|
||
function countMatches(text: string, keywords: string[]): number {
|
||
const lower = text.toLowerCase();
|
||
let n = 0;
|
||
for (const k of keywords) if (lower.includes(k.toLowerCase())) n++;
|
||
return n;
|
||
}
|
||
|
||
/**
|
||
* 질의 추상도 분류. 키워드 카운트 우열로 결정, 동률·없음이면 'operational' (기본).
|
||
*/
|
||
export function classifyQueryLevel(query: string): AbstractionLevel {
|
||
if (!query) return 'operational';
|
||
const s = countMatches(query, QUERY_STRATEGIC_KEYWORDS);
|
||
const c = countMatches(query, QUERY_CONCRETE_KEYWORDS);
|
||
if (s > c && s >= 1) return 'strategic';
|
||
if (c > s && c >= 1) return 'concrete';
|
||
return 'operational';
|
||
}
|
||
|
||
/**
|
||
* 한 chunk 의 추상도 분류 — 폴더 경로 → 파일명/제목 → 본문 순으로 강도 감소.
|
||
* 어느 신호도 없으면 'operational' (기본).
|
||
*/
|
||
export function classifyChunkLevel(chunk: RetrievalChunk): AbstractionLevel {
|
||
// 1. 폴더 경로 (가장 강함)
|
||
const fp = (chunk.metadata?.filePath || '').toLowerCase();
|
||
if (fp) {
|
||
for (const h of FOLDER_STRATEGIC_HINTS) if (fp.includes(`/${h}`) || fp.includes(`\\${h}`)) return 'strategic';
|
||
for (const h of FOLDER_CONCRETE_HINTS) if (fp.includes(`/${h}`) || fp.includes(`\\${h}`)) return 'concrete';
|
||
for (const h of FOLDER_OPERATIONAL_HINTS) if (fp.includes(`/${h}`) || fp.includes(`\\${h}`)) return 'operational';
|
||
}
|
||
|
||
// 2. 제목
|
||
const t = (chunk.title || '').toLowerCase();
|
||
if (t) {
|
||
let strat = 0, conc = 0;
|
||
for (const h of TITLE_STRATEGIC_HINTS) if (t.includes(h.toLowerCase())) strat++;
|
||
for (const h of TITLE_CONCRETE_HINTS) if (t.includes(h.toLowerCase())) conc++;
|
||
if (strat > conc && strat >= 1) return 'strategic';
|
||
if (conc > strat && conc >= 1) return 'concrete';
|
||
}
|
||
|
||
return 'operational';
|
||
}
|
||
|
||
const LEVEL_INDEX: Record<AbstractionLevel, number> = {
|
||
concrete: 0, operational: 1, strategic: 2,
|
||
};
|
||
|
||
export interface HierarchicalWeights {
|
||
/** 같은 레벨 매치 multiplier. 기본 1.15. */
|
||
sameLevelBonus: number;
|
||
/** 양 끝 mismatch (concrete↔strategic) multiplier. 기본 0.70. */
|
||
farMismatchPenalty: number;
|
||
}
|
||
|
||
export const DEFAULT_HIERARCHICAL_WEIGHTS: HierarchicalWeights = {
|
||
sameLevelBonus: 1.15,
|
||
farMismatchPenalty: 0.70,
|
||
};
|
||
|
||
/**
|
||
* 질의 레벨에 따라 chunks 의 score 를 hierarchical 매칭으로 재가중. in-place.
|
||
* metadata 에 분류 결과 기록 (debug/UI 노출).
|
||
*/
|
||
export function applyHierarchicalReweight(
|
||
chunks: RetrievalChunk[],
|
||
queryLevel: AbstractionLevel,
|
||
weights: HierarchicalWeights = DEFAULT_HIERARCHICAL_WEIGHTS,
|
||
): { sameLevel: number; farMismatch: number } {
|
||
let sameLevel = 0;
|
||
let farMismatch = 0;
|
||
const qi = LEVEL_INDEX[queryLevel];
|
||
for (const c of chunks) {
|
||
const cl = classifyChunkLevel(c);
|
||
(c.metadata as any).abstractionLevel = cl;
|
||
const ci = LEVEL_INDEX[cl];
|
||
const diff = Math.abs(qi - ci);
|
||
if (diff === 0) {
|
||
c.score *= weights.sameLevelBonus;
|
||
sameLevel++;
|
||
} else if (diff === 2) {
|
||
c.score *= weights.farMismatchPenalty;
|
||
farMismatch++;
|
||
}
|
||
// diff === 1: 인접 레벨 → 변화 없음
|
||
}
|
||
return { sameLevel, farMismatch };
|
||
}
|