Files
connectai/src/retrieval/actionabilityScoring.ts
T
koriweb 990ea0ae5f 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>
2026-05-29 16:05:30 +09:00

157 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}
}
}