feat: v2.2.173-193 — 4인 팀 운영 슬래시 13개 + ASTRA 검증 엔진 6종

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>
This commit is contained in:
2026-05-29 16:05:30 +09:00
parent f3439ddad5
commit 990ea0ae5f
46 changed files with 7172 additions and 136 deletions
+135
View File
@@ -0,0 +1,135 @@
/**
* 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 };
}