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
+156
View File
@@ -0,0 +1,156 @@
/**
* Actionability Scoring — 검색 결과를 "현재 작업 상태" 신호로 재가중.
*
* 기존 TF-IDF (단어 매칭) + recency (시간) 만으로는 "지금 이 사용자가 하고 있는
* 작업과 *직접 연결* 된 문서" 가 우선되지 않음. 예: 사용자가 `/runway` 명령을
* 막 실행했다면 runway / 재무 관련 문서가 같은 키워드 매치 점수여도 더 위로 와야 함.
*
* v1 신호 (사용자 선택):
* 1. **최근 슬래시 명령** — 마지막 N개 실행된 슬래시 명령 이름을 키워드로 활용
* → 명령 이름이 chunk title/content 에 포함되면 boost
* 2. **열린 파일 경로** — VS Code 활성 에디터의 파일 이름·확장자·부모 디렉터리를
* 키워드로 활용
*
* 점수 결합: TF-IDF normalized score (0~1) × (1 + actionabilityScore × weight)
* - weight 기본 0.3 → actionability=1.0 인 chunk 는 30% boost
* - actionability=0.0 인 chunk 는 변화 없음
* - TF-IDF 가 여전히 dominant 인 보수적 합산
*
* 향후 신호 (#1 v2 후보 — 사용자 선택 안 함):
* 3. 최근 7일 Chronicle ADR / decisions
* 4. 최근 24시간 customers/hire/runway 이벤트
*/
import * as path from 'path';
import * as vscode from 'vscode';
import { RetrievalChunk } from './types';
export interface WorkStateSignals {
/** 최근 실행된 슬래시 명령 이름 목록 ('/' 포함, 최신 순). 빈 배열이면 신호 없음. */
recentSlashCommands: string[];
/** VS Code 활성 에디터의 파일 절대 경로. undefined 면 신호 없음. */
openFilePath?: string;
}
export interface ActionabilityWeights {
/** 슬래시 명령 매치당 boost. 기본 0.30. */
slashCommandMatch: number;
/** 파일명 매치 boost. 기본 0.40 (가장 강함). */
openFileNameMatch: number;
/** 부모 디렉터리 매치 boost. 기본 0.20. */
openFileParentDirMatch: number;
/** 확장자 의미 매치 boost (e.g. .ts → typescript/tsx 단어). 기본 0.10. */
openFileExtMatch: number;
/** 최종 결합 가중치 — finalScore = base × (1 + actionability × this). 기본 0.30. */
combinedWeight: number;
}
export const DEFAULT_ACTIONABILITY_WEIGHTS: ActionabilityWeights = {
slashCommandMatch: 0.30,
openFileNameMatch: 0.40,
openFileParentDirMatch: 0.20,
openFileExtMatch: 0.10,
combinedWeight: 0.30,
};
/** VS Code 활성 에디터·최근 슬래시 명령에서 work-state 신호 캡처. */
export function captureWorkStateSignals(recentSlashCommands: string[]): WorkStateSignals {
const editor = vscode.window.activeTextEditor;
return {
recentSlashCommands: recentSlashCommands.slice(0, 5), // 최신 5개로 cap
openFilePath: editor?.document.uri.fsPath,
};
}
const EXTENSION_KEYWORDS: Record<string, RegExp> = {
'.ts': /\b(typescript|tsx?|ts)\b/i,
'.tsx': /\b(typescript|tsx|react)\b/i,
'.js': /\b(javascript|jsx?)\b/i,
'.jsx': /\b(javascript|jsx|react)\b/i,
'.py': /\b(python|py)\b/i,
'.md': /\b(markdown|md|문서)\b/i,
'.json': /\b(json|config)\b/i,
'.go': /\b(golang|go)\b/i,
'.rs': /\b(rust|rs)\b/i,
};
/**
* 한 chunk 의 actionability 점수 계산 — 0.0 ~ 1.0 (cap).
* 매치 boost 들의 단순 합산 후 1.0 cap.
*/
export function computeActionabilityScore(
chunk: RetrievalChunk,
signals: WorkStateSignals,
weights: ActionabilityWeights = DEFAULT_ACTIONABILITY_WEIGHTS,
): number {
if (!chunk) return 0;
const haystack = ((chunk.title || '') + ' ' + (chunk.content || '')).toLowerCase();
if (!haystack.trim()) return 0;
let score = 0;
// Signal 1: 최근 슬래시 명령 — '/runway' → 'runway' 키워드 매치
for (const cmd of signals.recentSlashCommands) {
const kw = cmd.replace(/^\//, '').toLowerCase().trim();
if (kw.length < 3) continue; // 너무 짧으면 노이즈 (/q 등)
// 단어 경계 매치 — substring 이 아닌 단어 단위 (영문 한정, 한글은 substring)
const isAscii = /^[a-z0-9-]+$/.test(kw);
const wordRe = isAscii ? new RegExp(`\\b${escapeRegex(kw)}\\b`, 'i') : null;
if (wordRe ? wordRe.test(haystack) : haystack.includes(kw)) {
score += weights.slashCommandMatch;
}
}
// Signal 2: 열린 파일 — 파일명·부모 디렉터리·확장자
if (signals.openFilePath) {
const fp = signals.openFilePath;
const ext = path.extname(fp).toLowerCase();
const base = path.basename(fp, ext).toLowerCase();
const parent = path.basename(path.dirname(fp)).toLowerCase();
// 파일 자체가 chunk 의 filePath 와 같으면 강한 boost (max)
const chunkFile = chunk.metadata?.filePath?.toLowerCase();
if (chunkFile && chunkFile === fp.toLowerCase()) {
score += weights.openFileNameMatch * 1.5; // exact file = 보너스
} else if (base.length >= 3) {
const baseRe = new RegExp(`\\b${escapeRegex(base)}\\b`, 'i');
if (baseRe.test(haystack)) score += weights.openFileNameMatch;
}
if (parent.length >= 3 && parent !== 'src' && parent !== 'lib') {
// 'src' / 'lib' 같은 일반 디렉터리는 신호로서 약함 — 제외
const parentRe = new RegExp(`\\b${escapeRegex(parent)}\\b`, 'i');
if (parentRe.test(haystack)) score += weights.openFileParentDirMatch;
}
const extRe = EXTENSION_KEYWORDS[ext];
if (extRe && extRe.test(haystack)) score += weights.openFileExtMatch;
}
return Math.min(score, 1.0);
}
function escapeRegex(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Chunks 배열의 score 를 actionability 로 재가중. *원본 score 를 덮어씀* — 호출자는
* 반드시 retrieval pipeline 안에서 normalizeScores 직후, selectWithinBudget 직전에만 호출.
*
* 각 chunk 에 actionabilityScore 를 metadata 에 기록 (디버깅·UI 표시 용도).
*/
export function applyActionabilityBoost(
chunks: RetrievalChunk[],
signals: WorkStateSignals,
weights: ActionabilityWeights = DEFAULT_ACTIONABILITY_WEIGHTS,
): void {
if (!signals || (signals.recentSlashCommands.length === 0 && !signals.openFilePath)) return;
for (const c of chunks) {
const a = computeActionabilityScore(c, signals, weights);
if (a > 0) {
(c.metadata as any).actionabilityScore = a;
c.score = c.score * (1 + a * weights.combinedWeight);
}
}
}