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:
@@ -3,15 +3,15 @@
|
||||
<!-- ASTRA:AUTO-START -->
|
||||
|
||||
## Snapshot
|
||||
- **Workspace**: `connectai` `v2.2.161` _(absolute path varies by environment; resolved from the active VS Code workspace)_
|
||||
- **Workspace**: `connectai` `v2.2.193` _(absolute path varies by environment; resolved from the active VS Code workspace)_
|
||||
- **Description**: The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.
|
||||
- **Stack**: TypeScript, Node.js, VS Code Extension, LM Studio SDK, Test runner
|
||||
- **Stats**: 396 source files, ~63,667 lines across 5 top-level modules.
|
||||
- **Stats**: 416 source files, ~70,115 lines across 5 top-level modules.
|
||||
|
||||
## Last Refresh
|
||||
- **Time**: 2026-05-26T08:10:10.182Z
|
||||
- **Time**: 2026-05-29T06:58:50.818Z
|
||||
- **Files newly analysed**: 3
|
||||
- **Files reused from cache**: 393
|
||||
- **Files reused from cache**: 413
|
||||
|
||||
## Directory Map
|
||||
```mermaid
|
||||
@@ -22,8 +22,8 @@ mindmap
|
||||
sidebar/
|
||||
lib/
|
||||
agent/
|
||||
retrieval/
|
||||
core/
|
||||
extension/
|
||||
media/
|
||||
tests/
|
||||
helpers/
|
||||
@@ -40,11 +40,11 @@ mindmap
|
||||
> Arrows: which top-level module imports from which.
|
||||
```mermaid
|
||||
flowchart LR
|
||||
src["src/<br/>247 files"]
|
||||
src["src/<br/>262 files"]
|
||||
media["media/<br/>6 files"]
|
||||
tests["tests/<br/>37 files"]
|
||||
core_py["core_py/<br/>6 files"]
|
||||
docs["docs/<br/>100 files"]
|
||||
docs["docs/<br/>105 files"]
|
||||
tests --> src
|
||||
```
|
||||
|
||||
@@ -58,8 +58,8 @@ flowchart LR
|
||||
> Imported by many other files — touching these has wide blast radius.
|
||||
- `src/utils.ts` — referenced by **87** files
|
||||
- `src/agent.ts` — referenced by **34** files
|
||||
- `src/config.ts` — referenced by **32** files
|
||||
- `src/core/services.ts` — referenced by **14** files
|
||||
- `src/config.ts` — referenced by **33** files
|
||||
- `src/core/services.ts` — referenced by **15** files
|
||||
- `src/features/company/index.ts` — referenced by **14** files · Public API for 1인 기업 모드. Consumers (sidebarProvider, chatHandlers, command handlers) import from this barrel so internal layout can move around without touching every call site.
|
||||
- `src/features/company/types.ts` — referenced by **14** files · Type definitions for the 1인 기업 (One-Person Company) mode. The mode turns the user into a virtual CEO that dispatches work to a roster of specialist agents. Each turn produces a session directory conta
|
||||
- `src/sidebarProvider.ts` — referenced by **11** files
|
||||
@@ -67,17 +67,17 @@ flowchart LR
|
||||
|
||||
## Modules
|
||||
|
||||
### `src/` — 247 files, ~46,059 lines
|
||||
### `src/` — 262 files, ~52,385 lines
|
||||
|
||||
**Sub-directories**
|
||||
- `src/features/` (87) — Astra Office — public API. 다음 세션에서 추가될 OfficeSnapshot presenter / schema 도 같은 entry 로 노출 예정. 현재 노출: full webview panel H
|
||||
- `src/features/` (92) — Astra Office — public API. 다음 세션에서 추가될 OfficeSnapshot presenter / schema 도 같은 entry 로 노출 예정. 현재 노출: full webview panel H
|
||||
- `src/sidebar/` (35) — Brain profile lifecycle 의 pure helpers — sidebarProvider 의 add/edit/delete 흐름에서 modal UI 와 config 쓰기를 제외한 데이터 변환 만 격리. 현
|
||||
- `src/lib/` (28) — Astra Mode Architecture Context Builder. 의도: 사용자가 Astra 자체의 mode 디자인 (Guard vs Multi-Agent 가 별도 모드여야 하는지) 을 묻는 메타 질문에 답할
|
||||
- `src/agent/` (25) — 25 files (.ts)
|
||||
- `src/agent/` (26) — Post-hoc Self-Check — 답변 완료 후 LLM 한 번 호출로 3가지 평가. 사용자 제안: "[Self-Check] 단계 — 이 답변이 사용자 질문에 직접 답하는가 / 규칙 준수 / 논리 모순 없는가".
|
||||
- `src/retrieval/` (16) — Actionability Scoring — 검색 결과를 "현재 작업 상태" 신호로 재가중. 기존 TF-IDF (단어 매칭) + recency (시간) 만으로는 "지금 이 사용자가 하고 있는 작업과 직접 연결 된 문서
|
||||
- `src/core/` (15) — Astra Path Resolver (경로 해결기) Astra의 모든 데이터 파일(.astra 디렉토리)의 경로를 중앙에서 관리합니다. 확장 프로그램의 설치 경로(extensionUri) 기반으로 .astra 디렉토
|
||||
- `src/memory/` (9) — Distillation Loop — stale Episodic Memory → Long-Term "episode-digest" 승급. 배경: Episodic Memory 가 무한히 누적되면 검색 노이즈. 30일+ 지
|
||||
- `src/extension/` (8) — 8 files (.ts)
|
||||
- `src/memory/` (8) — Episodic Memory (일화 기억) 과거 대화/회의/결정의 맥락 흐름을 저장합니다. 세션 종료 시 자동으로 에피소드를 요약하여 저장합니다. "왜 이렇게 결정했는지", "어떤 흐름으로 진행했는지" 기록. 저장
|
||||
- `src/retrieval/` (8) — Brain Index — persistent, mtime-keyed tokenized cache of the Second Brain RAG 검색은 매 질의마다 브레인의 모든 .md 파일을 읽고 토크나이즈해서 TF-I
|
||||
- `src/docs/` (6) — Bug: Edited agent.ts Edited agent.ts Edited agent.ts Edited agent.ts Edited agent.ts ...
|
||||
- `src/integrations/` (6) — Per-chat conversation history for the Telegram bot. Why this exists: the previous bot was stateless — every inbound mess
|
||||
- `src/lmstudio/` (4) — 4 files (.ts)
|
||||
@@ -85,22 +85,23 @@ flowchart LR
|
||||
|
||||
**Key files**
|
||||
- `src/utils.ts` (471 lines)
|
||||
- `src/agent.ts` (1484 lines)
|
||||
- `src/config.ts` (418 lines)
|
||||
- `src/config.ts` (550 lines)
|
||||
- `src/agent.ts` (1568 lines)
|
||||
- `src/features/company/types.ts` (446 lines) — Type definitions for the 1인 기업 (One-Person Company) mode. The mode turns the user into a virtual CEO that dispatches work to a roster of specialist agents. Each turn produces a session directory conta
|
||||
- `src/sidebarProvider.ts` (3186 lines)
|
||||
- `src/core/services.ts` (176 lines)
|
||||
- `src/sidebarProvider.ts` (3186 lines)
|
||||
- `src/lib/contextManager.ts` (278 lines) — Context Manager (컨텍스트 한계 관리) "context length = 132k" 는 "답변을 132k 토큰까지 생성해도 된다" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변 + 여유분 ≤ context length 이 모듈은 요청을 보내기 전에 입력 토큰을 추정하고, - 동적으로 출력 상한(maxTokens)을 계
|
||||
- `src/features/company/companyConfig.ts` (896 lines) — State + config plumbing for 1인 기업 모드. Two surfaces: - CompanyState (runtime data: enabled flag, company name, which agents are active, per-agent model overrides). Persisted in VS Code's globalState so
|
||||
- `src/integrations/telegram/telegramClient.ts` (154 lines)
|
||||
- `src/lib/paths.ts` (151 lines)
|
||||
- `src/agent/actions/types.ts` (41 lines)
|
||||
- `src/skills/agentKnowledgeMap.ts` (374 lines)
|
||||
- `src/retrieval/types.ts` (66 lines) — Retrieval Types (검색 결과 통합 타입) 모든 검색 소스(Brain, Memory, Project, Episode)의 결과를 통합 인터페이스로 정의합니다.
|
||||
- `src/memory/types.ts` (151 lines) — Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic
|
||||
- `src/retrieval/scoring.ts` (541 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.
|
||||
- `src/features/stocks/types.ts` (53 lines) — Stocks 모듈 공유 타입. investresults/targetstocks.json 스키마를 그대로 받아서, ConnectAI 의 <workspace>/.astra/stocks.json 으로 옮긴 뒤 같은 필드명을 유지. 한글 필드명은 사용자의 도메인 데이터라 변경하지 않는다 — 마이그레이션 충돌 회피 + 사용자가 직접 JSON 편집할 때 frictio
|
||||
- `src/lib/contextBuilders/promptDetection.ts` (85 lines) — 사용자 prompt 의 의도 분류 류 detection helpers. 모두 stateless 정규식 매칭. 옛 코드는 agent.ts 의 private 메서드로 박혀 있었는데, system prompt 빌더 (buildJarvisProjectBriefContext 등) 가 이걸 의존하면서 god-file 안에서 서로 얽힘. 헬퍼만 먼저 떼면 의존 그래프가
|
||||
- `src/retrieval/lessonHelpers.ts` (325 lines) — Lesson / Experience Memory — pure helpers (no vscode dependency) "Lesson" = a markdown file in the active brain that captures a past mistake/risk and how to avoid repeating it. Identified by a lessons
|
||||
- `src/memory/types.ts` (126 lines) — Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic
|
||||
- `src/retrieval/scoring.ts` (541 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.
|
||||
- `src/security.ts` (159 lines)
|
||||
- `src/features/secondBrainTrace.ts` (792 lines)
|
||||
- `src/features/providers/types.ts` (63 lines) — Cloud LLM provider routing — model id prefix → provider id 매핑. Prefix 규칙: openrouter:anthropic/claude-3.5-sonnet → { provider: 'openrouter', model: 'anthropic/claude-3.5-sonnet' } anthropic:claude-3-5
|
||||
@@ -108,7 +109,6 @@ flowchart LR
|
||||
- `src/lib/contextBuilders/localProjectIntent.ts` (233 lines)
|
||||
- `src/lib/engine.ts` (1114 lines)
|
||||
- `src/lmstudio/streamer.ts` (252 lines)
|
||||
- `src/core/responseRecovery.ts` (310 lines) — Response Recovery — Thought Quarantine + Final-only Retry + Auto-Continuation The user already asked their question; they're waiting for an answer, not for a chance to babysit the generation engine. S
|
||||
|
||||
### `media/` — 6 files, ~7,671 lines
|
||||
|
||||
@@ -165,17 +165,17 @@ flowchart LR
|
||||
- `core_py/optimizer.py` (55 lines)
|
||||
- `core_py/queue_worker.py` (82 lines)
|
||||
|
||||
### `docs/` — 100 files, ~3,653 lines
|
||||
### `docs/` — 105 files, ~3,775 lines
|
||||
|
||||
**Sub-directories**
|
||||
- `docs/records/` (87) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고...
|
||||
- `docs/records/` (92) — Bug: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고...
|
||||
- `docs/docs/` (5) — Bug: Viewed integrationretrieval.test.ts:1-59 integrationretrieval.test.ts를 통해 ...
|
||||
- `docs/Meeting/` (0)
|
||||
|
||||
**Key files**
|
||||
- `docs/TELEGRAM_REMOTE_EXECUTION_PLAN.md` (452 lines) — Telegram Remote Execution 기획서
|
||||
- `docs/AgentEngine_Architecture.md` (314 lines) — AgentEngine Architecture Document
|
||||
- `docs/records/ConnectAI/timeline.md` (212 lines) — Project Timeline
|
||||
- `docs/records/ConnectAI/timeline.md` (227 lines) — Project Timeline
|
||||
- `docs/ASTRA_OFFICE_REFACTOR.md` (198 lines) — Astra Office Refactor — Design Doc
|
||||
- `docs/EXPERIENCE_MEMORY_PLAN.md` (122 lines) — Experience Memory (Mistake / Lesson Loop) — Implementation Plan
|
||||
- `docs/records/ConnectAI/development/2026-05-02_connectai_project_knowledge_overview.md` (121 lines) — Astra Project Knowledge Overview
|
||||
@@ -232,7 +232,7 @@ flowchart LR
|
||||
- `g1nation.calendar.refresh` — Astra: Google Calendar 새로고침 📅
|
||||
- `g1nation.calendar.connectOAuth` — Astra: Google Calendar OAuth 연결 (쓰기) 🔐
|
||||
- `g1nation.devilAgent.toggle` — Astra: Toggle Devil Agent 🎭
|
||||
- **Configuration** (93 settings):
|
||||
- **Configuration** (121 settings):
|
||||
- `g1nation.multiAgentEnabled` *(boolean)* _(default: `false`)_ — Enable Multi-Agent Workflow (Planner -> Researcher -> Writer) for complex tasks.
|
||||
- `g1nation.datacollectBridgeUrl` *(string)* _(default: `"http://127.0.0.1:3002"`)_ — Wiki/Datacollect MCP Bridge URL. /research, /benchmark, /youtube chat slash commands route here. The Bridge must be running (`npm run bridge` in the Datacollect project).
|
||||
- `g1nation.datacollectSavePath` *(string)* _(default: `""`)_
|
||||
@@ -240,6 +240,9 @@ flowchart LR
|
||||
- `g1nation.datacollectMaxPages` *(number)* _(default: `8`)_
|
||||
- `g1nation.datacollectSynthesisTemperature` *(number)* _(default: `0.1`)_
|
||||
- `g1nation.chatTemperature` *(number)* _(default: `0.3`)_
|
||||
- `g1nation.meetUsesTasks` *(boolean)* _(default: `true`)_
|
||||
- `g1nation.meetUsesCalendar` *(boolean)* _(default: `false`)_
|
||||
- `g1nation.teamVoiceGuide` *(string)* _(default: `""`)_
|
||||
- `g1nation.memoryEnabled` *(boolean)* _(default: `true`)_ — Enable layered memory injection before each model response.
|
||||
- `g1nation.memoryShortTermMessages` *(number)* _(default: `8`)_ — Number of recent conversation messages included as short-term memory.
|
||||
- `g1nation.memoryMediumTermSessions` *(number)* _(default: `5`)_ — Number of recent saved chat sessions included as medium-term memory.
|
||||
@@ -288,12 +291,9 @@ flowchart LR
|
||||
- `g1nation.agentSkillsPath` *(string)* _(default: `""`)_ — Absolute path to the agent skills folder (`.agent/skills/*.md`). When empty, defaults to '<workspace>/.agent/skills'. Use this on Windows or when your skills live outside the workspace.
|
||||
- `g1nation.embeddingModel` *(string)* _(default: `""`)_ — Embedding model registered in LM Studio / Ollama (e.g. 'text-embedding-bge-small-en-v1.5', 'nomic-embed-text', 'multilingual-e5-small'). When empty, Astra uses TF-IDF only. When set, the brain is embe
|
||||
- `g1nation.embeddingBlendAlpha` *(number)* _(default: `0.5`)_ — Hybrid score blend: 0 = pure TF-IDF (sparse / keyword), 1 = pure embedding cosine (dense / semantic), 0.5 = balanced. Only used when g1nation.embeddingModel is set. Default 0.5.
|
||||
- `g1nation.knowledgeMix.secondBrainWeight` *(number)* _(default: `50`)_ — Knowledge Mix (0–100): how heavily the assistant should lean on Second Brain evidence vs. its own general knowledge. 0 = Second Brain disabled (model knowledge only). 50 = balanced (legacy default). 1
|
||||
- `g1nation.workflow.multiAgentMode` *(string)* _(default: `"auto"`)_
|
||||
- `g1nation.workflow.autoCtxFractionThreshold` *(number)* _(default: `0.3`)_
|
||||
- `g1nation.chunkedSwitchTokens` *(number)* _(default: `50000`)_
|
||||
- `g1nation.chunkedMaxSections` *(number)* _(default: `3`)_
|
||||
- _…and 33 more_
|
||||
- `g1nation.conflictHighlightingEnabled` *(boolean)* _(default: `true`)_ — Conflict Surface — 검색된 출처에서 충돌/논란 신호 감지 시 [CONFLICT WARNINGS] 블록을 시스템 프롬프트에 주입. LLM 이 상충되는 관점을 명시하고 사용자 판단에 위임하도록. 기본 켜짐.
|
||||
- `g1nation.conflictSeverityThreshold` *(string)* _(default: `"medium"`)_ — Conflict 자기-신호 surface 시 최소 severity 임계. low=가장 민감(노이즈 가능), medium=균형(기본), high=강한 충돌만.
|
||||
- _…and 61 more_
|
||||
|
||||
## Dependencies
|
||||
- **Runtime** (2): `@lmstudio/sdk`, `pdf-parse`
|
||||
@@ -341,7 +341,7 @@ Astra는 대표님의 명시적인 승인 하에 로컬 시스템의 강력한
|
||||
**Designed for High-Performance Decision Making.**
|
||||
Copyright (C) **g1nation**. All rights reserved.
|
||||
|
||||
_Last auto-scan: 2026-05-26T08:10:10.182Z · signature `850d1550`_
|
||||
_Last auto-scan: 2026-05-29T06:58:50.818Z · signature `e7ea4eff`_
|
||||
<!-- ASTRA:AUTO-END -->
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"generatedAt": "2026-05-26T08:10:10.234Z",
|
||||
"generatedAt": "2026-05-29T06:58:50.872Z",
|
||||
"files": {
|
||||
"src/agent/actions/brainOps.ts": {
|
||||
"mtimeMs": 1779764602582.9768,
|
||||
@@ -135,9 +135,9 @@
|
||||
]
|
||||
},
|
||||
"src/agent/handlePrompt/buildAstraModeSystemPrompt.ts": {
|
||||
"mtimeMs": 1779764602589.828,
|
||||
"size": 5335,
|
||||
"lines": 82,
|
||||
"mtimeMs": 1780033670993.9626,
|
||||
"size": 8427,
|
||||
"lines": 133,
|
||||
"role": "",
|
||||
"imports": [
|
||||
"src/lib/contextBuilders/localProjectIntent",
|
||||
@@ -298,6 +298,13 @@
|
||||
"src/agent"
|
||||
]
|
||||
},
|
||||
"src/agent/postHocSelfCheck.ts": {
|
||||
"mtimeMs": 1780033021215.5054,
|
||||
"size": 8943,
|
||||
"lines": 230,
|
||||
"role": "Post-hoc Self-Check — 답변 완료 후 LLM 한 번 호출로 3가지 평가. 사용자 제안: \"[Self-Check] 단계 — 이 답변이 사용자 질문에 직접 답하는가 / 규칙 준수 / 논리 모순 없는가\". 기존 CoVe (v2.2.184) 와 차이: - CoVe = 답변 작성 전 모델에게 self-verify 지시 (instructional, 1",
|
||||
"imports": []
|
||||
},
|
||||
"src/agent/sessions/compressSummary.ts": {
|
||||
"mtimeMs": 1779764602596.8809,
|
||||
"size": 2780,
|
||||
@@ -310,9 +317,9 @@
|
||||
]
|
||||
},
|
||||
"src/agent.ts": {
|
||||
"mtimeMs": 1779764602581.5493,
|
||||
"size": 81527,
|
||||
"lines": 1484,
|
||||
"mtimeMs": 1780033640914.1235,
|
||||
"size": 85839,
|
||||
"lines": 1568,
|
||||
"role": "",
|
||||
"imports": [
|
||||
"src/utils",
|
||||
@@ -383,6 +390,7 @@
|
||||
"src/agent/handlePrompt/buildAstraModeSystemPrompt",
|
||||
"src/agent/handlePrompt/computeBudgetedRequest",
|
||||
"src/agent/handlePrompt/processFinalAnswer",
|
||||
"src/agent/postHocSelfCheck",
|
||||
"src/agent/handlePrompt/applyAutoContinuation",
|
||||
"src/features/approval/approvalQueue",
|
||||
"src/features/providers",
|
||||
@@ -425,9 +433,9 @@
|
||||
]
|
||||
},
|
||||
"src/config.ts": {
|
||||
"mtimeMs": 1779764602599.9685,
|
||||
"size": 22439,
|
||||
"lines": 418,
|
||||
"mtimeMs": 1780033618147.1584,
|
||||
"size": 30668,
|
||||
"lines": 550,
|
||||
"role": "",
|
||||
"imports": []
|
||||
},
|
||||
@@ -850,24 +858,34 @@
|
||||
"imports": []
|
||||
},
|
||||
"src/features/calendar/index.ts": {
|
||||
"mtimeMs": 1779065453871.9966,
|
||||
"size": 593,
|
||||
"lines": 32,
|
||||
"mtimeMs": 1779879082214.362,
|
||||
"size": 711,
|
||||
"lines": 40,
|
||||
"role": "",
|
||||
"imports": [
|
||||
"src/features/calendar/icsParser",
|
||||
"src/features/calendar/calendarCache",
|
||||
"src/features/calendar/oauth",
|
||||
"src/features/calendar/calendarApi"
|
||||
"src/features/calendar/calendarApi",
|
||||
"src/features/calendar/tasksApi"
|
||||
]
|
||||
},
|
||||
"src/features/calendar/oauth.ts": {
|
||||
"mtimeMs": 1779065453873.0256,
|
||||
"size": 10940,
|
||||
"lines": 235,
|
||||
"mtimeMs": 1779869692244.0415,
|
||||
"size": 10986,
|
||||
"lines": 236,
|
||||
"role": "Google OAuth 2.0 — loopback (Desktop app) 흐름. Google 은 Desktop 앱 OAuth client 에 대해 http://127.0.0.1:<ephemeralport> redirect URI 를 허용한다. 본 모듈은: 1. ephemeral port 에 일회용 HTTP 서버 띄움 2. 사용자 브라우저로 Google 로",
|
||||
"imports": []
|
||||
},
|
||||
"src/features/calendar/tasksApi.ts": {
|
||||
"mtimeMs": 1779879080585.057,
|
||||
"size": 7206,
|
||||
"lines": 169,
|
||||
"role": "Google Tasks API v1 — task create 호출. Calendar / Sheets 와 같은 OAuth 토큰을 공유한다 (scope 에 tasks 포함). Tasks 는 date-only 모델(시간 없음)이라 /meet 의 액션 아이템처럼 \"시간 없이 마감일만 있는 할 일\" 에 자연스럽게 맞는다. 외부 라이브러리 안 씀 — Tasks API",
|
||||
"imports": [
|
||||
"src/features/calendar/calendarApi"
|
||||
]
|
||||
},
|
||||
"src/features/company/agents.ts": {
|
||||
"mtimeMs": 1779764602611.032,
|
||||
"size": 38122,
|
||||
@@ -1090,6 +1108,13 @@
|
||||
"role": "Type definitions for the 1인 기업 (One-Person Company) mode. The mode turns the user into a virtual CEO that dispatches work to a roster of specialist agents. Each turn produces a session directory conta",
|
||||
"imports": []
|
||||
},
|
||||
"src/features/customers/customersStore.ts": {
|
||||
"mtimeMs": 1779952731356.5476,
|
||||
"size": 6815,
|
||||
"lines": 176,
|
||||
"role": "고객사 / MRR / 갱신 트래커. 4인 기업의 수입 쪽 — /runway 가 통장과 burn 을 본다면, 여기는 어디서 돈이 들어오나. Salesforce / HubSpot 같은 CRM 아닌 가벼운 event-sourced 로그. 저장 형식: JSON Lines (.jsonl) — append-only event log. 같은 customer 의 여러 이",
|
||||
"imports": []
|
||||
},
|
||||
"src/features/datacollect/bridgeClient.ts": {
|
||||
"mtimeMs": 1779764602617.1548,
|
||||
"size": 7210,
|
||||
@@ -1119,9 +1144,9 @@
|
||||
"imports": []
|
||||
},
|
||||
"src/features/datacollect/prompts/youtubePrompts.ts": {
|
||||
"mtimeMs": 1779764602619.1604,
|
||||
"size": 18108,
|
||||
"lines": 331,
|
||||
"mtimeMs": 1779852277877.3347,
|
||||
"size": 19109,
|
||||
"lines": 347,
|
||||
"role": "/youtube slash command 의 LLM 입력 빌더 + 자막 변환 헬퍼. - formatHms / fullScriptFromSegments / bucketSegments — segment list 가공 - YoutubeAnalysisMode — info/benchmark/both 라우팅 enum (slashRouter 가 사용) - buildIn",
|
||||
"imports": []
|
||||
},
|
||||
@@ -1133,21 +1158,24 @@
|
||||
"imports": []
|
||||
},
|
||||
"src/features/datacollect/slashRouter.ts": {
|
||||
"mtimeMs": 1779764602621.1665,
|
||||
"size": 63071,
|
||||
"lines": 1132,
|
||||
"mtimeMs": 1780035781878.5417,
|
||||
"size": 226019,
|
||||
"lines": 4090,
|
||||
"role": "",
|
||||
"imports": [
|
||||
"src/utils",
|
||||
"src/features/datacollect/bridgeClient",
|
||||
"src/features/calendar",
|
||||
"src/sidebar/managers/chronicleProjectStore",
|
||||
"src/memory",
|
||||
"src/config",
|
||||
"src/retrieval/terminologyBlock",
|
||||
"src/features/setup/datacollectSetup",
|
||||
"src/features/datacollect/prompts/synthesisPrompt",
|
||||
"src/features/datacollect/prompts/youtubePrompts",
|
||||
"src/features/datacollect/prompts/wikifyPrompt",
|
||||
"src/features/datacollect/prompts/meetPrompt",
|
||||
"src/features/datacollect/scheduling/calendarHelpers",
|
||||
"src/features/stocks"
|
||||
"src/features/datacollect/scheduling/calendarHelpers"
|
||||
]
|
||||
},
|
||||
"src/features/devilAgent/devilPrompt.ts": {
|
||||
@@ -1176,6 +1204,20 @@
|
||||
"src/features/devilAgent/devilService"
|
||||
]
|
||||
},
|
||||
"src/features/feedback/feedbackStore.ts": {
|
||||
"mtimeMs": 1779931731117.0737,
|
||||
"size": 3406,
|
||||
"lines": 81,
|
||||
"role": "고객 피드백 누적 저장소. 단일 운영자(대표) 모드에서 슬랙·이메일·CS 채널에 흩어진 고객 피드백을 /feedback <텍스트> 한 줄로 모아 둔다. 패턴 분석은 /feedback summary 로 LLM 이 누적 데이터를 보고 카테고리 분포 + 반복 주제를 추출. 저장 형식: JSON Lines (.jsonl) — 한 줄 = 한 entry. 누적·app",
|
||||
"imports": []
|
||||
},
|
||||
"src/features/hire/hireStore.ts": {
|
||||
"mtimeMs": 1779964473915.006,
|
||||
"size": 5586,
|
||||
"lines": 150,
|
||||
"role": "채용 파이프라인 트래커. 4인 → 5인 이상 확장 시점에 후보자가 여러 명, 여러 역할(개발/기획/디자인) 로 들어오기 시작 — 노션·스프레드시트·이메일에 흩어진 정보를 한 명령으로 본다. Event-sourced (customersStore 와 동일 패턴) — append-only 이벤트 로그를 재생해 후보자별 현재 단계 + 노트 누적 도출. 위치: <w",
|
||||
"imports": []
|
||||
},
|
||||
"src/features/projectArchitecture/index.ts": {
|
||||
"mtimeMs": 1778720117531.2734,
|
||||
"size": 25946,
|
||||
@@ -1326,6 +1368,13 @@
|
||||
"role": "Cloud LLM provider routing — model id prefix → provider id 매핑. Prefix 규칙: openrouter:anthropic/claude-3.5-sonnet → { provider: 'openrouter', model: 'anthropic/claude-3.5-sonnet' } anthropic:claude-3-5",
|
||||
"imports": []
|
||||
},
|
||||
"src/features/runway/runwayStore.ts": {
|
||||
"mtimeMs": 1779935315716.9917,
|
||||
"size": 6666,
|
||||
"lines": 173,
|
||||
"role": "Runway / Cash 누적 저장소. 4인 기업 운영의 가장 중요한 숫자 — 현금 잔고 / 월 소진율 / 남은 개월수 — 를 한 명령 (/runway) 로 본다. 회계 시스템은 아니고, 대표가 머리에 가지고 있는 \"지금 통장에 얼마, 한 달에 얼마 나감\" 을 코드 옆에서 잡는 가벼운 트래커. 저장 형식: JSON Lines (.jsonl) — 한 줄 = ",
|
||||
"imports": []
|
||||
},
|
||||
"src/features/secondBrainTrace.ts": {
|
||||
"mtimeMs": 1779411561816.5603,
|
||||
"size": 39429,
|
||||
@@ -1487,14 +1536,16 @@
|
||||
]
|
||||
},
|
||||
"src/features/stocks/slashStocks.ts": {
|
||||
"mtimeMs": 1779764602629.3262,
|
||||
"size": 13533,
|
||||
"lines": 279,
|
||||
"mtimeMs": 1779857133519.0603,
|
||||
"size": 33999,
|
||||
"lines": 621,
|
||||
"role": "",
|
||||
"imports": [
|
||||
"src/utils",
|
||||
"src/features/stocks/stocksStore",
|
||||
"src/features/stocks/yahooClient",
|
||||
"src/features/stocks/naverFundamentals",
|
||||
"src/core/services",
|
||||
"src/features/stocks/signalClassifier",
|
||||
"src/features/stocks/sheetsSync",
|
||||
"src/features/stocks/llmJudge",
|
||||
@@ -1563,9 +1614,9 @@
|
||||
"imports": []
|
||||
},
|
||||
"src/features/stocks/yahooClient.ts": {
|
||||
"mtimeMs": 1779769686275.8792,
|
||||
"size": 11167,
|
||||
"lines": 243,
|
||||
"mtimeMs": 1779856972057.0444,
|
||||
"size": 16112,
|
||||
"lines": 353,
|
||||
"role": "",
|
||||
"imports": [
|
||||
"src/utils"
|
||||
@@ -1737,9 +1788,9 @@
|
||||
]
|
||||
},
|
||||
"src/lib/contextBuilders/memoryContext.ts": {
|
||||
"mtimeMs": 1779764602638.9414,
|
||||
"size": 11267,
|
||||
"lines": 233,
|
||||
"mtimeMs": 1780033602782.2012,
|
||||
"size": 18059,
|
||||
"lines": 352,
|
||||
"role": "",
|
||||
"imports": [
|
||||
"src/agent",
|
||||
@@ -1750,7 +1801,15 @@
|
||||
"src/retrieval/embeddings",
|
||||
"src/retrieval/brainIndex",
|
||||
"src/skills/agentKnowledgeMap",
|
||||
"src/retrieval/knowledgeMix"
|
||||
"src/retrieval/knowledgeMix",
|
||||
"src/retrieval/conflictBlock",
|
||||
"src/retrieval/coveBlock",
|
||||
"src/retrieval/actionabilityScoring",
|
||||
"src/features/datacollect/slashRouter",
|
||||
"src/retrieval/semanticRerank",
|
||||
"src/retrieval/intentClarification",
|
||||
"src/retrieval/citationTrace",
|
||||
"src/retrieval/terminologyBlock"
|
||||
]
|
||||
},
|
||||
"src/lib/contextBuilders/modelCandidates.ts": {
|
||||
@@ -1951,19 +2010,30 @@
|
||||
"src/utils"
|
||||
]
|
||||
},
|
||||
"src/memory/distillation.ts": {
|
||||
"mtimeMs": 1780029994371.4304,
|
||||
"size": 7826,
|
||||
"lines": 200,
|
||||
"role": "Distillation Loop — stale Episodic Memory → Long-Term \"episode-digest\" 승급. 배경: Episodic Memory 가 무한히 누적되면 검색 노이즈. 30일+ 지난 에피소드는 \"지금 이 순간 관련 가능성\" 보다 \"역사적 패턴\" 가치가 커서, 디테일을 압축해 Long-Term 으로 옮기고 원본은 archi",
|
||||
"imports": [
|
||||
"src/memory/EpisodicMemory",
|
||||
"src/memory/LongTermMemory",
|
||||
"src/memory/types"
|
||||
]
|
||||
},
|
||||
"src/memory/EpisodicMemory.ts": {
|
||||
"mtimeMs": 1779411639504.3022,
|
||||
"size": 10940,
|
||||
"lines": 297,
|
||||
"mtimeMs": 1780029951842.101,
|
||||
"size": 13160,
|
||||
"lines": 341,
|
||||
"role": "Episodic Memory (일화 기억) 과거 대화/회의/결정의 맥락 흐름을 저장합니다. 세션 종료 시 자동으로 에피소드를 요약하여 저장합니다. \"왜 이렇게 결정했는지\", \"어떤 흐름으로 진행했는지\" 기록. 저장 위치: {brainPath}/memory/episodes/.json",
|
||||
"imports": [
|
||||
"src/memory/types"
|
||||
]
|
||||
},
|
||||
"src/memory/index.ts": {
|
||||
"mtimeMs": 1779411771475.9016,
|
||||
"size": 6530,
|
||||
"lines": 188,
|
||||
"mtimeMs": 1780030158922.7122,
|
||||
"size": 8053,
|
||||
"lines": 225,
|
||||
"role": "MemoryManager — 5-Layer Cognitive Memory System (통합 진입점) Astra의 모든 메모리 레이어를 통합 관리하는 중앙 매니저입니다. ① Short-Term Memory — 현재 대화 흐름 (FIFO) ② Long-Term Memory — 사용자 취향/규칙/결정 ③ Project Memory — 프로젝트별 지식 ④ Pro",
|
||||
"imports": [
|
||||
"src/config",
|
||||
@@ -1973,13 +2043,14 @@
|
||||
"src/memory/ProceduralMemory",
|
||||
"src/memory/EpisodicMemory",
|
||||
"src/memory/MemoryExtractor",
|
||||
"src/memory/types"
|
||||
"src/memory/types",
|
||||
"src/memory/distillation"
|
||||
]
|
||||
},
|
||||
"src/memory/LongTermMemory.ts": {
|
||||
"mtimeMs": 1779411767886.338,
|
||||
"size": 9226,
|
||||
"lines": 252,
|
||||
"mtimeMs": 1780029930791.1965,
|
||||
"size": 10507,
|
||||
"lines": 281,
|
||||
"role": "Long-Term Memory (장기 기억) 사용자의 취향, 프로젝트 목표, 반복 규칙, 과거 결정 사항을 영구적으로 저장하고 관리합니다. 저장 위치: {brainPath}/memory/longterm.json",
|
||||
"imports": [
|
||||
"src/memory/types"
|
||||
@@ -2025,12 +2096,21 @@
|
||||
]
|
||||
},
|
||||
"src/memory/types.ts": {
|
||||
"mtimeMs": 1778028987419.3413,
|
||||
"size": 3575,
|
||||
"lines": 126,
|
||||
"mtimeMs": 1780029896609.2737,
|
||||
"size": 4799,
|
||||
"lines": 151,
|
||||
"role": "Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic",
|
||||
"imports": []
|
||||
},
|
||||
"src/retrieval/actionabilityScoring.ts": {
|
||||
"mtimeMs": 1780029379776.906,
|
||||
"size": 6602,
|
||||
"lines": 156,
|
||||
"role": "Actionability Scoring — 검색 결과를 \"현재 작업 상태\" 신호로 재가중. 기존 TF-IDF (단어 매칭) + recency (시간) 만으로는 \"지금 이 사용자가 하고 있는 작업과 직접 연결 된 문서\" 가 우선되지 않음. 예: 사용자가 /runway 명령을 막 실행했다면 runway / 재무 관련 문서가 같은 키워드 매치 점수여도 더 위로 ",
|
||||
"imports": [
|
||||
"src/retrieval/types"
|
||||
]
|
||||
},
|
||||
"src/retrieval/brainIndex.ts": {
|
||||
"mtimeMs": 1778667252738.4248,
|
||||
"size": 13270,
|
||||
@@ -2042,6 +2122,25 @@
|
||||
"src/utils"
|
||||
]
|
||||
},
|
||||
"src/retrieval/citationTrace.ts": {
|
||||
"mtimeMs": 1780032397612.986,
|
||||
"size": 2348,
|
||||
"lines": 49,
|
||||
"role": "Citation Trace — 답변 끝 에 \"출처:\" 한 줄 명시 지시. CoVe Strict 모드 (v2.2.184) 와 차이: - CoVe Strict: 모든 사실 주장 뒤에 inline [S1] 인용 강제 — verbose, 학술적 - Citation Trace: 답변 끝에 사용된 출처 한 줄 정리 — 가벼움, 항상 ON 권장 둘은 함께 동작 가능. ",
|
||||
"imports": [
|
||||
"src/retrieval/types"
|
||||
]
|
||||
},
|
||||
"src/retrieval/conflictBlock.ts": {
|
||||
"mtimeMs": 1780025261798.6677,
|
||||
"size": 8276,
|
||||
"lines": 204,
|
||||
"role": "Conflict Surface — [CONFLICT WARNINGS] 시스템 프롬프트 블록 생성. 기존 scoring.ts 가 문서당 conflictSeverity(NONE/LOW/MEDIUM/HIGH) 를 이미 계산하지만(반대/논란/vs 등 indicator 단어 카운트), LLM 은 그 사실을 모름. buildAstraModeSystemPrompt 의 ",
|
||||
"imports": [
|
||||
"src/retrieval/types",
|
||||
"src/retrieval/scoring"
|
||||
]
|
||||
},
|
||||
"src/retrieval/contextBudget.ts": {
|
||||
"mtimeMs": 1779764602653.6396,
|
||||
"size": 5276,
|
||||
@@ -2051,6 +2150,15 @@
|
||||
"src/retrieval/types"
|
||||
]
|
||||
},
|
||||
"src/retrieval/coveBlock.ts": {
|
||||
"mtimeMs": 1780028941111.8562,
|
||||
"size": 6062,
|
||||
"lines": 121,
|
||||
"role": "Chain-of-Verification (CoVe) — [VERIFICATION CHECKLIST] 시스템 프롬프트 블록 생성. 배경: 사용자 피드백 \"추론 결과가 나오기 직전, 이 결론이 확보된 지식(제2뇌)에만 근거하고 있는가? 를 스스로 질문하고 검증하는 로직\" — 할루시네이션 방지 + 그라운딩 명확화. 원논문 CoVe 는 2-pass (draft →",
|
||||
"imports": [
|
||||
"src/retrieval/types"
|
||||
]
|
||||
},
|
||||
"src/retrieval/embeddings.ts": {
|
||||
"mtimeMs": 1778667198243.6443,
|
||||
"size": 7294,
|
||||
@@ -2060,10 +2168,19 @@
|
||||
"src/utils"
|
||||
]
|
||||
},
|
||||
"src/retrieval/hierarchicalLevel.ts": {
|
||||
"mtimeMs": 1780030714979.2786,
|
||||
"size": 5908,
|
||||
"lines": 135,
|
||||
"role": "Hierarchical Context Window — 질의·문서의 추상도 레벨 매칭으로 검색 노이즈 감소. 사용자 제안: \"사용자가 '배포해줘' 라고 하면 L1(실행) 우선, '전략 검토' 라고 하면 L3(전략) 우선\". 같은 키워드 매치 점수여도 추상도가 안 맞으면 noise. v1 — 3-level 휴리스틱 (LLM 호출 없음, 결정적): - concr",
|
||||
"imports": [
|
||||
"src/retrieval/types"
|
||||
]
|
||||
},
|
||||
"src/retrieval/index.ts": {
|
||||
"mtimeMs": 1779764602654.6475,
|
||||
"size": 24268,
|
||||
"lines": 514,
|
||||
"mtimeMs": 1780030794352.5571,
|
||||
"size": 26702,
|
||||
"lines": 549,
|
||||
"role": "RetrievalOrchestrator — Unified RAG Pipeline Astra의 모든 검색 소스를 통합 관리하는 오케스트레이터입니다. 검색 흐름: ① Query Planning — 의도 분류 + 검색 전략 결정 ② Parallel Search — Brain + Memory + Project + Episode 동시 검색 ③ Result Fusio",
|
||||
"imports": [
|
||||
"src/config",
|
||||
@@ -2075,9 +2192,18 @@
|
||||
"src/retrieval/contextBudget",
|
||||
"src/retrieval/brainIndex",
|
||||
"src/retrieval/lessonHelpers",
|
||||
"src/retrieval/embeddings"
|
||||
"src/retrieval/embeddings",
|
||||
"src/retrieval/actionabilityScoring",
|
||||
"src/retrieval/hierarchicalLevel"
|
||||
]
|
||||
},
|
||||
"src/retrieval/intentClarification.ts": {
|
||||
"mtimeMs": 1780032375486.0012,
|
||||
"size": 7076,
|
||||
"lines": 141,
|
||||
"role": "Intent Clarification — 모호한 질의에서 추측 답변 대신 질문 던지기 지시. 사용자 피드백: \"ASTRA 는 질문을 받으면 즉시 답변을 생성하려 함. 하지만 '날카로운' 파악을 위해서는 질문 뒤의 '실행 목적' 을 먼저 정의해야\". 예: \"배포해줘\" → 환경 (dev/prod) / 태그 묻기. \"그 부분 고쳐줘\" → 어느 파일/모듈인지 묻기",
|
||||
"imports": []
|
||||
},
|
||||
"src/retrieval/knowledgeMix.ts": {
|
||||
"mtimeMs": 1778821115241.6562,
|
||||
"size": 7786,
|
||||
@@ -2104,6 +2230,22 @@
|
||||
"role": "Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.",
|
||||
"imports": []
|
||||
},
|
||||
"src/retrieval/semanticRerank.ts": {
|
||||
"mtimeMs": 1780030762153.9978,
|
||||
"size": 8205,
|
||||
"lines": 213,
|
||||
"role": "LLM Semantic Re-ranking — TF-IDF / 임베딩이 놓치는 의도 매치를 작은 LLM 호출 한 번으로 잡는다. 동작: 1. 1차 검색(TF-IDF + embedding + 부스트들) 결과의 상위 K (기본 15) 후보를 추출 2. 가벼운 프롬프트로 LLM 에게 \"이 중 query 의도에 가장 부합하는 순서로 ID 나열\" 요청 3. LLM ",
|
||||
"imports": [
|
||||
"src/retrieval/types"
|
||||
]
|
||||
},
|
||||
"src/retrieval/terminologyBlock.ts": {
|
||||
"mtimeMs": 1780033577419.837,
|
||||
"size": 5735,
|
||||
"lines": 137,
|
||||
"role": "Terminology Dictionary — 프로젝트 표준 용어집을 시스템 프롬프트에 주입. 사용자 제안: \"표준 표기 강제 + 답변 내 표기 일관성 검증\". 예: runway vs 런웨이, P-Reinforce vs p-reinforce, Chronicle vs 크로니클. 설계 — 사용자 편집 markdown 파일: - 위치: <workspace>/.as",
|
||||
"imports": []
|
||||
},
|
||||
"src/retrieval/types.ts": {
|
||||
"mtimeMs": 1779764602656.6587,
|
||||
"size": 2485,
|
||||
@@ -3206,7 +3348,7 @@
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/chronicle.config.json": {
|
||||
"mtimeMs": 1779783003968.8809,
|
||||
"mtimeMs": 1780037924615.7922,
|
||||
"size": 371,
|
||||
"lines": 11,
|
||||
"role": "JSON configuration",
|
||||
@@ -3387,6 +3529,27 @@
|
||||
"role": "ADR: E:\\Wiki\\connectai 프로젝트에 대한 너의 평가 해줘.",
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/decisions/ADR-0026-질문이-있어-내가-당근이라는-중고-거래-사이트에서-8tb-hdd를-구매했어-안전거래-escrow-를-사용했어.md": {
|
||||
"mtimeMs": 1780032500203.2405,
|
||||
"size": 2483,
|
||||
"lines": 19,
|
||||
"role": "ADR: 질문이 있어, 내가 당근이라는 중고 거래 사이트에서 8TB HDD를 구매했어. 안전거래 (Escrow)를 사용했어. 일단 물건을 어제 받았고, ...",
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/decisions/ADR-0027-connectai-프로젝트의-아스트라-대부분-답을-잘-줘-근대-간혹-오타가-발생하거나-상황에-맞지-않는-단어.md": {
|
||||
"mtimeMs": 1780033038688.5405,
|
||||
"size": 1749,
|
||||
"lines": 19,
|
||||
"role": "ADR: connectAI 프로젝트의 아스트라 대부분 답을 잘 줘. 근대 간혹 오타가 발생하거나 상황에 맞지 않는 단어를 간혹 사용해서 혼선을 줄때가 있...",
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/decisions/ADR-0028-질문이-있어-내가-당근이라는-중고-거래-사이트에서-8tb-hdd를-구매했어-안전거래-escrow-를-사용했어.md": {
|
||||
"mtimeMs": 1780037924606.6091,
|
||||
"size": 2481,
|
||||
"lines": 19,
|
||||
"role": "ADR: 질문이 있어, 내가 당근이라는 중고 거래 사이트에서 8TB HDD를 구매했어. 안전거래 (Escrow)를 사용했어. 일단 물건을 어제 받았고, ...",
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/development/2026-05-02_answer-format-readability-tuning.md": {
|
||||
"mtimeMs": 1778028987330.4185,
|
||||
"size": 1564,
|
||||
@@ -3618,6 +3781,20 @@
|
||||
"role": "Development Log: /Volumes/Data/project/Antigravity/ConnectAI 코드 리뷰하고 사용자 입장에서 개선이 필요한 부분이 있는지, 제안...",
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/development/2026-05-29_e-wiki-connectai-프로젝트에-대해서-이야기-하는거야_implementation.md": {
|
||||
"mtimeMs": 1780031872506.9214,
|
||||
"size": 1611,
|
||||
"lines": 28,
|
||||
"role": "Development Log: E:\\Wiki\\connectai 프로젝트에 대해서 이야기 하는거야.",
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/development/2026-05-29_아스트라를-지금-보다-더-날카롭고-신뢰감이-높은-아이-그리고-나의-의도를-잘-파악하게-하기-위해서는-어떤부분_implementation.md": {
|
||||
"mtimeMs": 1780031951811.4153,
|
||||
"size": 1670,
|
||||
"lines": 22,
|
||||
"role": "Development Log: 아스트라를 지금 보다 더 날카롭고 신뢰감이 높은 아이 그리고 나의 의도를 잘 파악하게 하기 위해서는 어떤부분을 강화하는게 좋을까?",
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/discussions/2026-05-13_volumes-data-project-antigravity-connectai-이-프로젝트-작업-할-거야.md": {
|
||||
"mtimeMs": 1778720117404.2578,
|
||||
"size": 668,
|
||||
@@ -3710,9 +3887,9 @@
|
||||
"imports": []
|
||||
},
|
||||
"docs/records/ConnectAI/timeline.md": {
|
||||
"mtimeMs": 1779783003962.848,
|
||||
"size": 13603,
|
||||
"lines": 212,
|
||||
"mtimeMs": 1780037924607.6199,
|
||||
"size": 14606,
|
||||
"lines": 227,
|
||||
"role": "Project Timeline",
|
||||
"imports": []
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "직답 결과 — single-pass mock 응답입니다.",
|
||||
"createdAt": 1779872260856,
|
||||
"createdAt": 1780037632204,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "---\nid: wiki_on\ndate: 2026-05-27T08:57:40.858Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\n직답 결과 — single-pass mock 응답입니다.\n\n직답 결과 — single-pass mock 응답입니다.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `0/100` | ✅ Low |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[DIRECT]** 답변 작성 중... (단일 호출 fast-path) (24ms)\n",
|
||||
"createdAt": 1779872260858,
|
||||
"result": "---\nid: wiki_on\ndate: 2026-05-29T06:53:52.206Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\n직답 결과 — single-pass mock 응답입니다.\n\n직답 결과 — single-pass mock 응답입니다.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `0/100` | ✅ Low |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[DIRECT]** 답변 작성 중... (단일 호출 fast-path) (23ms)\n",
|
||||
"createdAt": 1780037632207,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"missionId": "wiki_on",
|
||||
"status": "completed",
|
||||
"startTime": "2026-05-27T08:57:40.831Z",
|
||||
"startTime": "2026-05-29T06:53:52.180Z",
|
||||
"totalElapsedMs": 28,
|
||||
"results": {
|
||||
"direct": "직답 결과 — single-pass mock 응답입니다."
|
||||
@@ -12,16 +12,16 @@
|
||||
{
|
||||
"from": "idle",
|
||||
"to": "direct",
|
||||
"durationMs": 24,
|
||||
"durationMs": 23,
|
||||
"message": "답변 작성 중... (단일 호출 fast-path)",
|
||||
"ts": "2026-05-27T08:57:40.855Z"
|
||||
"ts": "2026-05-29T06:53:52.203Z"
|
||||
},
|
||||
{
|
||||
"from": "direct",
|
||||
"to": "completed",
|
||||
"durationMs": 3,
|
||||
"durationMs": 4,
|
||||
"message": "미션 완료",
|
||||
"ts": "2026-05-27T08:57:40.858Z"
|
||||
"ts": "2026-05-29T06:53:52.207Z"
|
||||
}
|
||||
],
|
||||
"resilienceMetrics": {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
||||
"createdAt": 1779872267564,
|
||||
"createdAt": 1780037639311,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
|
||||
"createdAt": 1779872267563,
|
||||
"createdAt": 1780037639310,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]",
|
||||
"createdAt": 1779872267560,
|
||||
"createdAt": 1780037639305,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
|
||||
"createdAt": 1779872267562,
|
||||
"createdAt": 1780037639308,
|
||||
"modelVersion": "unknown"
|
||||
}
|
||||
+8
-8
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"missionId": "stress_conflict_1779872267539",
|
||||
"missionId": "stress_conflict_1780037639285",
|
||||
"status": "completed",
|
||||
"startTime": "2026-05-27T08:57:47.539Z",
|
||||
"startTime": "2026-05-29T06:53:59.285Z",
|
||||
"totalElapsedMs": 26,
|
||||
"results": {
|
||||
"outline": "[{\"heading\":\"본문\",\"scope\":\"전체 답변\"}]",
|
||||
@@ -14,30 +14,30 @@
|
||||
{
|
||||
"from": "idle",
|
||||
"to": "outline",
|
||||
"durationMs": 20,
|
||||
"durationMs": 19,
|
||||
"message": "답변 구조 잡는 중...",
|
||||
"ts": "2026-05-27T08:57:47.559Z"
|
||||
"ts": "2026-05-29T06:53:59.304Z"
|
||||
},
|
||||
{
|
||||
"from": "outline",
|
||||
"to": "section",
|
||||
"durationMs": 2,
|
||||
"durationMs": 3,
|
||||
"message": "본문 작성 중...",
|
||||
"ts": "2026-05-27T08:57:47.561Z"
|
||||
"ts": "2026-05-29T06:53:59.307Z"
|
||||
},
|
||||
{
|
||||
"from": "section",
|
||||
"to": "polish",
|
||||
"durationMs": 2,
|
||||
"message": "최종 다듬기 중...",
|
||||
"ts": "2026-05-27T08:57:47.563Z"
|
||||
"ts": "2026-05-29T06:53:59.309Z"
|
||||
},
|
||||
{
|
||||
"from": "polish",
|
||||
"to": "completed",
|
||||
"durationMs": 2,
|
||||
"message": "미션 완료",
|
||||
"ts": "2026-05-27T08:57:47.565Z"
|
||||
"ts": "2026-05-29T06:53:59.311Z"
|
||||
}
|
||||
],
|
||||
"resilienceMetrics": {
|
||||
+906
@@ -1,5 +1,911 @@
|
||||
# Astra Patch Notes
|
||||
|
||||
## v2.2.193 (2026-05-29)
|
||||
### 📚 /help — 슬래시 명령 카테고리 브라우저 + 엔진 상태
|
||||
누적된 슬래시 명령이 25개+. 사용자 발견 부담을 줄이기 위해 카테고리별 그룹핑 + ASTRA 추론 엔진 상태(6종 verification 블록 on/off) 한 화면.
|
||||
|
||||
**신규 `/help` 명령:**
|
||||
- `/help` — 전체 명령 카테고리별 + 엔진 상태
|
||||
- `/help <명령>` — 특정 명령의 짧은 설명 + 세부 도움말 안내
|
||||
|
||||
**카테고리 자동 그룹핑 (7개):**
|
||||
- ☀️ **일과 리듬 (Daily Cycle)** — /morning · /evening · /weekly · /cohort · /standup
|
||||
- 🏢 **4인 팀 운영 트래커** — /runway · /customers · /hire (event-sourced .jsonl)
|
||||
- 📋 **작업·블로커·1:1** — /task · /blocked · /onesie · /decisions
|
||||
- ✉️ **외부 출력·기록** — /draft · /feedback
|
||||
- 🔬 **리서치·분석** — /research · /benchmark · /youtube · /blog · /wikify · /meet (Datacollect bridge)
|
||||
- ⚙️ **시스템·메모리** — /memory · /glossary
|
||||
- 📈 **주식·외부** — /stocks
|
||||
- 🔌 **기타·외부 플러그인** — 위 카테고리에 안 잡히는 모든 명령 (외부 플러그인 자동 포함)
|
||||
|
||||
**ASTRA 추론 엔진 상태 (자동 표시):**
|
||||
6종 verification 블록의 *현재 on/off* 를 한 화면. 각 블록의 system prompt 라벨과 *어느 시점* 에 동작하는지 표시:
|
||||
- ✓ Intent Clarification — `[INTENT CLARIFICATION GUIDANCE]` (답변 시작 전)
|
||||
- ✓ Terminology Dictionary — `[TERMINOLOGY DICTIONARY]` (답변 작성 중)
|
||||
- ✓ Conflict Surface — `[CONFLICT WARNINGS]` (답변 작성 중)
|
||||
- ✓ Chain-of-Verification — `[VERIFICATION CHECKLIST]` (답변 작성 중)
|
||||
- ✓ Citation Trace — `[CITATION TRACE]` (답변 끝)
|
||||
- ⊘ Post-hoc Self-Check — `footer` (답변 완료 후, opt-in)
|
||||
|
||||
추가 신호 (검색 단계): Recency · Actionability · Hierarchical Level · Semantic Re-rank (opt-in).
|
||||
메모리 관리: Temporal Markers · Distillation Loop.
|
||||
|
||||
**동적 명령 목록:**
|
||||
- `listSlashCommands()` 사용 — 외부 플러그인이 추가한 명령도 자동 노출
|
||||
- 카테고리 매칭 함수 기반 — 새 명령 추가 시 매칭 함수만 업데이트하면 자동 분류
|
||||
|
||||
**신규 패키징:** `astra-2.2.193.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.192 (2026-05-29)
|
||||
### 📖 Terminology Dictionary + Term Check (엔진 변경)
|
||||
사용자 제안 "출력 품질 관리" 중 *진짜 새 가치* 만 추출 — Terminology Glossary 단일 메커니즘으로 #1(typo/용어 self-check) + #2(표준 용어집) 통합. **#3 sampling 튜닝은 이미 적용된 상태라 스킵**.
|
||||
|
||||
**감사 결과 (스킵한 이유):**
|
||||
- #3 Temperature/Sampling 튜닝 → 이미 `modelTemperature=0.3`, `datacollectSynthesisTemperature=0.1`, `top_p=0.85`, `top_k=20`, `repeat_penalty=1.1`, `repairKoreanGlitches` 적용. 추가 튜닝 시 다양성↓ 위험.
|
||||
- #1 별도 [Output Quality Control] 블록 → 시스템 프롬프트 비대 위험, *Glossary 블록 내 Term Check sub-clause* 로 통합이 깔끔.
|
||||
|
||||
---
|
||||
|
||||
**신규 메커니즘 — `.astra/glossary.md` 사용자 편집 파일**
|
||||
|
||||
- **위치**: `<workspace>/.astra/glossary.md` (설정으로 변경 가능)
|
||||
- **형식**: 자유 markdown — ASTRA 는 형식 강제 안 함, 본문 통째로 시스템 프롬프트에 주입
|
||||
- **권장 컨벤션** (`/glossary init` 템플릿):
|
||||
- `## 표준 표기` — `**ASTRA** (X: astra) — 본 extension` 형태
|
||||
- `## 영-한 표기 컨벤션` — Performance → 성능
|
||||
- `## 금지·비추 표현`
|
||||
- `## 슬래시 명령 표기`
|
||||
- **캐시**: mtime 기반 — 사용자가 편집할 때만 다시 읽음 (매 turn 디스크 read 0회)
|
||||
|
||||
**시스템 프롬프트 블록 `[TERMINOLOGY DICTIONARY]`:**
|
||||
1. 글로서리 본문 (cap 4000자, 초과분 잘림 안내)
|
||||
2. **[Term Check — 답변 직전 자기 점검]** 5개 규칙 (사용자 제안 #1 통합):
|
||||
- 표준 표기 우선, 변형·번역 임의 적용 금지
|
||||
- 표기 흔들림 방지 (같은 답변 안에서 일관)
|
||||
- 새 용어 도입 시 "새 용어: X" 명시
|
||||
- 금지 표기 검증 + 재작성
|
||||
- 표기 확신 없으면 "글로서리에 없어 일반 표기 사용" 명시
|
||||
|
||||
**신규 모듈: [terminologyBlock.ts](src/retrieval/terminologyBlock.ts)**
|
||||
- `buildTerminologyBlock(opts)` — 글로서리 + Term Check 합쳐 블록 생성
|
||||
- mtime cache 내장
|
||||
- `GLOSSARY_TEMPLATE` — 권장 템플릿 export
|
||||
|
||||
**신규 슬래시 명령 `/glossary`:**
|
||||
- `/glossary` — 상태 카드 (파일 존재/크기/cap/주입 여부/첫 5줄 미리보기)
|
||||
- `/glossary path` — 절대 경로
|
||||
- `/glossary init` — 권장 템플릿으로 초기 파일 생성 (이미 있으면 덮어쓰지 않음)
|
||||
- `/glossary reload` — 캐시 비우기 (편집 직후 즉시 반영 강제)
|
||||
- `/glossary help` — 가이드
|
||||
|
||||
**Wiring:**
|
||||
- [memoryContext.ts](src/lib/contextBuilders/memoryContext.ts) — 매 turn 글로서리 블록 계산 → `turnCtx.terminology`
|
||||
- [agent.ts](src/agent.ts) — turn reset 시 비움, system prompt build 시 전달
|
||||
- [buildAstraModeSystemPrompt.ts](src/agent/handlePrompt/buildAstraModeSystemPrompt.ts) — `[CONTEXT]` 밖, `[INTENT CLARIFICATION]` 직후 (verification 블록 군집)
|
||||
- Casual conversation 모드 비활성 (greeting 에 용어 강제 의미 없음)
|
||||
|
||||
**신규 설정 3개:**
|
||||
- `g1nation.glossaryEnabled` (boolean, 기본 true) — 마스터
|
||||
- `g1nation.glossaryPath` (string, 기본 `.astra/glossary.md`)
|
||||
- `g1nation.glossaryMaxBodyLength` (number 500~20000, 기본 4000)
|
||||
|
||||
**ASTRA 검증 6종 (engine 변경 누적, v2.2.183~192):**
|
||||
| # | 블록 | 시점 | 빌드 |
|
||||
|---|---|---|---|
|
||||
| INTENT | `[INTENT CLARIFICATION GUIDANCE]` | 답변 시작 전 | v2.2.190 |
|
||||
| **TERMINOLOGY** | **`[TERMINOLOGY DICTIONARY]`** | **답변 작성 중 (용어)** | **v2.2.192** |
|
||||
| CONFLICT | `[CONFLICT WARNINGS]` | 답변 작성 중 | v2.2.183 |
|
||||
| COVE | `[VERIFICATION CHECKLIST]` | 답변 작성 중 | v2.2.184 |
|
||||
| CITATION | `[CITATION TRACE]` | 답변 끝 | v2.2.190 |
|
||||
| SELF-CHECK | footer (separate LLM) | 답변 완료 후 | v2.2.191 |
|
||||
|
||||
**신규 패키징:** `astra-2.2.192.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.191 (2026-05-29)
|
||||
### 🔍 추론 신뢰도 — Post-hoc Self-Check (2-pass 검증, 엔진 변경)
|
||||
v2.2.190 의 #1+#2 (Intent + Citation) 짝꿍. 답변 *완료 후* 별도 LLM 호출 1회로 사후 검증. CoVe (v2.2.184) 가 *답변 전* instructional self-check 이면, 이번엔 *답변 후* 객관 평가.
|
||||
|
||||
**설계 결정 — 비동기 후처리:**
|
||||
- 기존 `_maybeEmitDevilRebuttal` (Devil Agent) 패턴 그대로 — main turn 종료 후 비동기, 실패해도 답변 보존
|
||||
- 답변이 webview 에 렌더된 직후 self-check footer streamChunk 로 append
|
||||
- 사용자는 답변 즉시 보고, 1~6초 후 self-check 결과 한 줄이 아래에 추가됨
|
||||
|
||||
**평가 3차원 (LLM 출력 JSON):**
|
||||
1. `answersQuestion` — yes/partial/no — 답변이 질문에 직접 답하나
|
||||
2. `grounded` — yes/partial/no/unknown — 답변이 제공된 출처에 근거하나
|
||||
3. `contradiction` — none/minor/major — 답변에 논리 모순 있나
|
||||
|
||||
**Footer 형식:**
|
||||
```
|
||||
---
|
||||
_🔍 **Self-check**: 답함=✓ · 근거=○ · 모순=없음 — 답변은 직접적이나 일부 주장이 모델 일반 지식 기반 _(2.4s · gemma2:2b)__
|
||||
```
|
||||
|
||||
**신규 모듈: [postHocSelfCheck.ts](src/agent/postHocSelfCheck.ts)**
|
||||
- `postHocSelfCheck(prompt, answer, sources, opts)` — LLM 호출 + JSON 파싱 + 안전 fallback
|
||||
- `formatSelfCheckFooter(result, model)` — 마크다운 footer 한 줄
|
||||
|
||||
**안전망:**
|
||||
- 응답 파싱 실패 / 타임아웃 / 오류 → 흐릿한 한 줄 footer ("⊘ failed: ...")
|
||||
- 답변 자체는 영향 없음 (Devil Agent 와 동일 패턴)
|
||||
- 짧은 timeout (기본 6초)
|
||||
- 별도 빠른 모델 지정 가능
|
||||
|
||||
**Wiring:**
|
||||
- [memoryContext.ts](src/lib/contextBuilders/memoryContext.ts) — 검색 후 `turnCtx.selfCheckSources` 에 상위 5개 chunk (title, excerpt) 저장
|
||||
- [agent.ts](src/agent.ts) — Devil rebuttal 옆에 `_maybePostHocSelfCheck` 호출 추가, 결과를 streamChunk 로 append
|
||||
|
||||
**신규 설정 3개:**
|
||||
- `g1nation.selfCheckEnabled` (boolean, **기본 false** — opt-in)
|
||||
- `g1nation.selfCheckModel` (string, 비면 defaultModel)
|
||||
- `g1nation.selfCheckTimeoutSec` (number 1~60, 기본 6)
|
||||
|
||||
---
|
||||
|
||||
**ASTRA 검증 5종 완전체 (engine 변경 누적):**
|
||||
| # | 블록 | 시점 | 모드 |
|
||||
|---|---|---|---|
|
||||
| INTENT (v2.2.190) | `[INTENT CLARIFICATION GUIDANCE]` | 답변 시작 전 | instructional |
|
||||
| CONFLICT (v2.2.183) | `[CONFLICT WARNINGS]` | 답변 작성 중 | instructional |
|
||||
| COVE (v2.2.184) | `[VERIFICATION CHECKLIST]` | 답변 작성 중 | instructional |
|
||||
| CITATION (v2.2.190) | `[CITATION TRACE]` | 답변 끝 | instructional |
|
||||
| **SELF-CHECK (v2.2.191)** | footer 한 줄 | **답변 완료 후** | **separate LLM call** |
|
||||
|
||||
**활용 권장:**
|
||||
- 기본 ON 4개 (INTENT/CONFLICT/COVE/CITATION) — 즉시 활용
|
||||
- `g1nation.selfCheckModel` 에 빠른 모델 설정 (예: gemma2:2b)
|
||||
- `g1nation.selfCheckEnabled = true` — opt-in. footer 한 줄로 답변 품질 즉시 검증
|
||||
|
||||
**신규 패키징:** `astra-2.2.191.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.190 (2026-05-29)
|
||||
### 🎯 추론 신뢰도 — Intent Clarification + Citation Trace (엔진 변경)
|
||||
사용자 제안 "Pre-inference Contextualization + Grounding" 중 ASTRA 에 *진짜 빈* 두 영역. ASTRA 검색 엔진 자체 변경 — 매 채팅 turn 자동 적용, 슬래시 명령 없음.
|
||||
|
||||
**감사 결과 (분리한 이유):**
|
||||
- 제안 #1 Intent Clarification → **갭 명확**: ASTRA 는 항상 답을 시도, *모호하면 묻기* 메커니즘 없음
|
||||
- 제안 #2 Citation Trace → CoVe Strict (v2.2.184) 와 부분 중복, *가벼운 always-on 형제* 로 추가
|
||||
- 제안 #3 Self-Correction Loop → CoVe 가 *instructional 형태* 로 이미 구현, *post-hoc 2-pass* 는 v2.2.191 에서 별도 빌드
|
||||
|
||||
---
|
||||
|
||||
**#1 Intent Clarification — 모호 질의 역질문 (기본 ON)**
|
||||
|
||||
신규 모듈: [intentClarification.ts](src/retrieval/intentClarification.ts)
|
||||
|
||||
- **5개 의도 차원 휴리스틱**:
|
||||
- `환경` — 배포/롤백 trigger + dev/prod 명시 검사
|
||||
- `대상` — 고쳐/수정 trigger + 파일/모듈/`.ts` 검사
|
||||
- `범위` — 리팩토링/정리 trigger + 전체/일부 검사
|
||||
- `포맷` — 요약/보고서 trigger + 표/리스트/markdown 검사
|
||||
- `마감` — 빨리/급함 trigger + 오늘/내일 검사
|
||||
- **trigger 만 있고 specifier 없으면 missing**
|
||||
- **Strictness 3-level**: low (2개+ missing), medium (1개+ missing, 기본), high (짧은 질의 추가 검사)
|
||||
- **시스템 프롬프트 블록 `[INTENT CLARIFICATION GUIDANCE]`**: 모호 차원 + 권장 역질문 예시 + "사용자가 '추정해도 OK' 했으면 가정 명시 후 진행" 등 4개 규칙
|
||||
|
||||
**효과**: "배포해줘" → ASTRA 가 추측하지 않고 "dev 인가요 prod 인가요?" 한 줄 질문 먼저.
|
||||
|
||||
---
|
||||
|
||||
**#2 Citation Trace — 답변 끝 출처 한 줄 (기본 ON)**
|
||||
|
||||
신규 모듈: [citationTrace.ts](src/retrieval/citationTrace.ts)
|
||||
|
||||
- **CoVe Strict 와 차이**: CoVe Strict = 모든 사실 주장 뒤 inline `[S1]` (verbose). Citation Trace = 답변 끝 한 줄 정리 (가벼움).
|
||||
- **시스템 프롬프트 블록 `[CITATION TRACE]`**: "답변 끝에 *출처:* 한 줄, 사용한 출처만 나열, 일반 지식이면 그 사실 명시" 등 5개 규칙
|
||||
- **CoVe 와 시너지**: CoVe 가 `[S1]..[SN]` 라벨 제공, Citation 이 끝에 정리
|
||||
|
||||
**효과**: 답변 끝에 자동으로 `*출처:* file1.md · chunk-title2` 같은 한 줄 → 사용자 검증 가능, 할루시네이션 억제.
|
||||
|
||||
---
|
||||
|
||||
**Wiring (둘 다):**
|
||||
- [memoryContext.ts](src/lib/contextBuilders/memoryContext.ts) — 검색 직후 두 블록 계산 → `turnCtx.intentClarification` / `turnCtx.citationTrace`
|
||||
- [agent.ts](src/agent.ts) — turn reset 시 비움, system prompt build 시 전달
|
||||
- [buildAstraModeSystemPrompt.ts](src/agent/handlePrompt/buildAstraModeSystemPrompt.ts) — `[CONTEXT]` 밖 주입. Intent block 은 다른 verification 블록보다 *앞* (모호하면 답변 자체를 안 만들어야 하므로). Citation 은 끝.
|
||||
|
||||
**신규 설정 3개:**
|
||||
- `g1nation.intentClarificationEnabled` (boolean, 기본 true)
|
||||
- `g1nation.intentClarificationStrictness` (low/medium/high, 기본 medium)
|
||||
- `g1nation.citationTraceEnabled` (boolean, 기본 true)
|
||||
|
||||
**ASTRA 추론 엔진 검증 3종 (#2/#3/#1 시리즈와 함께):**
|
||||
| 블록 | 시점 | 용도 |
|
||||
|---|---|---|
|
||||
| `[INTENT CLARIFICATION GUIDANCE]` | 답변 시작 전 | 모호하면 묻기 |
|
||||
| `[CONFLICT WARNINGS]` (v2.2.183) | 답변 작성 중 | 충돌 출처 양측 명시 |
|
||||
| `[VERIFICATION CHECKLIST]` (v2.2.184) | 답변 작성 중 | 근거 매핑 + 자기 점검 |
|
||||
| `[CITATION TRACE]` | 답변 끝 | 사용 출처 한 줄 정리 |
|
||||
|
||||
**다음 (v2.2.191):**
|
||||
- **#3 Post-hoc Self-Check** — 답변 *완료 후* LLM 호출 1회로 검증 (semantic re-rank 와 유사 패턴, 기본 OFF / opt-in)
|
||||
|
||||
**신규 패키징:** `astra-2.2.190.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.189 (2026-05-29)
|
||||
### 📅 4인 팀 운영 12단계 — /weekly 주간 리뷰 카드 (대표용)
|
||||
일 단위 리듬은 `/morning` · `/evening` 으로 단조. 주 단위 리듬은 비어 있었음 (`/standup weekly` 는 팀 공유용, 대표 전략 시야 아님). 그 갭을 메움.
|
||||
|
||||
**신규 `/weekly` 명령** (인자 없음, 매주 일요일/월요일 1회 실행 권장)
|
||||
|
||||
**`/standup weekly` 와 차이:**
|
||||
- `/standup weekly` → *팀에 공유* (멤버별 완료/진행/블로커, 슬랙 복붙)
|
||||
- `/weekly` → *대표가 본다* (cross-data delta, 회고 프롬프트)
|
||||
|
||||
**섹션 구성:**
|
||||
|
||||
**1. ✅ 이번 주 진척**
|
||||
- **작업**: 멤버별 카운트 + 지난 주 대비 ↑↓→
|
||||
- **📒 고객**: 신규/갱신/위험/이탈 이벤트 카운트 + 신규 MRR 합산
|
||||
- **🎯 채용**: 신규 후보/단계 이동/합격 이벤트
|
||||
- **💰 재무**: 주간 지출/수입/순 burn + 잔고 시작→끝 Δ
|
||||
- **📋 결정**: 이번 주 Chronicle ADR (최대 5건)
|
||||
|
||||
**2. 🔄 지난 주 대비 (ISO week 기반 월~일)**
|
||||
- 작업 / 신규 고객 / 이탈 / 순 burn / 채용 이벤트 — 각각 ↑↓→ delta
|
||||
- 화살표 + (이번 X ← 지난 Y) 형식
|
||||
|
||||
**3. 🌅 다음 주 준비**
|
||||
- 14일 내 갱신 (D-7 🔴 / D-14 🟡)
|
||||
- 다음 주 마감 작업 카운트
|
||||
- 정체 후보 (7일+ 미변동) top 3
|
||||
|
||||
**4. 💭 주간 회고 한 줄**
|
||||
- 6개 회고 질문 풀, 주차 결정적 선택 (같은 주 = 같은 질문)
|
||||
- 예: "이번 주 가장 중요한 결정은 무엇이었나? 다음 주에 어떤 결정을 미루면 안 되나?"
|
||||
|
||||
**ISO Week 기반 윈도우:**
|
||||
- 월요일 00:00 ~ 일요일 23:59:59
|
||||
- "YYYY-Wnn (M/D-M/D)" 라벨
|
||||
|
||||
**4인 팀 운영 누적 (1~12 — 일·주 리듬 완성):**
|
||||
|
||||
| 시간 | 명령 |
|
||||
|---|---|
|
||||
| 🌅 아침 (일) | `/morning` |
|
||||
| 🌞 낮 | `/blocked`, `/task`, `/onesie @멤버` |
|
||||
| 🌙 저녁 (일) | `/evening` |
|
||||
| 📊 팀 공유 | `/standup` |
|
||||
| 📅 **주말 (대표용)** | **`/weekly`** ⭐ |
|
||||
| 📈 분기 | `/cohort` |
|
||||
| 💰 트래커 | `/runway`, `/customers`, `/hire` |
|
||||
| 📋 의사결정 | `/decisions`, `/draft`, `/feedback` |
|
||||
|
||||
**신규 패키징:** `astra-2.2.189.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.188 (2026-05-29)
|
||||
### 📈 4인 팀 운영 11단계 — /cohort MoM 추세 분석
|
||||
`/customers` + `/runway` 의 events 가 누적되니 *추세* 가 보임. 한 시점 스냅샷(`/morning` 같은) 이 아닌 *6~12개월 흐름* 으로 비즈니스 방향 검토. 회계 SaaS 의 board metric 의 미니 버전.
|
||||
|
||||
**신규 `/cohort` 명령:**
|
||||
- `/cohort` — 최근 **6개월** 추세 (기본)
|
||||
- `/cohort yearly` — 최근 12개월
|
||||
- `/cohort <N>` — 최근 N개월 (1~24)
|
||||
|
||||
**섹션 구성:**
|
||||
|
||||
**1. 고객 & MRR 추이 (월별 테이블)**
|
||||
| 월 | 신규 | 갱신 | 이탈 | MRR Δ |
|
||||
- 누계: 신규 +N · 이탈 -M · 순 ±K
|
||||
- MRR 순증 (추가 신규 MRR 만, 이탈 차감은 history 부재로 미반영)
|
||||
- 월평균 신규/이탈 + 이탈/신규 비율 (단순 churn-rate 근사)
|
||||
|
||||
**2. 재무 추이 (월별 테이블)**
|
||||
| 월 | 지출 | 수입 | 순 burn | 월말 잔고 |
|
||||
- 6개월 누계
|
||||
- 월평균 burn
|
||||
|
||||
**3. 💡 인사이트 한 줄 (자동 도출)**
|
||||
- 🟢 최근 3개월 신규 가속 (이전 3개월 대비 +20%↑)
|
||||
- 🟡 신규 둔화 (-20%↓)
|
||||
- 🔴 burn 가속 (+30%↑)
|
||||
- 🟢 매출 커버리지 80%+ → 흑자 임박
|
||||
- 🟡 매출 커버리지 20% 미만 → 매출 기반 약함
|
||||
|
||||
**데이터 출처:**
|
||||
- `<workspace>/.astra/customers.jsonl` (add/renew/churn timestamp 그룹핑)
|
||||
- `<workspace>/.astra/runway.jsonl` (expense/revenue/snapshot 그룹핑)
|
||||
|
||||
**현 시점 한계 (의도적 단순화):**
|
||||
- Churn MRR 감소 = 직전 mrr 추적 안 함 — 보수적으로 0 계산
|
||||
- Update event 의 mrr 차이 = 0 (state 추적 없음)
|
||||
- 향후 v2: customers store 에 monthly snapshot 추가하면 정확한 churn MRR 계산 가능
|
||||
|
||||
**4인 팀 운영 누적 (1~11단계):**
|
||||
`/decisions` · `/onesie` · `/draft` · `/feedback` · `/blocked` · `/standup` · `/runway` · `/customers` · `/hire` · `/morning` · `/evening` · **`/cohort`** ⭐
|
||||
|
||||
**신규 패키징:** `astra-2.2.188.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.187 (2026-05-29)
|
||||
### 🔍 Retrieval 의도 매칭 — Hierarchical + LLM Semantic Re-rank (엔진 변경)
|
||||
사용자 제안 "지식 발굴 로직 강화" 중 *진짜 새 가치* 2개 적용. (제안의 Re-ranking 항목들 — Recency / Actionability / Conflict Check — 은 이미 v2.2.183~185 에서 완료. 제안자가 빌드 모름.)
|
||||
|
||||
**둘 다 ASTRA 검색 엔진 자체 변경 — 슬래시 명령 추가 없음, 매 채팅 turn 자동 적용.**
|
||||
|
||||
---
|
||||
|
||||
**#2 Hierarchical Context Window — 추상도 매칭 (기본 ON)**
|
||||
|
||||
신규 모듈: `src/retrieval/hierarchicalLevel.ts`
|
||||
|
||||
- **3-level 분류** (LLM 호출 없음, 결정적 휴리스틱):
|
||||
- `concrete` — 코드/로그/디버그/실행 (folder hints: code, log, debug; title hints: fix, bug, error)
|
||||
- `operational` — 작업/일정/운영/회의 (기본값)
|
||||
- `strategic` — 전략/비전/방향/의사결정 (folder hints: strategy, vision, decision; title hints: plan, direction)
|
||||
- **질의 분류**: 키워드 우열 — `'전략 검토'` → strategic, `'버그 고쳐'` → concrete
|
||||
- **재가중 정책**:
|
||||
- 같은 레벨 → × 1.15 (boost)
|
||||
- 인접 레벨 (concrete↔operational, operational↔strategic) → 변화 없음
|
||||
- 양 끝 mismatch (concrete↔strategic) → × 0.70 (penalty)
|
||||
- **시점**: `RetrievalOrchestrator.retrieve()` 의 normalize 직후, 토큰 예산 직전 — 어떤 chunk 가 살아남는지에 영향
|
||||
- **위치**: [hierarchicalLevel.ts](src/retrieval/hierarchicalLevel.ts) + retrieve() 파이프라인 stage ③-c
|
||||
|
||||
---
|
||||
|
||||
**#4 LLM Semantic Re-rank — 의도 매칭 (기본 OFF, 명시적 opt-in)**
|
||||
|
||||
신규 모듈: `src/retrieval/semanticRerank.ts`
|
||||
|
||||
- **로직**: 토큰 예산 통과한 `selectedChunks` 상위 K(기본 15) 를 LLM 한 번 호출로 의도 부합도 순 재정렬
|
||||
- **프롬프트**: `[C1]..[CK]` 라벨 + 발췌문 → LLM 이 JSON `{"ranking":[3,1,5,...]}` 한 줄 출력
|
||||
- **파싱 보호**: 응답 누락/중복/번호 오류 시 원순서 fallback. 타임아웃 시도 원순서 fallback. **재정렬 실패해도 검색 자체는 절대 실패 안 함.**
|
||||
- **위험 관리**:
|
||||
- 기본 OFF — `g1nation.semanticRerankEnabled` 명시 on 필요
|
||||
- 짧은 timeout (기본 8초) — 초과 시 원순서
|
||||
- 별도 빠른 모델 지정 가능 (`g1nation.semanticRerankModel`, 예: gemma2:2b) — 메인 모델 latency 보호
|
||||
- **시점**: `memoryContext.ts` 에서 retrieve() 결과의 selectedChunks 를 비동기 재정렬 후 context string 빌드
|
||||
- **효과**: LLM attention 은 컨텍스트 상단에 더 강함 → 의도 부합 chunk 가 상단 → 답변 품질↑
|
||||
|
||||
---
|
||||
|
||||
**신규 설정 5개:**
|
||||
- `g1nation.hierarchicalReweightEnabled` (boolean, 기본 true)
|
||||
- `g1nation.semanticRerankEnabled` (boolean, **기본 false** — opt-in)
|
||||
- `g1nation.semanticRerankModel` (string, 비면 defaultModel)
|
||||
- `g1nation.semanticRerankCandidateK` (number 2~30, 기본 15)
|
||||
- `g1nation.semanticRerankTimeoutSec` (number 1~60, 기본 8)
|
||||
|
||||
---
|
||||
|
||||
**감사 결과 — 스킵한 제안 항목:**
|
||||
- #1 Graph-based Retrieval → 스킵 (마크다운 wiki brain 에 NER+그래프 인프라 비용 큼, 가치 불확실)
|
||||
- #3 Re-ranking 항목들 → 이미 v2.2.183~185 에 완료:
|
||||
- Recency 가중 = scoring.ts recencyBoost
|
||||
- Actionability = actionabilityScoring.ts (v2.2.185)
|
||||
- Conflict Check = conflictBlock.ts + scoring.ts conflictMultiplier (v2.2.183)
|
||||
|
||||
**신규 패키징:** `astra-2.2.187.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.186 (2026-05-29)
|
||||
### 🧠 메모리 라이프사이클 — Temporal Markers + Distillation Loop
|
||||
사용자 제안 "정보의 계층화 + 생성 주기 관리" 중 *실제로 빈 곳* 2개에 집중. 제안의 절반은 이미 5-layer 로 구현돼 있어 *그 위에 라이프사이클 관리만 추가*.
|
||||
|
||||
**Part 1 — Temporal Markers (만료일)**
|
||||
- `LongTermEntry` + `EpisodicEntry` 에 `expiresAt?: number` (epoch ms) 추가.
|
||||
- 검색·context build 시 `expiresAt < now` 인 entry **자동 제외** — 만료 plan/decisions 가 stale 답변에 끼어드는 것 방지.
|
||||
- `LongTermMemory.setExpiration(idOrPrefix, expiresAt)` API — 슬래시 명령에서 호출.
|
||||
- 예: "Q3 2026 마케팅 계획" entry 에 `expiresAt = 2026-09-30` 설정 → 10월부터 검색 결과에서 자동 사라짐.
|
||||
|
||||
**Part 2 — Distillation Loop (stale → digest)**
|
||||
- 신규 모듈: `src/memory/distillation.ts`
|
||||
- 30일+ 지난 episodes → LongTerm `'episode-digest'` 카테고리로 승급. 원본은 `promoted=true` 마킹되어 검색에서 제외 (중복 노출 방지).
|
||||
- 신규 LongTermCategory: `'episode-digest'` 추가.
|
||||
- **LLM-less 설계 (v1)**: 기존 EpisodicEntry 의 title/summary/keyDecisions/topics 를 구조적으로 결합 — 비용 0, 결정적, 재현 가능.
|
||||
- **자동 트리거**: 세션 종료 시 (마지막 실행 후 N일 경과 시만, 기본 7일 간격).
|
||||
- **수동 트리거**: `/memory distill [age_days]`
|
||||
- **Archive 모드 2종**:
|
||||
- `'mark-promoted'` (기본): 플래그만, 파일 보존
|
||||
- `'archive-file'`: 파일을 `memory/episodes/archive/` 로 이동
|
||||
|
||||
**신규 슬래시 명령 `/memory`**
|
||||
- `/memory` — 현재 상태 (LongTerm/Episodic 카운트, 카테고리 분포, distillation 마지막 실행, 30일 내 만료 임박)
|
||||
- `/memory distill [age_days]` — 수동 distillation (기본 30일 임계)
|
||||
- `/memory expire <id-prefix> <YYYY-MM-DD>` — LongTerm entry 에 만료일 설정
|
||||
- `/memory list-expiring [days]` — N일 내 만료 entry 목록 (기본 30)
|
||||
- `/memory list-promoted` — 승급된 episodes (digest 형태로 LongTerm 에 살아 있음)
|
||||
- `/memory help` — 명령어 가이드
|
||||
|
||||
**신규 설정 4개:**
|
||||
- `g1nation.distillationEnabled` (boolean, 기본 true)
|
||||
- `g1nation.distillationAgeThresholdDays` (number 1~365, 기본 30)
|
||||
- `g1nation.distillationIntervalDays` (number 1~90, 기본 7)
|
||||
- `g1nation.distillationArchiveMode` (mark-promoted/archive-file, 기본 mark-promoted)
|
||||
|
||||
**감사 결과 (스킵한 제안 항목):**
|
||||
- L1 Working Memory → 이미 `ShortTermMemory.ts` 로 완료
|
||||
- L2 Episodic → 이미 `EpisodicMemory.ts` 로 완료
|
||||
- L3 unification (3 silo → 1 통합) → 스킵 (refactor 비용 > 가치, 작동 중)
|
||||
- 자유 해시태그 시스템 → 스킵 (기존 카테고리 enum 으로 80% 커버)
|
||||
- Conflict reconciliation 닫는 루프 → 추후 (UI 작업 + supersedeance 인프라 큼)
|
||||
|
||||
**신규 패키징:** `astra-2.2.186.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.185 (2026-05-29)
|
||||
### 🎯 추론 품질 #1 — Actionability Scoring (맥락 가중치, 마무리)
|
||||
사용자 피드백: "*단순히 키워드가 겹치는 정보가 아니라, 현재 작업의 상태와 직접적으로 연결된 지식에 더 높은 가중치를 부여*". 추론 품질 3단 시리즈의 마지막. #2 (Conflict Surface) + #3 (CoVe) 완성 위에 *검색 자체* 를 재가중하는 가장 깊은 변경.
|
||||
|
||||
**v1 신호 (사용자 선택):**
|
||||
1. **최근 슬래시 명령** — `slashRouter` 에 in-memory ring buffer 추가. 최근 10개 (중복 제거, 최신 우선). 명령 이름 (`/runway` → `runway`) 이 chunk 내용에 단어 경계 매치 시 boost.
|
||||
2. **열린 파일 경로** — VS Code 활성 에디터에서 캡처.
|
||||
- **Exact filePath 매치**: 가장 강한 boost (× 1.5)
|
||||
- **파일명 매치**: `weights.openFileNameMatch` (기본 0.40)
|
||||
- **부모 디렉터리 매치**: `weights.openFileParentDirMatch` (기본 0.20, 'src'/'lib' 제외)
|
||||
- **확장자 의미 매치**: `weights.openFileExtMatch` (기본 0.10, e.g. `.ts` → 'typescript' 단어)
|
||||
|
||||
**점수 결합 (보수적):**
|
||||
- `finalScore = baseScore × (1 + actionabilityScore × combinedWeight)`
|
||||
- `combinedWeight` 기본 0.30 → actionability=1.0 인 chunk 는 30% boost
|
||||
- TF-IDF 가 여전히 dominant — actionability 는 *동점 깨기* 역할
|
||||
|
||||
**신규 모듈: `src/retrieval/actionabilityScoring.ts`**
|
||||
- `captureWorkStateSignals(recentCmds)` — 신호 수집
|
||||
- `computeActionabilityScore(chunk, signals, weights)` — 0~1.0
|
||||
- `applyActionabilityBoost(chunks, signals, weights)` — 배열 in-place 재가중
|
||||
|
||||
**Wiring:**
|
||||
- `slashRouter.ts` — 명령 실행 직전에 `pushRecentSlashCommand(head)` 호출, ring buffer 갱신
|
||||
- `getRecentSlashCommands()` export — memoryContext 가 읽음
|
||||
- `RetrievalOrchestrator.retrieve()` — `normalizeScores` 직후, `selectWithinBudget` 직전에 actionability 적용 (어떤 chunk 가 token 예산을 살아남는지에 영향)
|
||||
- `RetrievalOptions.workStateSignals` 신규 필드 — undefined 면 legacy
|
||||
- `fusionLog` 에 boosted 개수 + 시그널 요약 기록 (디버그 footer 노출 가능)
|
||||
|
||||
**신규 설정 1개:**
|
||||
- `g1nation.actionabilityEnabled` (boolean, 기본 true) — 마스터 스위치
|
||||
|
||||
**추론 품질 3단 시리즈 완성:**
|
||||
| # | 로직 | 효과 | 위치 |
|
||||
|---|---|---|---|
|
||||
| **#2** | Conflict Surface | 충돌 출처 데이터 LLM 에 노출 | `conflictBlock.ts` |
|
||||
| **#3** | CoVe | 답변 직전 자기 검증 체크리스트 | `coveBlock.ts` |
|
||||
| **#1** | Actionability | 작업 상태 신호로 검색 재가중 | `actionabilityScoring.ts` |
|
||||
|
||||
**시너지:** Actionability 가 *어떤 출처가 답변에 포함되나* 를 개선 → CoVe 가 *그 출처로 어떻게 그라운딩하나* 를 지시 → Conflict Surface 가 *충돌 출처를 어떻게 처리하나* 를 명시. 검색 → 추론 → 응답 3단계 모두 verification 적용.
|
||||
|
||||
**향후 #1 v2 후보 (사용자 미선택, 추후 옵션):**
|
||||
- 최근 7일 Chronicle ADR / decisions 매치 boost
|
||||
- 최근 24시간 customers/hire/runway 이벤트 매치 boost
|
||||
|
||||
**신규 패키징:** `astra-2.2.185.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.184 (2026-05-29)
|
||||
### 🧭 추론 품질 #3 — Chain-of-Verification (CoVe)
|
||||
사용자 피드백: "*추론 결과가 나오기 직전, 이 결론이 확보된 지식(제2뇌)에만 근거하고 있는가? 를 스스로 질문하고 검증하는 로직*" — 할루시네이션 방지 + 그라운딩 명확화.
|
||||
|
||||
**설계 결정 — Instructional CoVe (v1):**
|
||||
원논문 CoVe 는 2-pass (draft → verify → revise). 그러나 ASTRA local-first 라 추가 LLM 호출 비용 큼 + 같은 모델 self-verify 효과 제한적. → v1 은 *instructional* — 시스템 프롬프트에 명시적 검증 체크리스트 주입해 모델이 한 패스 안에서 "답변 작성 전" 그라운딩 점검을 *내재화* 하도록. 향후 strict 모드 확장 가능 (knob 준비).
|
||||
|
||||
**신규 모듈: `src/retrieval/coveBlock.ts`**
|
||||
- `buildCoveChecklistBlock(chunks, userPrompt, options)` — [VERIFICATION CHECKLIST] 시스템 프롬프트 블록 생성.
|
||||
- **체크리스트 구조:**
|
||||
1. **근거 매핑 (Grounding Inventory)** — 상위 N 출처를 [S1]..[SN] 으로 라벨, 각 출처의 score + conflictSeverity + 발췌문.
|
||||
2. **답변 직전 자기 질문 (Pre-Output Self-Check)** — 4개 질문 (그라운딩, 단정 톤 적정성, 구체적 액션 제시, [CONFLICT WARNINGS] 와 통합).
|
||||
3. **STRICT 모드 (옵션)** — 모든 사실 주장 뒤 `[S1]` 형식 inline 인용 강제 (기본 off).
|
||||
|
||||
**Wiring:**
|
||||
- `memoryContext.ts` — 검색 직후 CoVe 블록 계산 → `turnCtx.coveChecklist` 저장
|
||||
- `agent.ts` — turn reset 시 비움, system prompt build 시 전달
|
||||
- `buildAstraModeSystemPrompt.ts` — `[CONFLICT WARNINGS]` 다음, `[CONTEXT]` *밖* 에 주입 (token-truncation 보호)
|
||||
- Casual conversation 모드 비활성
|
||||
|
||||
**신규 설정 3개:**
|
||||
- `g1nation.coveEnabled` (boolean, 기본 true)
|
||||
- `g1nation.coveTopSourcesCount` (number 1~15, 기본 5)
|
||||
- `g1nation.coveStrictMode` (boolean, 기본 false) — inline citation 강제
|
||||
|
||||
**Conflict Surface 와의 시너지:**
|
||||
- 둘이 보완: Conflict = "충돌 데이터 제공", CoVe = "그 데이터 어떻게 verify 할지 지시"
|
||||
- 함께 켜면 (기본): 모델이 충돌 출처를 식별 → 양측 명시 → 출처 ID 표기 → 사용자 판단에 위임 (3-step 그라운딩)
|
||||
|
||||
**남은 작업 (#1):**
|
||||
- **Actionability Scoring** — 현재 작업 상태(active slash command / 열린 파일 / 최근 ADR) 신호로 검색 재랭킹. 가장 큰 신규 설계.
|
||||
|
||||
**신규 패키징:** `astra-2.2.184.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.183 (2026-05-29)
|
||||
### 🔀 추론 품질 #2 — Conflict Surface (모순 검증 로직 1단계)
|
||||
ASTRA 의 `scoring.ts` 가 검색된 문서별로 `conflictSeverity` (NONE/LOW/MEDIUM/HIGH) 를 *이미 계산*하고 있었음 — 그런데 LLM 에게 *어느 출처가 충돌인지* 전달하지 않아 v4 정책의 "[CONFLICT WARNING] 플래그" 가 무용한 상태. 이번 빌드가 그 갭을 메움.
|
||||
|
||||
**신규 모듈: `src/retrieval/conflictBlock.ts`**
|
||||
- `buildConflictWarningsBlock(chunks, options)` — [CONFLICT WARNINGS] 시스템 프롬프트 블록 생성.
|
||||
- 두 종류 충돌 모두 surface:
|
||||
1. **자기-신호 (self-flag)** — 출처 내부에서 충돌 키워드(반대/논란/vs 등) 감지된 경우
|
||||
2. **교차-문서 발산 (cross-divergence)** — 같은 주제 ≥2 chunks 의 본문 Jaccard < 0.30 인 잠재 모순 쌍
|
||||
|
||||
**Wiring:**
|
||||
- `memoryContext.ts` — `result.selectedChunks` 가 retrieval 완료 직후 conflict 블록 계산 → `turnCtx.conflictWarnings` 에 저장
|
||||
- `agent.ts` — turn 시작 시 reset, system prompt build 시 인자로 전달
|
||||
- `buildAstraModeSystemPrompt.ts` — `[CONTEXT]` *밖* 에 주입 (token-truncation 보호)
|
||||
- Casual conversation 모드는 RAG 컨텍스트 자체를 안 쓰므로 충돌 경고도 생략
|
||||
|
||||
**프롬프트 지침 (LLM 에 명시):**
|
||||
1. 답변에 위 출처 중 하나라도 사용한다면, 충돌 가능성을 명시
|
||||
2. 어느 쪽이 옳다고 단정하지 말고, 사용자가 판단할 수 있도록 근거 분리 제시
|
||||
3. 충돌이 답변과 무관하면 무시 가능 — 다만 무관 판단 자체도 한 줄로 기록
|
||||
|
||||
**신규 설정 3개:**
|
||||
- `g1nation.conflictHighlightingEnabled` (boolean, 기본 true) — 마스터 스위치
|
||||
- `g1nation.conflictSeverityThreshold` (low/medium/high, 기본 medium) — 자기-신호 최소 severity
|
||||
- `g1nation.conflictCrossDocEnabled` (boolean, 기본 true) — 교차-문서 발산 감지 활성
|
||||
|
||||
**다음 (#3, #1 예정):**
|
||||
- **#3 CoVe (Chain-of-Verification)** — 결론 직전 자기 검증 루프 (할루시네이션 방지)
|
||||
- **#1 Actionability Scoring** — 검색 결과 재랭킹 시 현재 작업 상태 가중치 반영 (가장 큰 신규 설계)
|
||||
|
||||
**신규 패키징:** `astra-2.2.183.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.182 (2026-05-29)
|
||||
### 🌙 4인 팀 운영 10단계 — /evening 하루 마무리 카드
|
||||
`/morning` 의 짝. 하루 시작과 끝을 한 명령씩으로 단조. *오늘 무엇이 진척됐는지* 보고 → *내일 무엇을 준비할지* 확인 → *짧은 회고 한 줄* 로 마무리.
|
||||
|
||||
**신규 `/evening` 명령:**
|
||||
- 단일 형태 — 인자 없음. 매일 저녁 한 번 실행.
|
||||
|
||||
**섹션 구성:**
|
||||
|
||||
**1. ✅ 오늘의 진척**
|
||||
- **작업 완료** — 오늘 00:00 이후 completed 된 Google Tasks, 멤버별 그룹.
|
||||
- **📒 고객 이벤트** — `.astra/customers.jsonl` 오늘 append (add/renew/risk/churn/note).
|
||||
- **🎯 채용 이벤트** — `.astra/hire.jsonl` 오늘 append (stage 이동, offer, hire 등).
|
||||
- **💰 재무 기록** — `.astra/runway.jsonl` 오늘 append (잔고/지출/수입).
|
||||
|
||||
**2. 🌅 내일 준비**
|
||||
- 내일 마감 작업 (멤버별).
|
||||
- 7일 내 갱신 — D-3 🔴 / D-7 🟡.
|
||||
- 정체 후보 (7일+ 미변동) top 3.
|
||||
|
||||
**3. 🧭 회고 한 줄**
|
||||
- 8개 회고 질문 중 *날짜 기반 결정적 선택* — 같은 날 재실행해도 동일 질문.
|
||||
- 예: "오늘 가장 중요한 한 가지는 무엇이었나? 의도한 대로 됐나?"
|
||||
- 답을 어디 적으면 좋은지 안내 (`/decisions` · `/feedback` · `/customers note` · `/hire note`).
|
||||
|
||||
**Tasks OAuth 실패 시:**
|
||||
- "기록된 진척 없음" + Tasks 에러 메시지 별도 표시 — 다른 섹션은 정상 표시.
|
||||
|
||||
**4인 팀 운영 누적 (1~10단계 — 일과 사이클 완성):**
|
||||
`/decisions` · `/onesie` · `/draft` · `/feedback` · `/blocked` · `/standup` · `/runway` · `/customers` · `/hire` · `/morning` · **`/evening`** ⭐
|
||||
|
||||
**일과 ritual:**
|
||||
- 아침: `/morning` (긴급 + 액션 5)
|
||||
- 낮: `/blocked` `/onesie @멤버` (필요 시)
|
||||
- 저녁: `/evening` (진척 + 내일 + 회고)
|
||||
|
||||
**신규 패키징:** `astra-2.2.182.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.181 (2026-05-29)
|
||||
### ☀️ 4인 팀 운영 9단계 — /morning 매일 아침 통합 대시보드
|
||||
1~8단계로 만든 모든 트래커(runway / customers / hire / tasks / decisions) 의 핵심을 *한 명령*으로. CEO 의 아침 ritual — 6개 명령 안 치고 `/morning` 하나.
|
||||
|
||||
**신규 `/morning` 명령:**
|
||||
- `/morning` — 전체 대시보드 (긴급 + 재무 + 고객 + 팀 + 채용 + 오늘의 액션)
|
||||
- `/morning brief` — 짧은 버전 (긴급 + 액션 3개만, 30초 스캔용)
|
||||
|
||||
**섹션 구성:**
|
||||
1. **🚨 긴급** — 런웨이 < 6개월, 위험 고객, 7일 내 갱신, 지연 작업, 정체 후보 — 자동 집계
|
||||
2. **💰 재무** — 현금 / burn / 런웨이 신호등
|
||||
3. **📒 고객** — 총 MRR + 활성/위험/이탈 + 7일 내 갱신 top 5
|
||||
4. **👥 팀** — 지연/이번 주 작업 카운트 + 멤버별 지연 top 4
|
||||
5. **🎯 채용** — 단계별 진행 한 줄 요약 + 정체 후보 top 3
|
||||
6. **📋 오늘의 액션** — 위 데이터에서 자동 도출 top 5 (휴리스틱 우선순위)
|
||||
|
||||
**액션 도출 휴리스틱:**
|
||||
- 우선순위: 런웨이 위험 > 위험 고객(MRR 큰 순) > 갱신 임박 > 지연 작업 > 정체 후보 > inbox 누적
|
||||
- 각 액션은 *명시적 다음 행동* 포함 ("투자자 미팅", "갱신 의사 확인", "스크리닝 시간 확보" 등)
|
||||
|
||||
**데이터 부족 시 우아한 처리:**
|
||||
- 각 섹션이 독립적으로 "데이터 없음 — `/runway cash` 로 시작" 안내.
|
||||
- Tasks OAuth 실패해도 나머지 섹션 계속 표시.
|
||||
|
||||
**4인 팀 운영 누적 (1~9단계):**
|
||||
`/decisions` · `/onesie` · `/draft` · `/feedback` · `/blocked` · `/standup` · `/runway` · `/customers` · `/hire` · **`/morning`** ⭐
|
||||
|
||||
**신규 패키징:** `astra-2.2.181.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.180 (2026-05-29)
|
||||
### 👥 4인 팀 운영 8단계 — /hire 채용 파이프라인 트래커
|
||||
4인 → 5인 이상 확장 시점. 후보자가 노션·스프레드시트·이메일에 흩어지지 않게 한 명령으로 본다. `/customers` 와 동일한 event-sourced 패턴.
|
||||
|
||||
**신규 `/hire` 명령 (서브명령 11개):**
|
||||
- `/hire` — 파이프라인 대시보드 (역할별/단계별 현황, 정체 알림)
|
||||
- `/hire add <이름> <역할>` — inbox 단계 신규 후보
|
||||
- `/hire stage <이름> <새 단계>` — 단계 이동
|
||||
- `/hire note <이름> <텍스트>` — 자유 메모
|
||||
- `/hire offer <이름> <연봉> [입사일]` — 오퍼 발송 (단계=offer)
|
||||
- `/hire hire <이름> [입사일]` — 입사 확정 (단계=hired)
|
||||
- `/hire reject <이름> <사유>` — 거절 (회사 측)
|
||||
- `/hire decline <이름> <사유>` — 후보 사양
|
||||
- `/hire show <이름>` — 상세 + 이력 (부분 매치 OK)
|
||||
- `/hire list [active/closed/단계명/역할]` — 필터 목록
|
||||
- `/hire path` — 파일 위치
|
||||
|
||||
**기본 파이프라인:**
|
||||
inbox → screened → interview → final → offer → accepted → hired
|
||||
(터미널: rejected · declined)
|
||||
|
||||
**대시보드 핵심:**
|
||||
- 진행 중 / 합격 / 종료 카운트
|
||||
- **역할별 진행** (백엔드 N명 · 디자이너 M명) — 채용 균형 시야
|
||||
- **단계별 후보** + 각자 정체 일수 (`⏰ N일 정체` — 7일 이상 미변동 시)
|
||||
- 최근 합격 5명 (입사일 순)
|
||||
|
||||
**저장 — 로컬 only:**
|
||||
- `<workspace>/.astra/hire.jsonl` (append-only 이벤트)
|
||||
- 민감 정보(이름, 연봉, 거절 사유) 외부 안 보냄.
|
||||
|
||||
**4인 팀 운영 누적 (1~8단계):**
|
||||
`/decisions` · `/onesie` · `/draft` · `/feedback` · `/blocked` · `/standup` · `/runway` · `/customers` · **`/hire`** ⭐
|
||||
|
||||
**신규 패키징:** `astra-2.2.180.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.179 (2026-05-28)
|
||||
### 📒 4인 팀 운영 7단계 — /customers 고객사/MRR/갱신/위험 트래커
|
||||
`/runway` 가 *지출 쪽* 시야라면 `/customers` 는 *수입 쪽*. Salesforce 없이도 4인 기업이 알아야 하는 모든 매출 정보 — MRR, 갱신 임박, 위험 고객 — 를 한 명령으로.
|
||||
|
||||
**Event-sourced 설계 (CRM 아님):**
|
||||
- 모든 변경(add/update/renew/risk/churn/note) = append-only 이벤트.
|
||||
- 현재 상태 = 같은 customerId 의 이벤트를 시간순 재생.
|
||||
- 손상·실수 복구 쉬움 — .jsonl 한 줄만 지우면 그 이벤트만 무효화.
|
||||
|
||||
**신규 `/customers` 명령 (서브명령 10개):**
|
||||
- `/customers` — 대시보드 (총 MRR, 활성/위험/이탈, 30일 내 갱신, 위험 MRR 비율)
|
||||
- `/customers add <이름> <MRR> [갱신일] [요금제]` — 신규
|
||||
- `/customers update <이름> mrr=… renewal=… plan=…` — 정보 수정
|
||||
- `/customers renew <이름> <새 갱신일> [새 MRR]` — 갱신 처리 (active 복귀)
|
||||
- `/customers risk <이름> <사유>` — 위험 표시
|
||||
- `/customers churn <이름> <사유>` — 이탈 처리 (MRR 0 으로)
|
||||
- `/customers note <이름> <텍스트>` — 자유 메모
|
||||
- `/customers show <이름>` — 상세 + 이벤트 히스토리 (부분 매치 OK)
|
||||
- `/customers list [active/risk/churned]` — 필터 목록
|
||||
- `/customers path` — 파일 경로
|
||||
|
||||
**대시보드 핵심 지표:**
|
||||
- 총 MRR + 연 환산 (자동)
|
||||
- 활성/위험/이탈 카운트
|
||||
- **위험 MRR 비율** — 전체 MRR 의 몇 %가 위험에 노출됐는지 (이 숫자 보고 우선순위)
|
||||
- **30일 내 갱신** — D-7 🔴 / D-14 🟡 / D-30 🟢 신호등
|
||||
- MRR 순 활성 고객 top 10
|
||||
|
||||
**금액 단위:** `200만` / `1.5억` / `300000` 모두 OK (한국식).
|
||||
**갱신일:** `YYYY-MM-DD`.
|
||||
|
||||
**저장 — 로컬 only:**
|
||||
- `<workspace>/.astra/customers.jsonl` (append-only 이벤트 로그)
|
||||
- 민감 정보(고객사 이름, 매출) 외부 안 보냄.
|
||||
|
||||
**4인 팀 운영 누적 (1~7단계):**
|
||||
`/decisions` · `/onesie` · `/draft` · `/feedback` · `/blocked` · `/standup` · `/runway` · **`/customers`** ⭐
|
||||
|
||||
**신규 패키징:** `astra-2.2.179.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.178 (2026-05-28)
|
||||
### 💰 4인 팀 운영 6단계 — /runway 현금/소진율/런웨이 트래커
|
||||
4인 기업 CEO 가 매일·매주 확인하는 가장 중요한 숫자 — *통장에 얼마, 한 달에 얼마 나감, 몇 개월 버틸 수 있나*. 회계 SaaS 연동 없이 한 명령으로 한 화면.
|
||||
|
||||
**신규 `/runway` 명령 (서브명령 7개):**
|
||||
- `/runway` — 현재 상태 카드 (현금 / burn / 런웨이 + 위험 등급)
|
||||
- `/runway cash <금액> [메모]` — 통장 잔고 스냅샷
|
||||
- `/runway expense <금액> [메모]` — 지출 기록 (자동 burn 계산에 반영)
|
||||
- `/runway revenue <금액> [메모]` — 수입 기록 (burn 상쇄)
|
||||
- `/runway burn <월 금액>` — 월 소진율 수동 설정 (자동 계산보다 우선)
|
||||
- `/runway log [N]` — 최근 N건 (기본 20, 최신순)
|
||||
- `/runway path` — .jsonl 파일 위치
|
||||
|
||||
**금액 파싱 — 한국식 단위 그대로:**
|
||||
- `5000만` / `1.5억` / `300000` / `1,500,000` / `10k` / `5m` 모두 OK.
|
||||
|
||||
**런웨이 위험 등급:**
|
||||
- 🟢 6개월+ (안전)
|
||||
- 🟡 3~6개월 (자금 계획 점검 권장)
|
||||
- 🔴 3개월 미만 (즉시 조달/절감 필요)
|
||||
- ♾️ 흑자 운영 (지출 ≤ 수입)
|
||||
|
||||
**Burn 계산 우선순위:**
|
||||
1. `/runway burn` 으로 수동 설정한 값 (예측 가능, 계절성 무시)
|
||||
2. 최근 30일 실적 (expense − revenue, 30일 미만이면 일평균 × 30 환산)
|
||||
|
||||
**저장 — 로컬 only:**
|
||||
- `<workspace>/.astra/runway.jsonl` (append-only, 사람 편집 가능)
|
||||
- 민감 정보(현금 잔고) 외부 안 보냄 — ASTRA local-first.
|
||||
|
||||
**4인 팀 운영 누적 (1~6단계):**
|
||||
`/decisions` · `/onesie` · `/draft` · `/feedback` · `/blocked` · `/standup` · **`/runway`** ⭐
|
||||
|
||||
**신규 패키징:** `astra-2.2.178.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.177 (2026-05-28)
|
||||
### 📊 4인 팀 운영 5단계 — /standup 공유용 스탠드업 카드
|
||||
`/blocked` 는 *대표가 본다* (분석), `/standup` 은 *팀에 공유한다* (소통). 같은 Tasks 데이터지만 다른 포맷·관점.
|
||||
|
||||
**신규 `/standup [daily/weekly/monthly]` 명령:**
|
||||
- 기본: `weekly` (7일 윈도우). 옵션: `daily` (1일) · `monthly` (30일).
|
||||
- **멤버별 그룹핑 + 3-row 표준 스탠드업 포맷**:
|
||||
- ✅ 완료 (윈도우 내 완료, 최대 8건)
|
||||
- 🎯 진행/예정 (다가오는 마감 + 마감 미정, 최대 6건)
|
||||
- 🚧 블로커 (지연된 항목, 최대 5건)
|
||||
- 멤버 정렬: 활동량(완료+진행+지연×2) 내림차순 — 가장 바쁜 사람 위.
|
||||
- 부록: **이번 기간 결정 (Chronicle ADR)** 목록, 최대 10건.
|
||||
- 출력: 단순 마크다운 — **슬랙·노션 그대로 복붙 가능**.
|
||||
|
||||
**`/blocked` vs `/standup`:**
|
||||
- `/blocked`: 대표 시야 확보용. 멤버별 압박 요약 + `/onesie` 안내. 매일 아침.
|
||||
- `/standup`: 팀 공유용. 표준 standup 3-row. 매일/매주 게시.
|
||||
|
||||
**4인 팀 운영 누적 (1~5단계):**
|
||||
`/decisions` · `/onesie` · `/draft` · `/feedback` · `/blocked` · **`/standup`** ⭐
|
||||
|
||||
**신규 패키징:** `astra-2.2.177.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.176 (2026-05-28)
|
||||
### 🚨 4인 팀 운영 4단계 — /blocked 전사 across 지연·블로커 뷰
|
||||
대표의 *일일 아침 ritual* 도구. `/onesie` 가 멤버 단위라면 `/blocked` 는 전체 across 한 화면 — "오늘 어디 살펴봐야 하나" 즉시 잡힘.
|
||||
|
||||
**신규 `/blocked [@멤버]` 명령:**
|
||||
- Google Tasks 전체 조회 후 owner 자동 추출(제목 `[멤버]` prefix · notes `담당: 이름` 패턴 모두 인식 — `/task` 와 `/meet` 양쪽 호환).
|
||||
- **3개 섹션 자동 분류:**
|
||||
- 🔴 지연 (마감 < 오늘) — 가장 오래 지연된 것부터, 최대 20건
|
||||
- 🟡 이번 주 마감 (오늘~+7일) — 임박 순, 최대 15건
|
||||
- ⚪ 마감일 없음 — owner 별, 우선순위 합의 대상, 최대 10건
|
||||
- **멤버별 압박 요약** (전체 보기일 때) — 지연·이번주 가중치로 정렬 → 압박 큰 멤버부터 `/onesie @<멤버>` 안내.
|
||||
- `@멤버` 인자 시 그 멤버만 (압축된 1인 뷰).
|
||||
|
||||
**사용 예:**
|
||||
- `/blocked` → 매일 아침 전체 점검
|
||||
- `/blocked @기획자` → 기획자만 압축 뷰
|
||||
|
||||
**4인 팀 운영 누적 (1~4단계 완성):**
|
||||
- `/decisions` · `/onesie` · `/draft` · `/feedback` · **`/blocked`** ⭐
|
||||
|
||||
**신규 패키징:** `astra-2.2.176.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.175 (2026-05-28)
|
||||
### 📥 4인 팀 운영 3단계 — /feedback 고객 신호 누적 + 패턴 분석
|
||||
3단계: 슬랙·이메일·CS 채널에 흩어진 고객 피드백을 한 곳에 모으고 LLM이 자동 분류·패턴 분석. **ASTRA local-first 의 강점이 가장 살아나는 영역** — 민감 정보도 외부 전송 없이 로컬 처리.
|
||||
|
||||
**신규 모듈 [feedbackStore.ts](src/features/feedback/feedbackStore.ts):**
|
||||
- 저장 위치: `<workspace>/.astra/customer-feedback.jsonl` (한 줄 = 한 항목, append-only, 사람이 직접 편집 가능).
|
||||
- `FeedbackEntry` 타입: id / timestamp / text / source / categories / sentiment.
|
||||
|
||||
**신규 `/feedback` 4-mode 명령:**
|
||||
- `/feedback <텍스트>` — 저장 + **LLM 자동 카테고리 분류** (1~3개 카테고리 + sentiment positive/neutral/negative). LLM 분류 실패해도 원본은 보존.
|
||||
- `/feedback list [카테고리]` — 누적 목록 (최신순, 최대 20건). 카테고리 필터 가능.
|
||||
- `/feedback summary` — 3건 이상 누적 시 LLM 패턴 분석 리포트 (카테고리 분포 + 감정 분포 + 반복 패턴 Top 3 + 추천 액션).
|
||||
- `/feedback path` — 저장 파일 경로 표시.
|
||||
|
||||
**사용 예:**
|
||||
- `/feedback 결제 흐름이 너무 복잡해서 중간에 포기했어요 - 김XX`
|
||||
- `/feedback list 결제` → 결제 카테고리 누적 피드백만
|
||||
- `/feedback summary` → 전체 패턴 리포트
|
||||
|
||||
**다음 단계 후보** (별도): `/blocked` 전사 블로커 뷰, /feedback 자동 주간 리포트 (매주 월요일).
|
||||
|
||||
**신규 패키징:** `astra-2.2.175.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.174 (2026-05-27)
|
||||
### 🏢 4인 팀 운영 2단계 — /onesie 1:1 카드 + /draft 외부 커뮤니케이션 초안
|
||||
2단계 묶음 (1단계는 v2.2.173 `@담당자` 태그 + `/decisions`). 단독 운영자(대표)의 주간 ritual 효율화.
|
||||
|
||||
**신규 `/onesie [멤버]` — 1:1 미팅 준비 카드:**
|
||||
- Google Tasks API 전체 조회 후 멤버 필터 (제목 `[멤버]` prefix · notes `@멤버` · notes `담당: 멤버` 패턴 모두 인식).
|
||||
- 자동 카테고리: **최근 30일 완료 / 지연 / 진행 중·다가오는 / 마감일 없음**.
|
||||
- Chronicle ADR 에서 해당 멤버 언급된 결정 최신 5건 동시 표시.
|
||||
- **자동 대화 토픽 제안** — 상태 신호(지연 多 → 블로커, 완료 0 → 시간 사용 확인, 마감일 없음 多 → 우선순위 합의)에서 유도.
|
||||
- 사용 예: `/onesie 기획자` · `/onesie @디자이너` · `/onesie 개발`.
|
||||
|
||||
**신규 `/draft [유형] [요청]` — 외부 커뮤니케이션 초안:**
|
||||
- 6개 유형: `email` · `slack` · `blog` · `newsletter` · `investor-update` · `proposal`.
|
||||
- 각 유형별 분량/구조/톤 가이드 자동 적용.
|
||||
- 신규 설정 **`g1nation.teamVoiceGuide`** (multilineText) — 팀 보이스 가이드를 한 번 저장하면 모든 초안에 자동 반영.
|
||||
- 사용 예: `/draft email 김 대표님께 미팅 일정 조율 요청, 다음 주 가능 시간 3개 제안`
|
||||
|
||||
**토대 — `listTasks` Google Tasks API 조회:**
|
||||
- [tasksApi.ts](src/features/calendar/tasksApi.ts): `listTasks(context, {showCompleted, maxResults})` — 신규 `ListedTask` 타입 (id/title/status/due/completed/notes).
|
||||
- /onesie 외 향후 `/blocked`, 진행률 리포트 등에 재활용 가능.
|
||||
|
||||
**신규 패키징:** `astra-2.2.174.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.173 (2026-05-27)
|
||||
### 🏢 4인 팀 운영 — @담당자 태그 + /decisions 결정 검색
|
||||
4인 단독 운영 모드(대표가 모든 데이터 입력 + ASTRA가 조회·합성·재활용 담당)의 두 가지 토대 기능.
|
||||
|
||||
**`/task` 에 `@담당자` 태그 추가:**
|
||||
- 첫 토큰이 `@` 로 시작하면 owner 로 파싱 → 제목 앞에 `[담당자]` prefix 자동 추가.
|
||||
- 사용 예: `/task @기획자 스펙 1차 정리 26/06/01 26/06/05` → Tasks/Calendar 양쪽에 `[기획자] 스펙 1차 정리` 로 표시.
|
||||
- 노트에도 `담당: @기획자` 한 줄 자동 기록 → `/decisions` 가 grep 으로 찾을 수 있음.
|
||||
|
||||
**신규 `/decisions [키워드] [@담당자]` — Chronicle ADR 검색:**
|
||||
- 이미 chronicle 이 쌓고 있는 결정 기록(`<recordRoot>/decisions/ADR-NNNN-*.md`)을 키워드 + 담당자로 검색.
|
||||
- 모든 등록된 chronicle 프로젝트 일괄 스캔, 최신순, 최대 20건.
|
||||
- 출력: 제목 + 날짜 + 프로젝트 + 키워드 주변 스니펫 + 파일 경로.
|
||||
- 사용 예: `/decisions 환불 정책` · `/decisions @기획자` · `/decisions 결제 @개발`
|
||||
- 인자 없이 실행 시 도움말 표시.
|
||||
|
||||
**다음 단계 후보** (별도 작업): `/onesie [멤버]` — 1:1 미팅 준비 카드, `/draft [유형]` — 외부 커뮤니케이션 초안.
|
||||
|
||||
**신규 패키징:** `astra-2.2.173.vsix`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## v2.2.172 (2026-05-27)
|
||||
### 🎛️ /meet — 기본값을 Tasks 단독 등록으로 변경 (중복 방지)
|
||||
- **`g1nation.meetUsesCalendar` 기본값 `true` → `false`.** Tasks 도 Calendar 사이드바에 같이 보이므로 둘 다 켜져 있으면 동일 항목이 중복 노출(일정 + 할 일 각 1건씩)되던 문제 해결.
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"corePurpose": "",
|
||||
"detailLevel": "standard",
|
||||
"createdAt": "2026-05-20T09:42:40.003Z",
|
||||
"updatedAt": "2026-05-26T08:10:03.968Z"
|
||||
"updatedAt": "2026-05-29T06:58:44.615Z"
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# ADR: 질문이 있어, 내가 당근이라는 중고 거래 사이트에서 8TB HDD를 구매했어. 안전거래 (Escrow)를 사용했어. 일단 물건을 어제 받았고, ...
|
||||
|
||||
## Status
|
||||
accepted
|
||||
|
||||
## Context
|
||||
질문이 있어, 내가 당근이라는 중고 거래 사이트에서 8TB HDD를 구매했어. 안전거래 (Escrow)를 사용했어. 일단 물건을 어제 받았고, 어제 설치했는데 하드가 사망한것 같아. 인지가 안되더라고. 그래서 일단 당근 마켓에 분쟁 신청을 했고, 판매자에게 채팅 메시지를 보냈어. 당근 내용을 보니 담당자가 나와 판매자간의 대화 내용을 데이터로 사용하기 위해 모니터링 한다고 하더라고. 그러곤 오늘 판매자가 통화 요청을 했어. 일단 통화는 어렵고, 당근 채팅 창에서만 대화를 해야 기록에 남을 것 같다고 함. 그러곤 당근에서 연락옴. 일단 배송업체에서 어떤 답변을 받는지 기다리고 답변을 받으면 다시 논의 하자고 답변을 줌. 내가 뭘해야해?
|
||||
|
||||
## Decision
|
||||
결론: 현재 상황에서 가장 중요한 것은 '추가적인 증거 확보'와 '당근 마켓 가이드라인에 따른 기록 유지'이며, 판매자에게는 '물건의 결함'을 입증할 객관적 자료를 제시해야 합니다. 지금 사용자가 처한 상황은 '물품 하자(사망)'로 인한 분쟁 중이며, 당근 측에서 모니터으로를 통해 대화 내용을 검토하고 있으므로, 감정적인 대응보다는 객상적인 데이터로 승부해야 하는 시점입니다. 다음의 단계별 행동 지침을 따르시기 바랍니다. 1. 객관적 결함 증거 확보 (가장 중요) 판매자가 '통화'를 요청했다는 것은 채팅 기록 외에 구두로 상황을 무마하거나, 사용자의 논리를 흐리려는 의도가 있을 수 있습니다. 따라서 반드시 '시각적/기술적 증거'를 먼저 만드세요. - 사진/영상 촬영: 하드가 컴퓨터에 연결된 상태에서 '인식되지 않는 화면(디스크 관리 창)'을 사진이나 영상으로 찍으세요. 8TB 용량이 표시되지 않거나, '알 수 없는 영역'으로 뜨는 것을 보여줘야 합니다. - S.M.A.T/로그 확인:...
|
||||
|
||||
## Reason
|
||||
Captured automatically because the conversation contained decision-oriented language.
|
||||
|
||||
## Alternatives
|
||||
Not captured yet.
|
||||
|
||||
## Consequences
|
||||
- Future prompts should treat this as project context unless the user changes direction.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# ADR: connectAI 프로젝트의 아스트라 대부분 답을 잘 줘. 근대 간혹 오타가 발생하거나 상황에 맞지 않는 단어를 간혹 사용해서 혼선을 줄때가 있...
|
||||
|
||||
## Status
|
||||
accepted
|
||||
|
||||
## Context
|
||||
connectAI 프로젝트의 아스트라 대부분 답을 잘 줘. 근대 간혹 오타가 발생하거나 상황에 맞지 않는 단어를 간혹 사용해서 혼선을 줄때가 있거든. 이 부분을 개선하려면 어떠한 부분을 개선하는게 좋을까?
|
||||
|
||||
## Decision
|
||||
결론: 아스트라의 답변에서 발생하는 오타나 부적절한 단어 선택(Hallucination/Typo)을 개선하기 위해서는, 모델 자체의 성능에 의존하기보다 '출력 전 검증 로직(Post-processing)'과 '프롬프트 내 제약 조건(Constraint)의 정밀화'를 강화해야 합니다. 현재 아스트라는 지식(Second Brain)을 활용하는 능력은 뛰어나지만, 생성된 텍스트의 '품질 관리(QA)' 단계가 부족한 상태입니다. 이를 해결하기 위한 3가지 기술적 개선 방안을 제안합니다. 1. 출력 검증 레이어 도입 (Self-Correction Loop) 모델이 답변을 생성한 직후, 사용자에게 전달하기 전에 스스로 오류를 찾아내는 단계를 추가해야 합니다. - 강화 방안: `[Post-generation Check]` 단계를 시스템 프롬프트에 명시합니다. - "작성된 답변에 프로젝트의 핵심 용어(예: `connectai`, `p-rein동`)가 올바른 철자로 작성되었는지 확인하라." - "문...
|
||||
|
||||
## Reason
|
||||
Captured automatically because the conversation contained decision-oriented language.
|
||||
|
||||
## Alternatives
|
||||
Not captured yet.
|
||||
|
||||
## Consequences
|
||||
- Future prompts should treat this as project context unless the user changes direction.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# ADR: 질문이 있어, 내가 당근이라는 중고 거래 사이트에서 8TB HDD를 구매했어. 안전거래 (Escrow)를 사용했어. 일단 물건을 어제 받았고, ...
|
||||
|
||||
## Status
|
||||
accepted
|
||||
|
||||
## Context
|
||||
질문이 있어, 내가 당근이라는 중고 거래 사이트에서 8TB HDD를 구매했어. 안전거래 (Escrow)를 사용했어. 일단 물건을 어제 받았고, 어제 설치했는데 하드가 사망한것 같아. 인지가 안되더라고. 그래서 일단 당근 마켓에 분쟁 신청을 했고, 판매자에게 채팅 메시지를 보냈어. 당근 내용을 보니 담당자가 나와 판매자간의 대화 내용을 데이터로 사용하기 위해 모니터링 한다고 하더라고. 그러곤 오늘 판매자가 통화 요청을 했어. 일단 통화는 어렵고, 당근 채팅 창에서만 대화를 해야 기록에 남을 것 같다고 함. 그러곤 당근에서 연락옴. 일단 배송업체에서 어떤 답변을 받는지 기다리고 답변을 받으면 다시 논의 하자고 답변을 줌. 내가 뭘해야해?
|
||||
|
||||
## Decision
|
||||
결론: 현재 상황에서 가장 중요한 것은 '추가적인 증거 확보'와 '당근 마켓 가이드라인에 따른 기록 유지'이며, 판매자에게는 '물건의 결함'을 입증할 객관적 자료를 제시해야 합니다. 지금 사용자가 처한 상황은 '물품 하자(사망)'로 인한 분쟁 중이며, 당근 측에서 모니터링을 통해 대화 내용을 검토하고 있으므로, 감정적인 대응보다는 객관적인 데이터로 승부해야 하는 시점입니다. 다음의 단계별 행동 지침을 따르시기 바랍니다. 1. 객관적 결함 증거 확보 (가장 중요) 판매자가 '통화'를 요청했다는 것은 채팅 기록 외에 구두로 상황을 무마하거나, 사용자의 논리를 흐리려는 의도가 있을 수 있습니다. 따라서 반드시 '시각적/기술적 증거'를 먼저 만드세요. - 사진/영상 촬영: 하드가 컴퓨터에 연결된 상태에서 '인식되지 않는 화면(디스크 관리 창)'을 사진이나 영상으로 찍으세요. 8TB 용량이 표시되지 않거나, '알 수 없는 영역'으로 뜨는 것을 보여줘야 합니다. - S.M.A.T/로그 확인: ...
|
||||
|
||||
## Reason
|
||||
Captured automatically because the conversation contained decision-oriented language.
|
||||
|
||||
## Alternatives
|
||||
Not captured yet.
|
||||
|
||||
## Consequences
|
||||
- Future prompts should treat this as project context unless the user changes direction.
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Development Log: E:\Wiki\connectai 프로젝트에 대해서 이야기 하는거야.
|
||||
|
||||
## Purpose
|
||||
Record the implementation or verification outcome from the current conversation.
|
||||
|
||||
## Implementation Summary
|
||||
결론: `E:\Wiki\connectai` 프로젝트의 맥락에서, 방대한 지식 중 의미 있는 것을 발굴하는 것은 결국 '검색된 정보(Retrieved)와 현재 작업(Active Task) 사이의 논리적 간극을 어떻게 메울 것인가'의 문제입니다. 현재 아스트라는 `connectai` 프로젝트의 구조를 파악하고 있으며, 이 프로젝트의 핵심은 단순한 지식 저장이 아니라 '지능형 에이전트의 실행과 관리'에 있습니다. 따라서 지식을 발굴할 때 다음 세 가지 관점을 적용하면 훨씬 강력한 추론이 가능해집니다. 1. 프로젝트 의존성 기반의 확장 검색 (Dependency-aware Retrieval) - `connectai`는 VS Code Extension이며, `package.json`, `src/agent.ts`, `src/config.ts` 등 상호 연결된 모듈로 구성되어 있습니다. - 지식을 가져올 때 단순히 텍mplate 파일만 보는 것이 아니라, "이 설정(`config.ts`)이 변경...
|
||||
|
||||
## Architecture
|
||||
Captured automatically from the assistant response and active project context.
|
||||
|
||||
## Changed Files
|
||||
- package.json
|
||||
- src/agent.ts
|
||||
- src/config.ts
|
||||
- config.ts
|
||||
- agent-knowledge-map.json
|
||||
- architecture.md
|
||||
- skills/*.md
|
||||
|
||||
## Dependency Notes
|
||||
No new dependency note was captured automatically.
|
||||
|
||||
## Bugs
|
||||
No bugs recorded.
|
||||
|
||||
## Lessons
|
||||
- Automatic project records should be generated in the background when the turn contains durable project knowledge.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Development Log: 아스트라를 지금 보다 더 날카롭고 신뢰감이 높은 아이 그리고 나의 의도를 잘 파악하게 하기 위해서는 어떤부분을 강화하는게 좋을까?
|
||||
|
||||
## Purpose
|
||||
Record the implementation or verification outcome from the current conversation.
|
||||
|
||||
## Implementation Summary
|
||||
결론: 아스트라가 사용자의 의도를 정확히 꿰뚫고(Sharpness) 신뢰할 수 있는 답변(Reliability)을 내놓기 위해서는, '추론 전 단계의 맥락 정밀화(Pre-inference Contextualization)'와 '추론 후 검증 루프(Post-inference Verification)'를 강화해야 합니다. 단순히 모델의 성능에 의존하는 것이 아니라, 아스트라가 스스로 '생각의 구조'를 설계할 수 있는 장치를 만드는 것이 핵심입니다. 이를 위한 3가지 강화 전략을 제안합니다. 1. 사용자 의도 파악의 정밀화: Intent-to-Context Mapping 현재 아스트라는 사용자의 질문을 받으면 즉시 답변을 생성하려 합니다. 하지만 '날카로운' 파악을 위해서는 질문 뒤에 숨겨진 '실행 목적'을 먼저 정의해야 합니다. - 강화 방안: 질문이 들어오면 바로 추론하지 않고, [의도 분류 $\rightarrow$ 필요 컨텍스트 식별 $\rightarrow$ 누락된 정보 확인] 단계...
|
||||
|
||||
## Architecture
|
||||
Captured automatically from the assistant response and active project context.
|
||||
|
||||
## Changed Files
|
||||
- src/agent.ts
|
||||
|
||||
## Dependency Notes
|
||||
No new dependency note was captured automatically.
|
||||
|
||||
## Bugs
|
||||
No bugs recorded.
|
||||
|
||||
## Lessons
|
||||
- Automatic project records should be generated in the background when the turn contains durable project knowledge.
|
||||
@@ -210,3 +210,18 @@
|
||||
|
||||
## 2026-05-26
|
||||
- Auto decision record created: decisions\ADR-0025-e-wiki-connectai-프로젝트에-대한-너의-평가-해줘.md
|
||||
|
||||
## 2026-05-29
|
||||
- Auto development record created: development\2026-05-29_e-wiki-connectai-프로젝트에-대해서-이야기-하는거야_implementation.md
|
||||
|
||||
## 2026-05-29
|
||||
- Auto development record created: development\2026-05-29_아스트라를-지금-보다-더-날카롭고-신뢰감이-높은-아이-그리고-나의-의도를-잘-파악하게-하기-위해서는-어떤부분_implementation.md
|
||||
|
||||
## 2026-05-29
|
||||
- Auto decision record created: decisions\ADR-0026-질문이-있어-내가-당근이라는-중고-거래-사이트에서-8tb-hdd를-구매했어-안전거래-escrow-를-사용했어.md
|
||||
|
||||
## 2026-05-29
|
||||
- Auto decision record created: decisions\ADR-0027-connectai-프로젝트의-아스트라-대부분-답을-잘-줘-근대-간혹-오타가-발생하거나-상황에-맞지-않는-단어.md
|
||||
|
||||
## 2026-05-29
|
||||
- Auto decision record created: decisions\ADR-0028-질문이-있어-내가-당근이라는-중고-거래-사이트에서-8tb-hdd를-구매했어-안전거래-escrow-를-사용했어.md
|
||||
|
||||
Generated
+1
-1
@@ -6,7 +6,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "astra",
|
||||
"version": "2.2.172",
|
||||
"version": "2.2.193",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lmstudio/sdk": "^1.5.0",
|
||||
|
||||
+149
-1
@@ -2,7 +2,7 @@
|
||||
"name": "astra",
|
||||
"displayName": "Astra",
|
||||
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
|
||||
"version": "2.2.172",
|
||||
"version": "2.2.193",
|
||||
"publisher": "g1nation",
|
||||
"license": "MIT",
|
||||
"icon": "assets/icon.png",
|
||||
@@ -252,6 +252,12 @@
|
||||
"default": false,
|
||||
"markdownDescription": "`/meet` 액션 아이템을 **Google Calendar** 일정(all-day)으로도 등록할지 여부. **기본 `false`** — Tasks 단독 등록으로 중복 방지 (Tasks 도 캘린더 사이드바에 같이 보이므로 둘 다 켜면 중복). true 로 켜면 Tasks + Calendar 양쪽 모두 등록."
|
||||
},
|
||||
"g1nation.teamVoiceGuide": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"editPresentation": "multilineText",
|
||||
"markdownDescription": "`/draft` 외부 커뮤니케이션 초안 작성 시 모든 생성에 적용되는 **팀 보이스 가이드**. 말투/금기 표현/자주 쓰는 표현/회사 약어 정의 등을 자유 형식으로. 예: '회사명은 항상 \"Astra\" 로 표기. 존댓말 기본. 이모지는 슬랙에서만. 약어 ASAP/FYI 사용 금지...' 비워두면 가이드 없이 일반 초안 생성."
|
||||
},
|
||||
"g1nation.memoryEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -550,6 +556,148 @@
|
||||
"maximum": 1,
|
||||
"description": "Hybrid score blend: 0 = pure TF-IDF (sparse / keyword), 1 = pure embedding cosine (dense / semantic), 0.5 = balanced. Only used when g1nation.embeddingModel is set. Default 0.5."
|
||||
},
|
||||
"g1nation.conflictHighlightingEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Conflict Surface — 검색된 출처에서 충돌/논란 신호 감지 시 [CONFLICT WARNINGS] 블록을 시스템 프롬프트에 주입. LLM 이 상충되는 관점을 명시하고 사용자 판단에 위임하도록. 기본 켜짐."
|
||||
},
|
||||
"g1nation.conflictSeverityThreshold": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high"],
|
||||
"default": "medium",
|
||||
"description": "Conflict 자기-신호 surface 시 최소 severity 임계. low=가장 민감(노이즈 가능), medium=균형(기본), high=강한 충돌만."
|
||||
},
|
||||
"g1nation.conflictCrossDocEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "교차-문서 발산 감지 — 같은 주제 ≥2 chunks 의 본문 Jaccard < 0.30 인 잠재 모순 쌍을 [CONFLICT WARNINGS] 에 함께 표시. 기본 켜짐."
|
||||
},
|
||||
"g1nation.coveEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Chain-of-Verification (CoVe) — 답변 *작성 전* 그라운딩 체크리스트를 시스템 프롬프트에 주입해 모델이 self-verify 하도록. 할루시네이션 방지 + 출처 명확화. 기본 켜짐."
|
||||
},
|
||||
"g1nation.coveTopSourcesCount": {
|
||||
"type": "number",
|
||||
"default": 5,
|
||||
"minimum": 1,
|
||||
"maximum": 15,
|
||||
"description": "CoVe 체크리스트에 나열할 상위 출처 개수. 너무 많으면 프롬프트 비대, 너무 적으면 그라운딩 부족. 기본 5."
|
||||
},
|
||||
"g1nation.coveStrictMode": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "CoVe Strict 모드 — 모든 사실 주장 뒤에 출처 ID [S1] 형식으로 inline 인용 강제. 답변이 학술적·verbose 해질 수 있어 기본 off."
|
||||
},
|
||||
"g1nation.actionabilityEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Actionability Scoring — '현재 작업 상태' 신호(최근 슬래시 명령 + 열린 파일) 로 검색 결과를 재가중. 지금 작업 중인 컨텍스트와 직접 연결된 문서를 우선. 기본 켜짐."
|
||||
},
|
||||
"g1nation.distillationEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Distillation Loop — stale Episodic Memory 를 LongTerm 'episode-digest' 로 승급해 검색 노이즈 방지. /memory distill 수동 + 세션 종료 시 자동 트리거. 기본 켜짐."
|
||||
},
|
||||
"g1nation.distillationAgeThresholdDays": {
|
||||
"type": "number",
|
||||
"default": 30,
|
||||
"minimum": 1,
|
||||
"maximum": 365,
|
||||
"description": "며칠 이상 지난 episode 를 distill 대상으로 할지. 기본 30일."
|
||||
},
|
||||
"g1nation.distillationIntervalDays": {
|
||||
"type": "number",
|
||||
"default": 7,
|
||||
"minimum": 1,
|
||||
"maximum": 90,
|
||||
"description": "자동 distillation 의 최소 간격 (일). 마지막 실행 후 이 일수가 지나야 재실행. 기본 7일."
|
||||
},
|
||||
"g1nation.distillationArchiveMode": {
|
||||
"type": "string",
|
||||
"enum": ["mark-promoted", "archive-file"],
|
||||
"default": "mark-promoted",
|
||||
"description": "Distillation 후 원본 episode 처리: 'mark-promoted'=플래그만 (파일 보존, 기본), 'archive-file'=memory/episodes/archive/ 로 파일 이동."
|
||||
},
|
||||
"g1nation.hierarchicalReweightEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Hierarchical Context Window — 질의·문서 추상도(concrete/operational/strategic) 매칭으로 검색 결과 재가중. 같은 레벨 +15%, 양 끝 mismatch -30%. LLM 호출 없음, 결정적. 기본 켜짐."
|
||||
},
|
||||
"g1nation.semanticRerankEnabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Semantic Re-ranking — 검색된 selectedChunks 의 순서를 LLM 한 번 호출로 의도-부합도 순 재정렬. 매 turn 1회 추가 LLM 호출 (latency 비용). 기본 OFF — 명시적 on 해야 함."
|
||||
},
|
||||
"g1nation.semanticRerankModel": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Semantic Re-ranking 전용 모델 ID. 비면 defaultModel 사용. 빠른 작은 모델(예: gemma2:2b) 권장 — latency 줄임."
|
||||
},
|
||||
"g1nation.semanticRerankCandidateK": {
|
||||
"type": "number",
|
||||
"default": 15,
|
||||
"minimum": 2,
|
||||
"maximum": 30,
|
||||
"description": "Re-rank 대상 상위 후보 개수. 많을수록 quality↑ latency↑ token↑. 기본 15."
|
||||
},
|
||||
"g1nation.semanticRerankTimeoutSec": {
|
||||
"type": "number",
|
||||
"default": 8,
|
||||
"minimum": 1,
|
||||
"maximum": 60,
|
||||
"description": "Re-rank LLM 호출 타임아웃 (초). 초과 시 원순서 그대로 — 검색 실패 안 됨. 기본 8."
|
||||
},
|
||||
"g1nation.intentClarificationEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Intent Clarification — 모호 질의(환경/대상/범위/포맷/마감 누락) 감지 시 LLM 에게 추측 답변보다 *역질문 우선* 지시. 기본 켜짐."
|
||||
},
|
||||
"g1nation.intentClarificationStrictness": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high"],
|
||||
"default": "medium",
|
||||
"description": "모호 판정 임계. low=가장 덜 묻기(2개+ missing), medium=균형(1개+), high=가장 자주 묻기 (1개+ OR 짧은 질의+trigger)."
|
||||
},
|
||||
"g1nation.citationTraceEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Citation Trace — 답변 끝에 사용된 출처를 *출처:* 한 줄로 정리 지시. CoVe Strict 의 가벼운 형제, 항상 ON 권장. 기본 켜짐."
|
||||
},
|
||||
"g1nation.selfCheckEnabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Post-hoc Self-Check — 답변 완료 후 별도 LLM 호출 1회로 검증 (답변 직접도/그라운딩/논리 모순). 결과를 답변 아래 footer 한 줄로 표시. 매 turn 추가 LLM 호출 (latency 비용). 기본 OFF — 명시적 opt-in."
|
||||
},
|
||||
"g1nation.selfCheckModel": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Self-check 전용 모델 ID. 비면 defaultModel 사용. 빠른 작은 모델 (예: gemma2:2b) 권장."
|
||||
},
|
||||
"g1nation.selfCheckTimeoutSec": {
|
||||
"type": "number",
|
||||
"default": 6,
|
||||
"minimum": 1,
|
||||
"maximum": 60,
|
||||
"description": "Self-check LLM 호출 타임아웃 (초). 초과 시 흐릿한 한 줄 footer 로 fallback. 기본 6."
|
||||
},
|
||||
"g1nation.glossaryEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Terminology Dictionary — 사용자 편집 글로서리(.astra/glossary.md) 를 시스템 프롬프트에 주입. 표준 표기 강제 + 답변 직전 Term Check. 파일 없으면 자동 no-op. 기본 켜짐."
|
||||
},
|
||||
"g1nation.glossaryPath": {
|
||||
"type": "string",
|
||||
"default": ".astra/glossary.md",
|
||||
"description": "Glossary 파일 상대 경로 (workspace root 기준). 기본 '.astra/glossary.md'."
|
||||
},
|
||||
"g1nation.glossaryMaxBodyLength": {
|
||||
"type": "number",
|
||||
"default": 4000,
|
||||
"minimum": 500,
|
||||
"maximum": 20000,
|
||||
"description": "Glossary 본문 시스템 프롬프트 cap (chars). 너무 크면 토큰 비용↑. 초과분은 잘림. 기본 4000."
|
||||
},
|
||||
"g1nation.knowledgeMix.secondBrainWeight": {
|
||||
"type": "number",
|
||||
"default": 50,
|
||||
|
||||
+77
-1
@@ -164,6 +164,7 @@ import { buildAgentModeSystemPrompt } from './agent/handlePrompt/buildAgentModeS
|
||||
import { buildAstraModeSystemPrompt } from './agent/handlePrompt/buildAstraModeSystemPrompt';
|
||||
import { computeBudgetedRequest } from './agent/handlePrompt/computeBudgetedRequest';
|
||||
import { processFinalAnswer } from './agent/handlePrompt/processFinalAnswer';
|
||||
import { postHocSelfCheck, formatSelfCheckFooter, DEFAULT_SELF_CHECK_OPTIONS } from './agent/postHocSelfCheck';
|
||||
import { applyAutoContinuation } from './agent/handlePrompt/applyAutoContinuation';
|
||||
|
||||
export interface ChatMessage {
|
||||
@@ -285,10 +286,28 @@ export class AgentExecutor {
|
||||
lessons: string[];
|
||||
/** 이번 turn 에 결정된 Knowledge Mix — scope footer 표시용. */
|
||||
knowledgeMix: ResolvedKnowledgeMix | null;
|
||||
/** [CONFLICT WARNINGS] 시스템 프롬프트 블록 — 검색된 출처에서 충돌 신호 감지 시. */
|
||||
conflictWarnings: string;
|
||||
/** [VERIFICATION CHECKLIST] Chain-of-Verification 블록 — 그라운딩 자기 검증 지시. */
|
||||
coveChecklist: string;
|
||||
/** [INTENT CLARIFICATION GUIDANCE] — 모호 질의 감지 시 역질문 우선 지시. */
|
||||
intentClarification: string;
|
||||
/** [CITATION TRACE] — 답변 끝에 사용 출처 한 줄 정리 지시. */
|
||||
citationTrace: string;
|
||||
/** Self-check 용 — selected chunks 의 (title, content) 요약. memoryContext 가 채움. */
|
||||
selfCheckSources: Array<{ title: string; excerpt: string }>;
|
||||
/** [TERMINOLOGY DICTIONARY] — 사용자 편집 글로서리 + Term Check 지침. */
|
||||
terminology: string;
|
||||
} = {
|
||||
retrieval: null,
|
||||
lessons: [],
|
||||
knowledgeMix: null,
|
||||
conflictWarnings: '',
|
||||
coveChecklist: '',
|
||||
intentClarification: '',
|
||||
citationTrace: '',
|
||||
selfCheckSources: [],
|
||||
terminology: '',
|
||||
};
|
||||
|
||||
/** Per-turn state 일괄 정리. turn 시작/abort/load session 시 호출. */
|
||||
@@ -296,6 +315,12 @@ export class AgentExecutor {
|
||||
this._turnCtx.retrieval = null;
|
||||
this._turnCtx.lessons = [];
|
||||
this._turnCtx.knowledgeMix = null;
|
||||
this._turnCtx.conflictWarnings = '';
|
||||
this._turnCtx.coveChecklist = '';
|
||||
this._turnCtx.intentClarification = '';
|
||||
this._turnCtx.citationTrace = '';
|
||||
this._turnCtx.selfCheckSources = [];
|
||||
this._turnCtx.terminology = '';
|
||||
}
|
||||
|
||||
private readonly options: AgentExecutorOptions;
|
||||
@@ -647,6 +672,11 @@ export class AgentExecutor {
|
||||
isCasualConversation,
|
||||
localPathContext,
|
||||
knowledgeMix: this._turnCtx.knowledgeMix,
|
||||
conflictWarningsCtx: this._turnCtx.conflictWarnings,
|
||||
coveChecklistCtx: this._turnCtx.coveChecklist,
|
||||
intentClarificationCtx: this._turnCtx.intentClarification,
|
||||
citationTraceCtx: this._turnCtx.citationTrace,
|
||||
terminologyCtx: this._turnCtx.terminology,
|
||||
});
|
||||
// Context budget computation → src/agent/handlePrompt/computeBudgetedRequest.ts
|
||||
const imageCount = (reqMessages as any[])
|
||||
@@ -1200,6 +1230,12 @@ export class AgentExecutor {
|
||||
contextLength: ctxLimits.contextLength,
|
||||
engine,
|
||||
});
|
||||
// ── Post-hoc Self-Check (v2.2.191) — 별도 LLM 호출 1회로 답변 사후 검증. ──
|
||||
// 비동기 — Devil 과 동일 패턴. 결과를 footer 한 줄로 append.
|
||||
void this._maybePostHocSelfCheck({
|
||||
userPrompt: prompt || '',
|
||||
assistantAnswer: finalAssistantContent,
|
||||
});
|
||||
} else {
|
||||
this.webview.postMessage({ type: 'streamChunk', value: finalAssistantContent });
|
||||
}
|
||||
@@ -1282,10 +1318,18 @@ export class AgentExecutor {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
const workspacePath = workspaceFolders ? workspaceFolders[0].uri.fsPath : undefined;
|
||||
|
||||
const cfgNow = getConfig();
|
||||
this.memoryManager.onSessionEnd(
|
||||
this.currentTaskId,
|
||||
this.chatHistory.filter((m) => !m.internal),
|
||||
workspacePath
|
||||
workspacePath,
|
||||
cfgNow.localBrainPath ? {
|
||||
enabled: cfgNow.distillationEnabled !== false,
|
||||
ageThresholdDays: cfgNow.distillationAgeThresholdDays ?? 30,
|
||||
intervalDays: cfgNow.distillationIntervalDays ?? 7,
|
||||
archiveMode: (cfgNow.distillationArchiveMode || 'mark-promoted') as any,
|
||||
brainPath: cfgNow.localBrainPath,
|
||||
} : undefined,
|
||||
);
|
||||
logInfo('Memory extraction completed for session end.', { taskId: this.currentTaskId });
|
||||
recordTelemetry({
|
||||
@@ -1366,6 +1410,38 @@ export class AgentExecutor {
|
||||
}, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-hoc Self-Check — 답변 *완료 후* LLM 1회 호출로 3가지 평가
|
||||
* (사용자 질의 직접 답함 / 출처 그라운딩 / 논리 모순). 비동기 — main turn 에 영향 없음.
|
||||
* 기본 OFF (g1nation.selfCheckEnabled). 결과는 footer 한 줄로 streamChunk append.
|
||||
*/
|
||||
private async _maybePostHocSelfCheck(opts: {
|
||||
userPrompt: string;
|
||||
assistantAnswer: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const cfg = getConfig();
|
||||
if (!cfg.selfCheckEnabled) return;
|
||||
if (!opts.userPrompt.trim() || !opts.assistantAnswer.trim()) return;
|
||||
const model = (cfg.selfCheckModel || '').trim() || cfg.defaultModel;
|
||||
if (!model || !cfg.ollamaUrl) return;
|
||||
const sources = this._turnCtx.selfCheckSources || [];
|
||||
|
||||
const result = await postHocSelfCheck(opts.userPrompt, opts.assistantAnswer, sources, {
|
||||
ollamaUrl: cfg.ollamaUrl,
|
||||
model,
|
||||
timeoutMs: (cfg.selfCheckTimeoutSec ?? 6) * 1000,
|
||||
excerptLength: DEFAULT_SELF_CHECK_OPTIONS.excerptLength,
|
||||
maxSources: DEFAULT_SELF_CHECK_OPTIONS.maxSources,
|
||||
});
|
||||
|
||||
// 성공 실패 모두 footer 표시 — 사용자가 self-check 가 *돌고 있는지* 알 수 있게.
|
||||
// 실패 시 흐릿한 한 줄, 성공 시 평가 한 줄.
|
||||
const footer = formatSelfCheckFooter(result, model);
|
||||
this.webview?.postMessage({ type: 'streamChunk', value: footer });
|
||||
} catch { /* swallow — self-check never breaks the turn */ }
|
||||
}
|
||||
|
||||
private async callNonStreaming(params: {
|
||||
baseUrl: string;
|
||||
modelName: string;
|
||||
|
||||
@@ -22,6 +22,28 @@ export interface BuildAstraModeSystemPromptInput {
|
||||
localPathContext: string;
|
||||
/** From this._turnCtx.knowledgeMix — pass null when absent. */
|
||||
knowledgeMix: any;
|
||||
/**
|
||||
* [CONFLICT WARNINGS] 블록 — buildConflictWarningsBlock 산출. 빈 문자열이면 충돌 없음 → 주입 안 함.
|
||||
* v4 정책 텍스트의 "[CONFLICT WARNING] 플래그" 참조를 실제 데이터로 뒷받침.
|
||||
*/
|
||||
conflictWarningsCtx?: string;
|
||||
/**
|
||||
* [VERIFICATION CHECKLIST] CoVe 블록 — buildCoveChecklistBlock 산출. 답변 *작성 전*
|
||||
* 그라운딩 체크리스트로 모델 self-verify 지시. 빈 문자열이면 비활성.
|
||||
*/
|
||||
coveChecklistCtx?: string;
|
||||
/**
|
||||
* [INTENT CLARIFICATION GUIDANCE] — 모호 질의 감지 시 *역질문 우선* 지시. 모호 아닐 때 빈 문자열.
|
||||
*/
|
||||
intentClarificationCtx?: string;
|
||||
/**
|
||||
* [CITATION TRACE] — 답변 끝에 사용 출처 한 줄 정리 지시. 검색 결과 있을 때 채워짐.
|
||||
*/
|
||||
citationTraceCtx?: string;
|
||||
/**
|
||||
* [TERMINOLOGY DICTIONARY] — 사용자 편집 글로서리 + Term Check 지침. 파일 있을 때만.
|
||||
*/
|
||||
terminologyCtx?: string;
|
||||
}
|
||||
|
||||
export function buildAstraModeSystemPrompt(input: BuildAstraModeSystemPromptInput): string {
|
||||
@@ -40,6 +62,11 @@ export function buildAstraModeSystemPrompt(input: BuildAstraModeSystemPromptInpu
|
||||
isCasualConversation,
|
||||
localPathContext,
|
||||
knowledgeMix,
|
||||
conflictWarningsCtx,
|
||||
coveChecklistCtx,
|
||||
intentClarificationCtx,
|
||||
citationTraceCtx,
|
||||
terminologyCtx,
|
||||
} = input;
|
||||
|
||||
// 기존 Astra 모드 (에이전트 미선택)
|
||||
@@ -78,5 +105,29 @@ export function buildAstraModeSystemPrompt(input: BuildAstraModeSystemPromptInpu
|
||||
// priorConclusionCtx 는 modeBridgeCtx 와 같은 위치 (base systemPrompt 직후) — 모델이
|
||||
// 자기 직전 결론을 anchor 로 잡고 사용자의 follow-up 을 그 결론에 대한 정정으로 해석하게.
|
||||
const priorConclusionBlock = priorConclusionCtx ? '\n\n' + priorConclusionCtx : '';
|
||||
return `${systemPrompt}${modeBridgeCtx ? '\n\n' + modeBridgeCtx : ''}${priorConclusionBlock}${designerCtx}${projectArchitectureCtx}${localProjectKnowledgeCtx}${thinkingPartnerCtx}${astraStanceCtx}${secondBrainTraceCtx}${v4PolicyCtx}${knowledgeMixCtx}${casualCtx}\n\n[CONTEXT]\n${memoryCtx}\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}`;
|
||||
// [CONFLICT WARNINGS] 는 [CONTEXT] 밖에 — token-truncation 시 보호. v4 정책이
|
||||
// 충돌 처리 *방법* 을 명시하고, 이 블록이 *어느 출처가 충돌* 인지 데이터 제공.
|
||||
// Casual conversation 모드에서는 RAG context 자체를 안 쓰므로 충돌 경고도 무의미 — 생략.
|
||||
const conflictWarningsBlock = (!isCasualConversation && conflictWarningsCtx && conflictWarningsCtx.trim())
|
||||
? '\n\n' + conflictWarningsCtx
|
||||
: '';
|
||||
// [VERIFICATION CHECKLIST] CoVe — 답변 작성 전 self-verify 지시. Conflict 와 마찬가지로
|
||||
// [CONTEXT] 밖, casual 모드 비활성. CoVe 가 강하면 단정적 답변이 줄고 근거 인용 늘어남.
|
||||
const coveBlock = (!isCasualConversation && coveChecklistCtx && coveChecklistCtx.trim())
|
||||
? '\n\n' + coveChecklistCtx
|
||||
: '';
|
||||
// [INTENT CLARIFICATION GUIDANCE] — 모호 차원 감지 시 *역질문 우선*. Casual 모드는 제외.
|
||||
// 위치: 다른 verification block 보다 *앞* — 모호하면 답변 자체를 안 만들어야 하므로.
|
||||
const intentBlock = (!isCasualConversation && intentClarificationCtx && intentClarificationCtx.trim())
|
||||
? '\n\n' + intentClarificationCtx
|
||||
: '';
|
||||
// [CITATION TRACE] — 답변 끝에 출처 한 줄. CoVe 와 함께 동작 — CoVe 가 라벨, Citation 이 정리.
|
||||
const citationBlock = (!isCasualConversation && citationTraceCtx && citationTraceCtx.trim())
|
||||
? '\n\n' + citationTraceCtx
|
||||
: '';
|
||||
// [TERMINOLOGY DICTIONARY] — 사용자 편집 글로서리. casual 모드 비활성 (greeting 에 용어 강제 의미 없음).
|
||||
const terminologyBlock = (!isCasualConversation && terminologyCtx && terminologyCtx.trim())
|
||||
? '\n\n' + terminologyCtx
|
||||
: '';
|
||||
return `${systemPrompt}${modeBridgeCtx ? '\n\n' + modeBridgeCtx : ''}${priorConclusionBlock}${designerCtx}${projectArchitectureCtx}${localProjectKnowledgeCtx}${thinkingPartnerCtx}${astraStanceCtx}${secondBrainTraceCtx}${v4PolicyCtx}${knowledgeMixCtx}${casualCtx}${intentBlock}${terminologyBlock}${conflictWarningsBlock}${coveBlock}${citationBlock}\n\n[CONTEXT]\n${memoryCtx}\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Post-hoc Self-Check — 답변 *완료 후* LLM 한 번 호출로 3가지 평가.
|
||||
*
|
||||
* 사용자 제안: "[Self-Check] 단계 — 이 답변이 사용자 질문에 직접 답하는가 / 규칙
|
||||
* 준수 / 논리 모순 없는가".
|
||||
*
|
||||
* 기존 CoVe (v2.2.184) 와 차이:
|
||||
* - CoVe = *답변 작성 전* 모델에게 self-verify 지시 (instructional, 1 pass)
|
||||
* - Self-Check = *답변 완료 후* 별도 LLM 호출로 검증 (post-hoc, 2 pass)
|
||||
*
|
||||
* 비용·위험:
|
||||
* - 매 turn 추가 LLM 호출 1회 (latency 비용)
|
||||
* - 기본 OFF — semantic re-rank 와 같은 opt-in 패턴
|
||||
* - 짧은 timeout (기본 6초). 실패해도 답변 자체엔 영향 없음 — 그냥 평가 못 함.
|
||||
* - 빠른 작은 모델 권장 (예: gemma2:2b)
|
||||
*
|
||||
* 위치: 답변 streaming 완료 후, `usedScope` 메시지 전송 직전. 비동기 — 답변
|
||||
* 표시를 *블록 하지 않음*. 결과는 webview 에 별도 메시지로 push.
|
||||
*/
|
||||
|
||||
export interface SelfCheckOptions {
|
||||
ollamaUrl: string;
|
||||
model: string;
|
||||
timeoutMs: number;
|
||||
/** 출처 컨텍스트 미리보기 길이. 기본 180 chars. */
|
||||
excerptLength: number;
|
||||
/** 컨텍스트로 넘길 최대 출처 개수. 기본 5. */
|
||||
maxSources: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_SELF_CHECK_OPTIONS: Omit<SelfCheckOptions, 'ollamaUrl' | 'model'> = {
|
||||
timeoutMs: 6000,
|
||||
excerptLength: 180,
|
||||
maxSources: 5,
|
||||
};
|
||||
|
||||
export type SelfCheckVerdict = 'yes' | 'partial' | 'no' | 'unknown';
|
||||
export type ContradictionLevel = 'none' | 'minor' | 'major' | 'unknown';
|
||||
|
||||
export interface SelfCheckResult {
|
||||
success: boolean;
|
||||
answersQuestion: SelfCheckVerdict;
|
||||
grounded: SelfCheckVerdict;
|
||||
contradiction: ContradictionLevel;
|
||||
note: string;
|
||||
durationMs: number;
|
||||
/** 디버그·footer 표시용. */
|
||||
rawResponse?: string;
|
||||
}
|
||||
|
||||
const FAILURE_RESULT: Omit<SelfCheckResult, 'durationMs' | 'note'> = {
|
||||
success: false,
|
||||
answersQuestion: 'unknown',
|
||||
grounded: 'unknown',
|
||||
contradiction: 'unknown',
|
||||
};
|
||||
|
||||
function shortExcerpt(text: string, n: number): string {
|
||||
if (!text) return '';
|
||||
const cleaned = text.replace(/\s+/g, ' ').trim();
|
||||
return cleaned.length <= n ? cleaned : cleaned.slice(0, n) + '…';
|
||||
}
|
||||
|
||||
function buildPrompt(
|
||||
userPrompt: string,
|
||||
answer: string,
|
||||
sources: { title: string; excerpt: string }[],
|
||||
excerptLength: number,
|
||||
): { system: string; user: string } {
|
||||
const system = [
|
||||
'당신은 답변 검증기 (judge). 사용자 질문, 답변, 출처를 받아 3가지 평가:',
|
||||
'',
|
||||
'1. answersQuestion: 답변이 질문에 *직접* 답하는가? (yes/partial/no)',
|
||||
'2. grounded: 답변이 *제공된 출처에 근거* 하는가? (출처 없으면 unknown 가능) (yes/partial/no/unknown)',
|
||||
'3. contradiction: 답변에 *논리적 모순* 이 있나? (none/minor/major)',
|
||||
'',
|
||||
'[출력 형식 — 정확히 한 줄 JSON, 다른 텍스트 절대 금지]',
|
||||
'{"answersQuestion":"yes","grounded":"partial","contradiction":"none","note":"답변은 직접적이나 일부 주장이 모델 일반 지식 기반"}',
|
||||
'',
|
||||
'[규칙]',
|
||||
'- partial/minor 는 *진짜* 애매한 경우에만. 둘 중 하나로 단정 가능하면 단정.',
|
||||
'- note 는 1문장, 80자 이내, 핵심 평가 근거.',
|
||||
'- JSON 한 줄 외 텍스트 (서론·설명·코드블록) 절대 출력 금지.',
|
||||
].join('\n');
|
||||
|
||||
const srcLines = sources.length > 0
|
||||
? sources.map((s, i) => `[S${i + 1}] ${s.title}\n ${shortExcerpt(s.excerpt, excerptLength)}`).join('\n')
|
||||
: '(검색된 출처 없음 — grounded 는 unknown 또는 no 평가)';
|
||||
|
||||
const user = [
|
||||
'[사용자 질문]',
|
||||
userPrompt,
|
||||
'',
|
||||
'[답변]',
|
||||
answer,
|
||||
'',
|
||||
'[제공된 출처]',
|
||||
srcLines,
|
||||
'',
|
||||
'위 평가 기준에 따라 JSON 한 줄 출력.',
|
||||
].join('\n');
|
||||
|
||||
return { system, user };
|
||||
}
|
||||
|
||||
function parseResult(raw: string): Omit<SelfCheckResult, 'durationMs' | 'rawResponse'> | null {
|
||||
if (!raw) return null;
|
||||
const match = raw.match(/\{[\s\S]*?\}/);
|
||||
if (!match) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(match[0]);
|
||||
const aq = String(parsed?.answersQuestion || '').toLowerCase();
|
||||
const gr = String(parsed?.grounded || '').toLowerCase();
|
||||
const co = String(parsed?.contradiction || '').toLowerCase();
|
||||
const validVerdict = (v: string): v is SelfCheckVerdict => ['yes', 'partial', 'no', 'unknown'].includes(v);
|
||||
const validCo = (v: string): v is ContradictionLevel => ['none', 'minor', 'major', 'unknown'].includes(v);
|
||||
if (!validVerdict(aq) || !validVerdict(gr) || !validCo(co)) return null;
|
||||
const note = typeof parsed?.note === 'string' ? parsed.note.slice(0, 120) : '';
|
||||
return {
|
||||
success: true,
|
||||
answersQuestion: aq,
|
||||
grounded: gr,
|
||||
contradiction: co,
|
||||
note: note || '평가 노트 없음',
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function postHocSelfCheck(
|
||||
userPrompt: string,
|
||||
answer: string,
|
||||
sources: { title: string; excerpt: string }[],
|
||||
options: SelfCheckOptions,
|
||||
): Promise<SelfCheckResult> {
|
||||
const start = Date.now();
|
||||
if (!userPrompt.trim() || !answer.trim()) {
|
||||
return { ...FAILURE_RESULT, note: 'empty input', durationMs: Date.now() - start };
|
||||
}
|
||||
const sourcesCap = (sources || []).slice(0, options.maxSources);
|
||||
const { system, user } = buildPrompt(userPrompt, answer, sourcesCap, options.excerptLength);
|
||||
|
||||
const isOllama = options.ollamaUrl.includes(':11434') || options.ollamaUrl.includes('ollama');
|
||||
const endpoint = isOllama ? `${options.ollamaUrl}/api/chat` : `${options.ollamaUrl}/v1/chat/completions`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
||||
let raw = '';
|
||||
try {
|
||||
const body = isOllama
|
||||
? {
|
||||
model: options.model, stream: false,
|
||||
messages: [
|
||||
{ role: 'system', content: system },
|
||||
{ role: 'user', content: user },
|
||||
],
|
||||
options: { temperature: 0.0, num_predict: 200 },
|
||||
}
|
||||
: {
|
||||
model: options.model, stream: false, temperature: 0.0, max_tokens: 200,
|
||||
messages: [
|
||||
{ role: 'system', content: system },
|
||||
{ role: 'user', content: user },
|
||||
],
|
||||
};
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data: any = await res.json();
|
||||
raw = String(
|
||||
data?.message?.content ??
|
||||
data?.choices?.[0]?.message?.content ??
|
||||
data?.choices?.[0]?.text ??
|
||||
data?.response ??
|
||||
'',
|
||||
);
|
||||
} catch (e: any) {
|
||||
clearTimeout(timer);
|
||||
return {
|
||||
...FAILURE_RESULT,
|
||||
note: `LLM call failed: ${e?.name || e?.message || 'unknown'}`,
|
||||
durationMs: Date.now() - start,
|
||||
rawResponse: '',
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
const parsed = parseResult(raw);
|
||||
if (!parsed) {
|
||||
return {
|
||||
...FAILURE_RESULT,
|
||||
note: 'unparseable response',
|
||||
durationMs: Date.now() - start,
|
||||
rawResponse: raw.slice(0, 200),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...parsed,
|
||||
durationMs: Date.now() - start,
|
||||
rawResponse: raw.slice(0, 200),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과를 markdown 한 줄 footer 로 포맷 — 사용자가 답변 아래에서 바로 봄.
|
||||
*
|
||||
* 형식: `\n\n---\n_🔍 Self-check_: 답함=✓ · 근거=○ · 모순=없음 _(2.4s · 모델: gemma2:2b)_`
|
||||
*
|
||||
* 실패면 흐릿한 한 줄.
|
||||
*/
|
||||
export function formatSelfCheckFooter(result: SelfCheckResult, model: string): string {
|
||||
if (!result.success) {
|
||||
return `\n\n---\n_🔍 Self-check: ⊘ ${result.note} (${(result.durationMs / 1000).toFixed(1)}s)_`;
|
||||
}
|
||||
const aq = result.answersQuestion === 'yes' ? '✓'
|
||||
: result.answersQuestion === 'partial' ? '◐'
|
||||
: result.answersQuestion === 'no' ? '✗' : '?';
|
||||
const gr = result.grounded === 'yes' ? '✓'
|
||||
: result.grounded === 'partial' ? '◐'
|
||||
: result.grounded === 'no' ? '✗' : '?';
|
||||
const co = result.contradiction === 'none' ? '없음'
|
||||
: result.contradiction === 'minor' ? '경미'
|
||||
: result.contradiction === 'major' ? '⚠️ 중대' : '?';
|
||||
return `\n\n---\n_🔍 **Self-check**: 답함=${aq} · 근거=${gr} · 모순=${co} — ${result.note} _(${(result.durationMs / 1000).toFixed(1)}s · ${model})__`;
|
||||
}
|
||||
+132
@@ -60,6 +60,113 @@ export interface IAgentConfig {
|
||||
* Default 0.5 = equal weight, a reasonable starting point.
|
||||
*/
|
||||
embeddingBlendAlpha: number;
|
||||
/**
|
||||
* Conflict Surface — 검색된 출처의 conflictSeverity 신호를 [CONFLICT WARNINGS] 블록
|
||||
* 으로 시스템 프롬프트에 노출. v4 정책 텍스트(buildAstraModeSystemPrompt) 가 이미
|
||||
* "[CONFLICT WARNING] 플래그" 를 참조하지만, 데이터를 LLM 에 전달하지 않아 무용했음.
|
||||
* true(기본) → 충돌 감지 시 블록 주입, false → 비활성.
|
||||
*/
|
||||
conflictHighlightingEnabled: boolean;
|
||||
/**
|
||||
* Conflict 자기-신호 surface 시 최소 severity 임계.
|
||||
* 'low' → LOW 이상 (가장 민감, 노이즈 가능)
|
||||
* 'medium' → MEDIUM 이상 (기본, 균형)
|
||||
* 'high' → HIGH 만 (가장 보수적, 강한 충돌만)
|
||||
*/
|
||||
conflictSeverityThreshold: 'low' | 'medium' | 'high';
|
||||
/**
|
||||
* 교차-문서 발산 감지 (같은 주제 ≥2 chunks, 본문 Jaccard < 0.30 인 잠재 모순 쌍).
|
||||
* 자기-신호와 합쳐 [CONFLICT WARNINGS] 블록에 표시.
|
||||
*/
|
||||
conflictCrossDocEnabled: boolean;
|
||||
/**
|
||||
* CoVe (Chain-of-Verification) — 답변 *작성 전* 그라운딩 체크리스트를 시스템 프롬프트에
|
||||
* 주입해 모델이 self-verify 하도록. 할루시네이션 방지 + 그라운딩 명확화.
|
||||
* true(기본) → 검색된 출처 있을 때 [VERIFICATION CHECKLIST] 블록 주입.
|
||||
*/
|
||||
coveEnabled: boolean;
|
||||
/** CoVe 체크리스트에 나열할 상위 출처 개수. 기본 5. */
|
||||
coveTopSourcesCount: number;
|
||||
/**
|
||||
* CoVe Strict 모드 — 모든 사실 주장 뒤에 출처 ID([S1] 등) inline 인용 강제.
|
||||
* 답변이 좀 더 학술적·verbose 해질 수 있어 기본 off.
|
||||
*/
|
||||
coveStrictMode: boolean;
|
||||
/**
|
||||
* Actionability — "현재 작업 상태" 신호(최근 슬래시 명령 + 열린 파일) 로 검색 결과
|
||||
* 재가중. TF-IDF 매치 점수에 actionability boost 추가해 "지금 작업 중인 컨텍스트" 와
|
||||
* 직접 연결된 문서를 우선. 기본 true.
|
||||
*/
|
||||
actionabilityEnabled: boolean;
|
||||
/**
|
||||
* Memory Lifecycle — Distillation Loop: stale Episodic Memory 를 LongTerm 'episode-digest'
|
||||
* 로 승급. 누적 epimemory 가 검색 노이즈가 되는 것 방지.
|
||||
* true(기본) → /memory distill + 세션 종료 시 자동 트리거 (interval 기준).
|
||||
*/
|
||||
distillationEnabled: boolean;
|
||||
/** 며칠 이상 지난 episode 를 distill 대상으로. 기본 30. */
|
||||
distillationAgeThresholdDays: number;
|
||||
/** 자동 distillation 의 최소 간격 (일). 너무 자주 안 돌도록. 기본 7. */
|
||||
distillationIntervalDays: number;
|
||||
/**
|
||||
* Archive 모드:
|
||||
* - 'mark-promoted' (기본): promoted=true 마킹만, 파일 보존
|
||||
* - 'archive-file': promoted 마킹 + 파일을 memory/episodes/archive/ 로 이동
|
||||
*/
|
||||
distillationArchiveMode: 'mark-promoted' | 'archive-file';
|
||||
/**
|
||||
* Hierarchical Context Window — 질의·문서 추상도(concrete/operational/strategic) 매칭으로
|
||||
* 검색 결과 재가중. 같은 레벨 boost (× 1.15), 양 끝 mismatch penalty (× 0.7). LLM 호출 없음.
|
||||
*/
|
||||
hierarchicalReweightEnabled: boolean;
|
||||
/**
|
||||
* Semantic Re-ranking — 토큰 예산 통과한 selectedChunks 의 *순서* 를 LLM 한 번 호출로
|
||||
* 재정렬. 의도 매치를 키워드 매치보다 우선. 매 turn 1회 추가 LLM 호출 (latency 비용).
|
||||
* 기본 OFF — 명시적으로 on 해야 함.
|
||||
*/
|
||||
semanticRerankEnabled: boolean;
|
||||
/** 재정렬 전용 모델 ID. 비면 defaultModel 사용. 빠른 작은 모델 권장. */
|
||||
semanticRerankModel: string;
|
||||
/** Re-rank 대상 상위 후보 개수. 기본 15. */
|
||||
semanticRerankCandidateK: number;
|
||||
/** Re-rank LLM 호출 타임아웃 (초). 기본 8. */
|
||||
semanticRerankTimeoutSec: number;
|
||||
/**
|
||||
* Intent Clarification — 모호 질의에서 *추측 답변 대신 역질문* 지시.
|
||||
* 휴리스틱 차원(환경/대상/범위/포맷/마감) 별 trigger + specifier 매치. 기본 true.
|
||||
*/
|
||||
intentClarificationEnabled: boolean;
|
||||
/**
|
||||
* 모호 판정 임계:
|
||||
* - 'low': 2개 이상 missing dimension 일 때만 ambiguous (가장 덜 묻기)
|
||||
* - 'medium' (기본): 1개 이상 missing → ambiguous
|
||||
* - 'high': 1개 이상 missing OR 짧은 prompt(<20자)+trigger 있으면 ambiguous (가장 자주 묻기)
|
||||
*/
|
||||
intentClarificationStrictness: 'low' | 'medium' | 'high';
|
||||
/**
|
||||
* Citation Trace — 답변 끝에 "출처:" 한 줄 정리 지시. CoVe Strict 의 가벼운 형제.
|
||||
* 검색 결과 있을 때만 동작. 기본 true.
|
||||
*/
|
||||
citationTraceEnabled: boolean;
|
||||
/**
|
||||
* Post-hoc Self-Check — 답변 *완료 후* LLM 1회 호출로 검증 (답변 직접도 / 그라운딩 /
|
||||
* 논리 모순). 매 turn 추가 LLM 호출 (latency 비용). 기본 OFF — 명시적 opt-in.
|
||||
* 결과는 답변 아래 footer 한 줄로 표시.
|
||||
*/
|
||||
selfCheckEnabled: boolean;
|
||||
/** Self-check 전용 모델 ID. 비면 defaultModel. 빠른 작은 모델 권장. */
|
||||
selfCheckModel: string;
|
||||
/** Self-check LLM 호출 타임아웃 (초). 기본 6. */
|
||||
selfCheckTimeoutSec: number;
|
||||
/**
|
||||
* Terminology Dictionary — 사용자 편집 글로서리 파일을 시스템 프롬프트에 주입,
|
||||
* 표준 표기 강제 + 답변 직전 자기 점검(Term Check). 기본 true. 파일 없으면 자동 no-op.
|
||||
*/
|
||||
glossaryEnabled: boolean;
|
||||
/** Glossary 파일 상대 경로 (workspace root 기준). 기본 '.astra/glossary.md'. */
|
||||
glossaryPath: string;
|
||||
/** Glossary 본문 시스템 프롬프트 cap (chars). 너무 크면 토큰 비용↑. 기본 4000. */
|
||||
glossaryMaxBodyLength: number;
|
||||
/**
|
||||
* Global Knowledge Mix weight (0–100). Controls how much the assistant leans on
|
||||
* Second Brain evidence vs. model general knowledge when answering.
|
||||
@@ -307,6 +414,31 @@ export function getConfig(): IAgentConfig {
|
||||
finalOnlyRetryOnThoughtLeak: cfg.get<boolean>('finalOnlyRetryOnThoughtLeak', true),
|
||||
embeddingModel: (cfg.get<string>('embeddingModel', '') || '').trim(),
|
||||
embeddingBlendAlpha: Math.max(0, Math.min(1, cfg.get<number>('embeddingBlendAlpha', 0.5))),
|
||||
conflictHighlightingEnabled: cfg.get<boolean>('conflictHighlightingEnabled', true),
|
||||
conflictSeverityThreshold: (cfg.get<string>('conflictSeverityThreshold', 'medium') as 'low' | 'medium' | 'high') || 'medium',
|
||||
conflictCrossDocEnabled: cfg.get<boolean>('conflictCrossDocEnabled', true),
|
||||
coveEnabled: cfg.get<boolean>('coveEnabled', true),
|
||||
coveTopSourcesCount: Math.max(1, Math.min(15, cfg.get<number>('coveTopSourcesCount', 5))),
|
||||
coveStrictMode: cfg.get<boolean>('coveStrictMode', false),
|
||||
actionabilityEnabled: cfg.get<boolean>('actionabilityEnabled', true),
|
||||
distillationEnabled: cfg.get<boolean>('distillationEnabled', true),
|
||||
distillationAgeThresholdDays: Math.max(1, Math.min(365, cfg.get<number>('distillationAgeThresholdDays', 30))),
|
||||
distillationIntervalDays: Math.max(1, Math.min(90, cfg.get<number>('distillationIntervalDays', 7))),
|
||||
distillationArchiveMode: (cfg.get<string>('distillationArchiveMode', 'mark-promoted') as 'mark-promoted' | 'archive-file') || 'mark-promoted',
|
||||
hierarchicalReweightEnabled: cfg.get<boolean>('hierarchicalReweightEnabled', true),
|
||||
semanticRerankEnabled: cfg.get<boolean>('semanticRerankEnabled', false),
|
||||
semanticRerankModel: cfg.get<string>('semanticRerankModel', '') || '',
|
||||
semanticRerankCandidateK: Math.max(2, Math.min(30, cfg.get<number>('semanticRerankCandidateK', 15))),
|
||||
semanticRerankTimeoutSec: Math.max(1, Math.min(60, cfg.get<number>('semanticRerankTimeoutSec', 8))),
|
||||
intentClarificationEnabled: cfg.get<boolean>('intentClarificationEnabled', true),
|
||||
intentClarificationStrictness: (cfg.get<string>('intentClarificationStrictness', 'medium') as 'low' | 'medium' | 'high') || 'medium',
|
||||
citationTraceEnabled: cfg.get<boolean>('citationTraceEnabled', true),
|
||||
selfCheckEnabled: cfg.get<boolean>('selfCheckEnabled', false),
|
||||
selfCheckModel: cfg.get<string>('selfCheckModel', '') || '',
|
||||
selfCheckTimeoutSec: Math.max(1, Math.min(60, cfg.get<number>('selfCheckTimeoutSec', 6))),
|
||||
glossaryEnabled: cfg.get<boolean>('glossaryEnabled', true),
|
||||
glossaryPath: cfg.get<string>('glossaryPath', '.astra/glossary.md') || '.astra/glossary.md',
|
||||
glossaryMaxBodyLength: Math.max(500, Math.min(20000, cfg.get<number>('glossaryMaxBodyLength', 4000))),
|
||||
knowledgeMixSecondBrainWeight: Math.max(0, Math.min(100, Math.round(
|
||||
cfg.get<number>('knowledgeMix.secondBrainWeight', 50)
|
||||
))),
|
||||
|
||||
@@ -33,6 +33,8 @@ export {
|
||||
|
||||
export {
|
||||
createTask,
|
||||
listTasks,
|
||||
TaskInput,
|
||||
CreatedTask,
|
||||
ListedTask,
|
||||
} from './tasksApi';
|
||||
|
||||
@@ -103,3 +103,67 @@ export async function createTask(
|
||||
return { ok: false, error: e?.message || String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
export interface ListedTask {
|
||||
id: string;
|
||||
title: string;
|
||||
status: 'needsAction' | 'completed';
|
||||
/** 'YYYY-MM-DD' 형식. due 가 없는 task 는 undefined. */
|
||||
due?: string;
|
||||
/** 완료 시각 ISO timestamp. status 'completed' 일 때만 있음. */
|
||||
completed?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Tasks 목록 조회 — /onesie 1:1 카드 등에서 멤버별 필터링용.
|
||||
*
|
||||
* 기본 default list 의 task 들을 가져온다 (완료 포함). 호출자가 클라이언트 측에서
|
||||
* 제목 prefix `[멤버]` 나 notes 의 `@멤버` / `담당: 멤버` 패턴으로 필터하면 됨.
|
||||
*/
|
||||
export async function listTasks(
|
||||
context: vscode.ExtensionContext,
|
||||
options: { taskListId?: string; showCompleted?: boolean; maxResults?: number } = {},
|
||||
): Promise<{ ok: true; tasks: ListedTask[] } | { ok: false; error: string }> {
|
||||
const tokenResult = await getFreshAccessToken(context);
|
||||
if (!tokenResult.ok) return { ok: false, error: tokenResult.error };
|
||||
|
||||
const taskListId = (options.taskListId || '@default').trim() || '@default';
|
||||
const params = new URLSearchParams({
|
||||
maxResults: String(options.maxResults ?? 100),
|
||||
showCompleted: options.showCompleted !== false ? 'true' : 'false',
|
||||
showHidden: 'true', // 완료 후 숨김 처리된 것도 포함 — 1:1 회고에 최근 완료가 중요.
|
||||
});
|
||||
const url = `${API_BASE}/lists/${encodeURIComponent(taskListId)}/tasks?${params.toString()}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${tokenResult.accessToken}` },
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
const json: any = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const msg: string = json?.error?.message || `HTTP ${res.status}`;
|
||||
if (res.status === 401 || res.status === 403 || /insufficient|scope/i.test(msg)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Tasks API 권한 부족 — "Astra: Google Calendar OAuth 연결 (쓰기)" 명령 재실행 + Tasks 스코프 동의 필요. (원인: ${msg})`,
|
||||
};
|
||||
}
|
||||
return { ok: false, error: msg };
|
||||
}
|
||||
const items: any[] = Array.isArray(json.items) ? json.items : [];
|
||||
const tasks: ListedTask[] = items.map((item: any) => ({
|
||||
id: String(item.id || ''),
|
||||
title: String(item.title || ''),
|
||||
status: item.status === 'completed' ? 'completed' : 'needsAction',
|
||||
due: typeof item.due === 'string' ? item.due.slice(0, 10) : undefined,
|
||||
completed: typeof item.completed === 'string' ? item.completed : undefined,
|
||||
notes: typeof item.notes === 'string' ? item.notes : undefined,
|
||||
}));
|
||||
return { ok: true, tasks };
|
||||
} catch (e: any) {
|
||||
return { ok: false, error: e?.message || String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 고객사 / MRR / 갱신 트래커.
|
||||
*
|
||||
* 4인 기업의 수입 쪽 — `/runway` 가 통장과 burn 을 본다면, 여기는 *어디서 돈이 들어오나*.
|
||||
* Salesforce / HubSpot 같은 CRM 아닌 가벼운 event-sourced 로그.
|
||||
*
|
||||
* 저장 형식: JSON Lines (.jsonl) — append-only event log. 같은 customer 의 여러 이벤트를
|
||||
* 시간 순으로 재생하면 현재 상태 (MRR, 갱신일, 위험 등급) 가 나온다.
|
||||
*
|
||||
* 위치: `<workspace>/.astra/customers.jsonl`. 사람이 직접 편집 가능, grep / 백업 친화.
|
||||
* 민감 정보(고객사 이름, 매출) 포함되므로 외부로 안 보냄 — 로컬 only.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const STORE_REL_PATH = '.astra/customers.jsonl';
|
||||
|
||||
export type CustomerEventType = 'add' | 'note' | 'risk' | 'churn' | 'renew' | 'update';
|
||||
|
||||
export interface CustomerEvent {
|
||||
/** unique id — timestamp 기반. */
|
||||
id: string;
|
||||
/** ISO timestamp. */
|
||||
timestamp: string;
|
||||
/** 고객 식별자 — 소문자·trim 한 name 의 slug. 같은 customer 의 이벤트끼리 그룹. */
|
||||
customerId: string;
|
||||
/** 표시용 원본 이름 (가장 최근 이벤트의 이름 우선). */
|
||||
customerName: string;
|
||||
/** 이벤트 종류. */
|
||||
type: CustomerEventType;
|
||||
/** 월 매출 — add/renew/update 에서 사용. */
|
||||
mrr?: number;
|
||||
/** 갱신일 (YYYY-MM-DD) — add/renew/update. */
|
||||
renewalAt?: string;
|
||||
/** 요금제명 — 'pro' / 'enterprise' / 'starter' 등. */
|
||||
plan?: string;
|
||||
/** 자유 텍스트 — note / risk / churn 의 사유. */
|
||||
memo?: string;
|
||||
}
|
||||
|
||||
export type CustomerStatus = 'active' | 'at-risk' | 'churned';
|
||||
|
||||
export interface CustomerState {
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
mrr: number;
|
||||
plan?: string;
|
||||
renewalAt?: string;
|
||||
status: CustomerStatus;
|
||||
startedAt: string;
|
||||
lastEventAt: string;
|
||||
eventCount: number;
|
||||
notes: { timestamp: string; type: CustomerEventType; memo: string }[];
|
||||
}
|
||||
|
||||
export function getCustomersFilePath(): string | null {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (!folders || folders.length === 0) return null;
|
||||
return path.join(folders[0].uri.fsPath, STORE_REL_PATH);
|
||||
}
|
||||
|
||||
export function customerIdFromName(name: string): string {
|
||||
return name.toLowerCase().trim().replace(/\s+/g, '-').replace(/[^\w가-힣-]/g, '');
|
||||
}
|
||||
|
||||
export function readEvents(): CustomerEvent[] {
|
||||
const filePath = getCustomersFilePath();
|
||||
if (!filePath || !fs.existsSync(filePath)) return [];
|
||||
const out: CustomerEvent[] = [];
|
||||
let content = '';
|
||||
try { content = fs.readFileSync(filePath, 'utf-8'); } catch { return []; }
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
const e = JSON.parse(trimmed);
|
||||
if (e && typeof e.id === 'string' && typeof e.customerId === 'string' && typeof e.type === 'string') {
|
||||
out.push(e as CustomerEvent);
|
||||
}
|
||||
} catch { /* skip malformed */ }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function appendEvent(event: CustomerEvent): { ok: true; filePath: string } | { ok: false; error: string } {
|
||||
const filePath = getCustomersFilePath();
|
||||
if (!filePath) return { ok: false, error: '워크스페이스 폴더가 없어 저장 불가.' };
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.appendFileSync(filePath, JSON.stringify(event) + '\n', 'utf-8');
|
||||
return { ok: true, filePath };
|
||||
} catch (e: any) {
|
||||
return { ok: false, error: e?.message || String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 로그를 재생해 customerId 별 현재 상태 도출.
|
||||
*
|
||||
* - add: 신규 생성 (status=active, startedAt 설정)
|
||||
* - update / renew: mrr / renewalAt / plan 갱신, status=active 로 복귀 (renew 시)
|
||||
* - risk: status=at-risk, memo 누적
|
||||
* - churn: status=churned, mrr=0
|
||||
* - note: 노트 누적만, 상태 무변경
|
||||
*/
|
||||
export function computeCustomerStates(): Map<string, CustomerState> {
|
||||
const events = readEvents().slice().sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
||||
const states = new Map<string, CustomerState>();
|
||||
for (const e of events) {
|
||||
let s = states.get(e.customerId);
|
||||
if (!s) {
|
||||
if (e.type !== 'add') {
|
||||
// add 이벤트 없이 다른 이벤트가 먼저 와도 묵시적 생성 — 데이터 손상 방어.
|
||||
s = {
|
||||
customerId: e.customerId,
|
||||
customerName: e.customerName,
|
||||
mrr: 0,
|
||||
status: 'active',
|
||||
startedAt: e.timestamp,
|
||||
lastEventAt: e.timestamp,
|
||||
eventCount: 0,
|
||||
notes: [],
|
||||
};
|
||||
states.set(e.customerId, s);
|
||||
} else {
|
||||
s = {
|
||||
customerId: e.customerId,
|
||||
customerName: e.customerName,
|
||||
mrr: e.mrr ?? 0,
|
||||
plan: e.plan,
|
||||
renewalAt: e.renewalAt,
|
||||
status: 'active',
|
||||
startedAt: e.timestamp,
|
||||
lastEventAt: e.timestamp,
|
||||
eventCount: 0,
|
||||
notes: [],
|
||||
};
|
||||
states.set(e.customerId, s);
|
||||
}
|
||||
}
|
||||
s.customerName = e.customerName || s.customerName;
|
||||
s.lastEventAt = e.timestamp;
|
||||
s.eventCount += 1;
|
||||
|
||||
switch (e.type) {
|
||||
case 'add':
|
||||
// 위에서 이미 처리 (첫 진입 분기) — 중복 add 면 update 처럼.
|
||||
if (e.mrr !== undefined) s.mrr = e.mrr;
|
||||
if (e.renewalAt) s.renewalAt = e.renewalAt;
|
||||
if (e.plan) s.plan = e.plan;
|
||||
break;
|
||||
case 'update':
|
||||
case 'renew':
|
||||
if (e.mrr !== undefined) s.mrr = e.mrr;
|
||||
if (e.renewalAt) s.renewalAt = e.renewalAt;
|
||||
if (e.plan) s.plan = e.plan;
|
||||
if (e.type === 'renew' && s.status !== 'churned') s.status = 'active';
|
||||
break;
|
||||
case 'risk':
|
||||
if (s.status !== 'churned') s.status = 'at-risk';
|
||||
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'risk', memo: e.memo });
|
||||
break;
|
||||
case 'churn':
|
||||
s.status = 'churned';
|
||||
s.mrr = 0;
|
||||
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'churn', memo: e.memo });
|
||||
break;
|
||||
case 'note':
|
||||
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'note', memo: e.memo });
|
||||
break;
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 고객 피드백 누적 저장소.
|
||||
*
|
||||
* 단일 운영자(대표) 모드에서 슬랙·이메일·CS 채널에 흩어진 고객 피드백을
|
||||
* `/feedback <텍스트>` 한 줄로 모아 둔다. 패턴 분석은 `/feedback summary` 로
|
||||
* LLM 이 누적 데이터를 보고 카테고리 분포 + 반복 주제를 추출.
|
||||
*
|
||||
* 저장 형식: JSON Lines (.jsonl) — 한 줄 = 한 entry. 누적·append-only, 사람이
|
||||
* 직접 편집 가능, grep / 백업 친화. 위치: `<workspace>/.astra/customer-feedback.jsonl`.
|
||||
*
|
||||
* 워크스페이스 폴더가 없으면 저장 불가 (null 반환). 사용자가 `/feedback path` 로
|
||||
* 위치 확인 가능. 민감 정보 포함 가능성 있으므로 외부로 안 보냄 — 로컬 only.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const STORE_REL_PATH = '.astra/customer-feedback.jsonl';
|
||||
|
||||
export interface FeedbackEntry {
|
||||
/** unique id — timestamp 기반 (정렬·dedup 용도). */
|
||||
id: string;
|
||||
/** ISO timestamp of when this entry was captured. */
|
||||
timestamp: string;
|
||||
/** 사용자가 입력한 원본 텍스트 (그대로 보존). */
|
||||
text: string;
|
||||
/** 선택적 출처 — 'slack' / 'email' / 'cs' / 'review' 등. */
|
||||
source?: string;
|
||||
/** LLM 이 부여한 카테고리 (1~3개). */
|
||||
categories?: string[];
|
||||
/** LLM 판정 — 'positive' / 'neutral' / 'negative'. */
|
||||
sentiment?: 'positive' | 'neutral' | 'negative';
|
||||
}
|
||||
|
||||
export function getFeedbackFilePath(): string | null {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (!folders || folders.length === 0) return null;
|
||||
return path.join(folders[0].uri.fsPath, STORE_REL_PATH);
|
||||
}
|
||||
|
||||
export function readFeedback(): FeedbackEntry[] {
|
||||
const filePath = getFeedbackFilePath();
|
||||
if (!filePath || !fs.existsSync(filePath)) return [];
|
||||
const out: FeedbackEntry[] = [];
|
||||
let content = '';
|
||||
try { content = fs.readFileSync(filePath, 'utf-8'); } catch { return []; }
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
const entry = JSON.parse(trimmed);
|
||||
if (entry && typeof entry.id === 'string' && typeof entry.text === 'string') {
|
||||
out.push(entry as FeedbackEntry);
|
||||
}
|
||||
} catch { /* skip malformed line — append-only 라 손상 1줄이 전체 무력화하면 안 됨 */ }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function appendFeedback(entry: FeedbackEntry): { ok: true; filePath: string } | { ok: false; error: string } {
|
||||
const filePath = getFeedbackFilePath();
|
||||
if (!filePath) return { ok: false, error: '워크스페이스 폴더가 없어 저장 불가. VS Code 에서 폴더를 열어 주세요.' };
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.appendFileSync(filePath, JSON.stringify(entry) + '\n', 'utf-8');
|
||||
return { ok: true, filePath };
|
||||
} catch (e: any) {
|
||||
return { ok: false, error: e?.message || String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
/** 누적 항목 수 — 빠른 확인용. */
|
||||
export function countFeedback(): number {
|
||||
const filePath = getFeedbackFilePath();
|
||||
if (!filePath || !fs.existsSync(filePath)) return 0;
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
return content.split('\n').filter((l) => l.trim()).length;
|
||||
} catch { return 0; }
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 채용 파이프라인 트래커.
|
||||
*
|
||||
* 4인 → 5인 이상 확장 시점에 후보자가 여러 명, 여러 역할(개발/기획/디자인) 로
|
||||
* 들어오기 시작 — 노션·스프레드시트·이메일에 흩어진 정보를 한 명령으로 본다.
|
||||
*
|
||||
* Event-sourced (customersStore 와 동일 패턴) — append-only 이벤트 로그를
|
||||
* 재생해 후보자별 현재 단계 + 노트 누적 도출.
|
||||
*
|
||||
* 위치: `<workspace>/.astra/hire.jsonl`. 사람 직접 편집 가능.
|
||||
* 민감 정보(이름, 연봉, 거절 사유) 포함 — 로컬 only.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const STORE_REL_PATH = '.astra/hire.jsonl';
|
||||
|
||||
export type HireEventType = 'add' | 'stage' | 'note' | 'offer' | 'reject' | 'decline' | 'hire';
|
||||
|
||||
/**
|
||||
* 기본 파이프라인 단계. 사용자가 다른 단계명 지정 가능 — 그냥 문자열로 저장.
|
||||
* 표시 순서·정렬용으로 알려진 단계는 가중치 부여.
|
||||
*/
|
||||
export const KNOWN_STAGES = ['inbox', 'screened', 'interview', 'final', 'offer', 'accepted', 'hired', 'rejected', 'declined'] as const;
|
||||
export type KnownStage = typeof KNOWN_STAGES[number];
|
||||
|
||||
export interface HireEvent {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
candidateId: string;
|
||||
candidateName: string;
|
||||
role: string;
|
||||
type: HireEventType;
|
||||
/** stage 전환 시 새 단계. add 시 시작 단계 (기본 'inbox'). */
|
||||
stage?: string;
|
||||
/** offer 의 연봉 — KRW. */
|
||||
salary?: number;
|
||||
/** 입사 예정일 / 거절·이탈 사유. */
|
||||
memo?: string;
|
||||
}
|
||||
|
||||
export interface CandidateState {
|
||||
candidateId: string;
|
||||
candidateName: string;
|
||||
role: string;
|
||||
stage: string;
|
||||
salary?: number;
|
||||
addedAt: string;
|
||||
lastEventAt: string;
|
||||
eventCount: number;
|
||||
notes: { timestamp: string; type: HireEventType; memo: string }[];
|
||||
}
|
||||
|
||||
export function getHireFilePath(): string | null {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (!folders || folders.length === 0) return null;
|
||||
return path.join(folders[0].uri.fsPath, STORE_REL_PATH);
|
||||
}
|
||||
|
||||
export function candidateIdFromName(name: string): string {
|
||||
return name.toLowerCase().trim().replace(/\s+/g, '-').replace(/[^\w가-힣-]/g, '');
|
||||
}
|
||||
|
||||
export function readHireEvents(): HireEvent[] {
|
||||
const filePath = getHireFilePath();
|
||||
if (!filePath || !fs.existsSync(filePath)) return [];
|
||||
const out: HireEvent[] = [];
|
||||
let content = '';
|
||||
try { content = fs.readFileSync(filePath, 'utf-8'); } catch { return []; }
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
const e = JSON.parse(trimmed);
|
||||
if (e && typeof e.id === 'string' && typeof e.candidateId === 'string' && typeof e.type === 'string') {
|
||||
out.push(e as HireEvent);
|
||||
}
|
||||
} catch { /* skip malformed */ }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function appendHireEvent(event: HireEvent): { ok: true; filePath: string } | { ok: false; error: string } {
|
||||
const filePath = getHireFilePath();
|
||||
if (!filePath) return { ok: false, error: '워크스페이스 폴더가 없어 저장 불가.' };
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.appendFileSync(filePath, JSON.stringify(event) + '\n', 'utf-8');
|
||||
return { ok: true, filePath };
|
||||
} catch (e: any) {
|
||||
return { ok: false, error: e?.message || String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
export function computeCandidateStates(): Map<string, CandidateState> {
|
||||
const events = readHireEvents().slice().sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
||||
const states = new Map<string, CandidateState>();
|
||||
for (const e of events) {
|
||||
let s = states.get(e.candidateId);
|
||||
if (!s) {
|
||||
s = {
|
||||
candidateId: e.candidateId,
|
||||
candidateName: e.candidateName,
|
||||
role: e.role || '',
|
||||
stage: e.stage || 'inbox',
|
||||
addedAt: e.timestamp,
|
||||
lastEventAt: e.timestamp,
|
||||
eventCount: 0,
|
||||
notes: [],
|
||||
};
|
||||
states.set(e.candidateId, s);
|
||||
}
|
||||
s.candidateName = e.candidateName || s.candidateName;
|
||||
s.role = e.role || s.role;
|
||||
s.lastEventAt = e.timestamp;
|
||||
s.eventCount += 1;
|
||||
|
||||
switch (e.type) {
|
||||
case 'add':
|
||||
s.stage = e.stage || s.stage || 'inbox';
|
||||
break;
|
||||
case 'stage':
|
||||
if (e.stage) s.stage = e.stage;
|
||||
break;
|
||||
case 'offer':
|
||||
s.stage = 'offer';
|
||||
if (e.salary !== undefined) s.salary = e.salary;
|
||||
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'offer', memo: e.memo });
|
||||
break;
|
||||
case 'hire':
|
||||
s.stage = 'hired';
|
||||
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'hire', memo: e.memo });
|
||||
break;
|
||||
case 'reject':
|
||||
s.stage = 'rejected';
|
||||
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'reject', memo: e.memo });
|
||||
break;
|
||||
case 'decline':
|
||||
s.stage = 'declined';
|
||||
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'decline', memo: e.memo });
|
||||
break;
|
||||
case 'note':
|
||||
if (e.memo) s.notes.push({ timestamp: e.timestamp, type: 'note', memo: e.memo });
|
||||
break;
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Runway / Cash 누적 저장소.
|
||||
*
|
||||
* 4인 기업 운영의 가장 중요한 숫자 — 현금 잔고 / 월 소진율 / 남은 개월수 — 를
|
||||
* 한 명령 (`/runway`) 로 본다. 회계 시스템은 아니고, 대표가 머리에 가지고 있는
|
||||
* "지금 통장에 얼마, 한 달에 얼마 나감" 을 코드 옆에서 잡는 가벼운 트래커.
|
||||
*
|
||||
* 저장 형식: JSON Lines (.jsonl) — 한 줄 = 한 entry. 외부 회계 SaaS 연동 안 함.
|
||||
* 위치: `<workspace>/.astra/runway.jsonl`. 사람이 직접 편집 가능, grep / 백업 친화.
|
||||
*
|
||||
* 민감 정보(현금 잔고) 포함되므로 외부로 안 보냄 — 로컬 only.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const STORE_REL_PATH = '.astra/runway.jsonl';
|
||||
|
||||
export type RunwayEntryType = 'snapshot' | 'expense' | 'revenue' | 'burn';
|
||||
|
||||
export interface RunwayEntry {
|
||||
/** unique id — timestamp 기반. */
|
||||
id: string;
|
||||
/** ISO timestamp. */
|
||||
timestamp: string;
|
||||
/** 항목 종류 — snapshot(잔고) / expense(지출) / revenue(수입) / burn(월 소진율 수동 설정). */
|
||||
type: RunwayEntryType;
|
||||
/** 금액 — KRW 기본 단위, 소수점 허용. */
|
||||
amount: number;
|
||||
/** 통화 — 기본 'KRW'. 추후 'USD' 등 확장 가능. */
|
||||
currency?: string;
|
||||
/** 카테고리 — expense 의 경우 'salary' / 'rent' / 'saas' / 'misc' 등. */
|
||||
category?: string;
|
||||
/** 자유 메모. */
|
||||
memo?: string;
|
||||
}
|
||||
|
||||
export function getRunwayFilePath(): string | null {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (!folders || folders.length === 0) return null;
|
||||
return path.join(folders[0].uri.fsPath, STORE_REL_PATH);
|
||||
}
|
||||
|
||||
export function readRunway(): RunwayEntry[] {
|
||||
const filePath = getRunwayFilePath();
|
||||
if (!filePath || !fs.existsSync(filePath)) return [];
|
||||
const out: RunwayEntry[] = [];
|
||||
let content = '';
|
||||
try { content = fs.readFileSync(filePath, 'utf-8'); } catch { return []; }
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
try {
|
||||
const entry = JSON.parse(trimmed);
|
||||
if (entry && typeof entry.id === 'string' && typeof entry.amount === 'number' && typeof entry.type === 'string') {
|
||||
out.push(entry as RunwayEntry);
|
||||
}
|
||||
} catch { /* skip malformed line */ }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function appendRunway(entry: RunwayEntry): { ok: true; filePath: string } | { ok: false; error: string } {
|
||||
const filePath = getRunwayFilePath();
|
||||
if (!filePath) return { ok: false, error: '워크스페이스 폴더가 없어 저장 불가. VS Code 에서 폴더를 열어 주세요.' };
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.appendFileSync(filePath, JSON.stringify(entry) + '\n', 'utf-8');
|
||||
return { ok: true, filePath };
|
||||
} catch (e: any) {
|
||||
return { ok: false, error: e?.message || String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 상태 계산 — 최신 snapshot, 최근 30일 net burn, 명시적 burn 설정 중 우선순위.
|
||||
*
|
||||
* - latestCash: 가장 최근 'snapshot' entry 의 amount (없으면 null).
|
||||
* - explicitBurn: 가장 최근 'burn' entry — 사용자가 수동 설정한 월 소진율.
|
||||
* - computedBurn: 최근 30일 expense - revenue, 30일 미만이면 일 평균 × 30 으로 보정.
|
||||
* - effectiveBurn: explicitBurn 우선, 없으면 computedBurn.
|
||||
* - runwayMonths: latestCash / effectiveBurn — burn 이 0 이하면 Infinity.
|
||||
*/
|
||||
export interface RunwayStatus {
|
||||
latestCash: number | null;
|
||||
latestCashAt: string | null;
|
||||
explicitBurn: number | null;
|
||||
computedBurn: number | null;
|
||||
effectiveBurn: number | null;
|
||||
runwayMonths: number | null;
|
||||
last30Expense: number;
|
||||
last30Revenue: number;
|
||||
last30Days: number;
|
||||
totalEntries: number;
|
||||
}
|
||||
|
||||
export function computeRunwayStatus(now: Date = new Date()): RunwayStatus {
|
||||
const entries = readRunway();
|
||||
const nowMs = now.getTime();
|
||||
const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
let latestCash: number | null = null;
|
||||
let latestCashAt: string | null = null;
|
||||
let explicitBurn: number | null = null;
|
||||
let last30Expense = 0;
|
||||
let last30Revenue = 0;
|
||||
let oldestRecentMs = nowMs;
|
||||
let hasRecent = false;
|
||||
|
||||
for (const e of entries) {
|
||||
const t = Date.parse(e.timestamp);
|
||||
if (e.type === 'snapshot') {
|
||||
if (!latestCashAt || (Date.parse(e.timestamp) >= Date.parse(latestCashAt))) {
|
||||
latestCash = e.amount;
|
||||
latestCashAt = e.timestamp;
|
||||
}
|
||||
} else if (e.type === 'burn') {
|
||||
if (!explicitBurn || t >= (entries.find(x => x.type === 'burn' && x.amount === explicitBurn)?.timestamp ? Date.parse(e.timestamp) : 0)) {
|
||||
explicitBurn = e.amount;
|
||||
}
|
||||
} else if (e.type === 'expense' && nowMs - t <= thirtyDaysMs) {
|
||||
last30Expense += e.amount;
|
||||
if (t < oldestRecentMs) oldestRecentMs = t;
|
||||
hasRecent = true;
|
||||
} else if (e.type === 'revenue' && nowMs - t <= thirtyDaysMs) {
|
||||
last30Revenue += e.amount;
|
||||
if (t < oldestRecentMs) oldestRecentMs = t;
|
||||
hasRecent = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 최신 burn 정확히 다시 — 위 로직이 꼬여서 단순화.
|
||||
explicitBurn = null;
|
||||
let burnAt = 0;
|
||||
for (const e of entries) {
|
||||
if (e.type !== 'burn') continue;
|
||||
const t = Date.parse(e.timestamp);
|
||||
if (t >= burnAt) { explicitBurn = e.amount; burnAt = t; }
|
||||
}
|
||||
|
||||
const netBurn30 = last30Expense - last30Revenue;
|
||||
let computedBurn: number | null = null;
|
||||
let last30Days = 0;
|
||||
if (hasRecent) {
|
||||
const span = Math.max(1, Math.ceil((nowMs - oldestRecentMs) / (24 * 60 * 60 * 1000)));
|
||||
last30Days = Math.min(30, span);
|
||||
// 30일 미만이면 일 평균 × 30 으로 환산.
|
||||
if (last30Days < 30) computedBurn = (netBurn30 / last30Days) * 30;
|
||||
else computedBurn = netBurn30;
|
||||
}
|
||||
|
||||
const effectiveBurn = explicitBurn ?? computedBurn;
|
||||
let runwayMonths: number | null = null;
|
||||
if (latestCash !== null && effectiveBurn !== null && effectiveBurn > 0) {
|
||||
runwayMonths = latestCash / effectiveBurn;
|
||||
} else if (latestCash !== null && effectiveBurn !== null && effectiveBurn <= 0) {
|
||||
runwayMonths = Infinity;
|
||||
}
|
||||
|
||||
return {
|
||||
latestCash,
|
||||
latestCashAt,
|
||||
explicitBurn,
|
||||
computedBurn,
|
||||
effectiveBurn,
|
||||
runwayMonths,
|
||||
last30Expense,
|
||||
last30Revenue,
|
||||
last30Days,
|
||||
totalEntries: entries.length,
|
||||
};
|
||||
}
|
||||
@@ -15,6 +15,14 @@ import {
|
||||
mapWeightToRetrievalRatio,
|
||||
ResolvedKnowledgeMix,
|
||||
} from '../../retrieval/knowledgeMix';
|
||||
import { buildConflictWarningsBlock, ConflictThresholdSetting } from '../../retrieval/conflictBlock';
|
||||
import { buildCoveChecklistBlock } from '../../retrieval/coveBlock';
|
||||
import { captureWorkStateSignals } from '../../retrieval/actionabilityScoring';
|
||||
import { getRecentSlashCommands } from '../../features/datacollect/slashRouter';
|
||||
import { semanticRerank, DEFAULT_SEMANTIC_RERANK_OPTIONS } from '../../retrieval/semanticRerank';
|
||||
import { detectAmbiguity, buildIntentClarificationBlock, IntentStrictness } from '../../retrieval/intentClarification';
|
||||
import { buildCitationTraceBlock } from '../../retrieval/citationTrace';
|
||||
import { buildTerminologyBlock } from '../../retrieval/terminologyBlock';
|
||||
|
||||
/**
|
||||
* 한 turn 의 RAG / 5-layer memory 컨텍스트 빌드.
|
||||
@@ -54,6 +62,29 @@ export interface TurnContextSink {
|
||||
retrieval: TurnRetrievalSummary | null;
|
||||
lessons: string[];
|
||||
knowledgeMix: ResolvedKnowledgeMix | null;
|
||||
/**
|
||||
* [CONFLICT WARNINGS] 시스템 프롬프트 블록 — 빈 문자열이면 충돌 없음.
|
||||
* buildAstraModeSystemPrompt 가 직접 prompt 에 주입.
|
||||
*/
|
||||
conflictWarnings: string;
|
||||
/**
|
||||
* [VERIFICATION CHECKLIST] Chain-of-Verification 블록 — 빈 문자열이면 CoVe 비활성/근거 없음.
|
||||
* 모델이 답변 *작성 전* 그라운딩 체크하도록 instructional prompt 주입.
|
||||
*/
|
||||
coveChecklist: string;
|
||||
/**
|
||||
* [INTENT CLARIFICATION GUIDANCE] 블록 — 질의가 모호한 차원이 감지된 경우 LLM 에게
|
||||
* 답변보다 *역질문* 우선 지시. 빈 문자열이면 모호 차원 없음 또는 disable.
|
||||
*/
|
||||
intentClarification: string;
|
||||
/**
|
||||
* [CITATION TRACE] 블록 — 답변 끝에 사용 출처 한 줄 정리 지시. 검색 결과 있을 때만.
|
||||
*/
|
||||
citationTrace: string;
|
||||
/** Post-hoc Self-Check 용 — selected chunks 의 (title, excerpt) 요약. */
|
||||
selfCheckSources: Array<{ title: string; excerpt: string }>;
|
||||
/** [TERMINOLOGY DICTIONARY] 시스템 프롬프트 블록 — 글로서리 파일 있을 때만 채워짐. */
|
||||
terminology: string;
|
||||
}
|
||||
|
||||
export interface MemoryContextDeps {
|
||||
@@ -163,6 +194,12 @@ export async function buildMemoryContext(deps: MemoryContextDeps): Promise<strin
|
||||
const mixedBrainFileLimit = mapWeightToBrainFileLimit(knowledgeMix.weight, config.memoryLongTermFiles);
|
||||
const mixedRetrievalRatio = mapWeightToRetrievalRatio(knowledgeMix.weight);
|
||||
|
||||
// Actionability — work-state 신호 캡처 (최근 슬래시 명령 + 열린 파일).
|
||||
// 설정으로 disable 가능. 신호 없으면 retrieve() 가 legacy 동작.
|
||||
const workStateSignals = config.actionabilityEnabled !== false
|
||||
? captureWorkStateSignals(getRecentSlashCommands())
|
||||
: undefined;
|
||||
|
||||
// Unified RAG Pipeline 호출.
|
||||
const result = deps.retrievalOrchestrator.retrieve(deps.currentPrompt, {
|
||||
brain: deps.activeBrain,
|
||||
@@ -180,8 +217,29 @@ export async function buildMemoryContext(deps: MemoryContextDeps): Promise<strin
|
||||
queryEmbedding,
|
||||
embeddingModel: config.embeddingModel || undefined,
|
||||
embeddingBlendAlpha: config.embeddingBlendAlpha,
|
||||
workStateSignals,
|
||||
hierarchicalReweightEnabled: config.hierarchicalReweightEnabled !== false,
|
||||
});
|
||||
|
||||
// Semantic Re-rank (LLM, async) — selectedChunks 의 *순서* 만 재배치. 토큰 예산을
|
||||
// 통과한 chunks 안에서 의도-부합도 순으로 재정렬해 LLM attention bias 활용.
|
||||
// 기본 OFF — latency 우려. 사용자가 명시 enable 시만.
|
||||
if (config.semanticRerankEnabled && result.selectedChunks.length >= 3) {
|
||||
const rerankModel = (config.semanticRerankModel || '').trim() || config.defaultModel;
|
||||
if (rerankModel && config.ollamaUrl) {
|
||||
const rerankRes = await semanticRerank(deps.currentPrompt, result.selectedChunks, {
|
||||
ollamaUrl: config.ollamaUrl,
|
||||
model: rerankModel,
|
||||
candidateK: config.semanticRerankCandidateK ?? DEFAULT_SEMANTIC_RERANK_OPTIONS.candidateK,
|
||||
timeoutMs: (config.semanticRerankTimeoutSec ?? 8) * 1000,
|
||||
excerptLength: DEFAULT_SEMANTIC_RERANK_OPTIONS.excerptLength,
|
||||
});
|
||||
// In-place 교체 — buildContextString 가 이 배열을 그대로 읽음.
|
||||
result.selectedChunks = rerankRes.rerankedChunks;
|
||||
result.fusionLog.push(`Semantic re-rank: ${rerankRes.success ? '✓' : '✗'} ${rerankRes.note} (${rerankRes.durationMs}ms)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fire-and-forget background embedding. Vector 없는 파일만 embed 하므로
|
||||
// steady-state turn 은 작업량 0 — 다음 turn 이 혜택.
|
||||
if (config.embeddingModel) {
|
||||
@@ -225,6 +283,67 @@ export async function buildMemoryContext(deps: MemoryContextDeps): Promise<strin
|
||||
};
|
||||
|
||||
deps.turnCtx.lessons = lessonChunks.map((c) => c.content);
|
||||
|
||||
// Conflict Surface — selectedChunks 의 per-doc conflictSeverity 신호 + 교차-문서
|
||||
// 발산 후보를 LLM 에 노출. 블록은 [CONTEXT] *밖*에 주입돼 token-truncation 보호.
|
||||
// 설정으로 disable 가능 — 기본 켜져 있음 (v4 정책이 이미 CONFLICT WARNING 플래그 참조).
|
||||
if (config.conflictHighlightingEnabled !== false) {
|
||||
const threshold: ConflictThresholdSetting = (config.conflictSeverityThreshold || 'medium') as ConflictThresholdSetting;
|
||||
deps.turnCtx.conflictWarnings = buildConflictWarningsBlock(result.selectedChunks, {
|
||||
selfFlagThreshold: threshold,
|
||||
crossDivergenceEnabled: config.conflictCrossDocEnabled !== false,
|
||||
});
|
||||
} else {
|
||||
deps.turnCtx.conflictWarnings = '';
|
||||
}
|
||||
|
||||
// CoVe (Chain-of-Verification) — 답변 *작성 전* 그라운딩 체크리스트를 시스템 프롬프트에
|
||||
// 주입. 모델이 한 패스 안에서 self-verify. Conflict Surface 와 보완 관계 — 충돌
|
||||
// 데이터를 *어떻게* verify 할지 지시.
|
||||
if (config.coveEnabled !== false) {
|
||||
deps.turnCtx.coveChecklist = buildCoveChecklistBlock(result.selectedChunks, deps.currentPrompt, {
|
||||
topSourcesCount: config.coveTopSourcesCount ?? 5,
|
||||
strictMode: config.coveStrictMode === true,
|
||||
});
|
||||
} else {
|
||||
deps.turnCtx.coveChecklist = '';
|
||||
}
|
||||
|
||||
// Intent Clarification — 모호 차원 감지 시 *역질문 우선* 지시. CoVe / Citation 과
|
||||
// 동일 패턴: instructional system prompt block.
|
||||
if (config.intentClarificationEnabled !== false) {
|
||||
const strict = (config.intentClarificationStrictness || 'medium') as IntentStrictness;
|
||||
const ambig = detectAmbiguity(deps.currentPrompt, strict);
|
||||
deps.turnCtx.intentClarification = buildIntentClarificationBlock(ambig);
|
||||
} else {
|
||||
deps.turnCtx.intentClarification = '';
|
||||
}
|
||||
|
||||
// Citation Trace — 답변 끝에 출처 한 줄 명시 지시. CoVe Strict 의 가벼운 형제.
|
||||
// 검색 결과가 있을 때만 의미 있음.
|
||||
if (config.citationTraceEnabled !== false && result.selectedChunks.length > 0) {
|
||||
deps.turnCtx.citationTrace = buildCitationTraceBlock(result.selectedChunks);
|
||||
} else {
|
||||
deps.turnCtx.citationTrace = '';
|
||||
}
|
||||
|
||||
// Self-Check 용 source 미리보기 — agent.ts 가 post-stream 에서 사용.
|
||||
deps.turnCtx.selfCheckSources = result.selectedChunks.slice(0, 5).map((c) => ({
|
||||
title: c.title || '(제목 없음)',
|
||||
excerpt: (c.content || '').slice(0, 200),
|
||||
}));
|
||||
|
||||
// Terminology Dictionary — 사용자 편집 글로서리 파일을 시스템 프롬프트 블록으로 주입.
|
||||
// 파일 없으면 빈 문자열 (no-op). 캐시 + mtime 체크로 매 turn 디스크 read 최소화.
|
||||
if (config.glossaryEnabled !== false) {
|
||||
deps.turnCtx.terminology = buildTerminologyBlock({
|
||||
relPath: config.glossaryPath || '.astra/glossary.md',
|
||||
maxBodyLength: config.glossaryMaxBodyLength ?? 4000,
|
||||
});
|
||||
} else {
|
||||
deps.turnCtx.terminology = '';
|
||||
}
|
||||
|
||||
// Lessons 블록은 일반 RAG context 보다 앞 — context-overflow truncation 에서 먼저
|
||||
// 살아남게.
|
||||
const lessonBlock = buildLessonChecklistBlock(lessonChunks.map((c) => ({ title: c.title, content: c.content })));
|
||||
|
||||
@@ -130,7 +130,12 @@ export class EpisodicMemory {
|
||||
* 프롬프트와 관련된 에피소드를 검색합니다.
|
||||
*/
|
||||
public findRelevantEpisodes(prompt: string, limit = 3): EpisodicEntry[] {
|
||||
const episodes = this.loadAllEpisodes();
|
||||
// Temporal + Distillation 필터: 만료된 episode 와 LongTerm 으로 이미 promote 된
|
||||
// episode 는 검색에서 제외 (digest 가 LongTerm 에 있으니 중복 노출 방지).
|
||||
const now = Date.now();
|
||||
const episodes = this.loadAllEpisodes()
|
||||
.filter((ep) => !ep.expiresAt || ep.expiresAt > now)
|
||||
.filter((ep) => !ep.promoted);
|
||||
const promptLower = prompt.toLowerCase();
|
||||
const terms = promptLower
|
||||
.split(/[^a-z0-9가-힣_]+/g)
|
||||
@@ -276,6 +281,45 @@ export class EpisodicMemory {
|
||||
.map(([word]) => word);
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 episode 의 promoted 플래그를 true 로 마킹하고 LongTerm digest id 를 기록.
|
||||
* Distillation Loop 가 호출. 파일 rewrite 1회.
|
||||
*/
|
||||
public markPromoted(episodeId: string, longTermDigestId: string): boolean {
|
||||
const episodes = this.loadAllEpisodes();
|
||||
const ep = episodes.find((e) => e.id === episodeId);
|
||||
if (!ep) return false;
|
||||
ep.promoted = true;
|
||||
ep.promotedToLongTermId = longTermDigestId;
|
||||
// Find the file holding this episode and rewrite.
|
||||
try {
|
||||
const files = fs.readdirSync(this.episodeDir).filter((f) => f.endsWith('.json'));
|
||||
for (const file of files) {
|
||||
const full = path.join(this.episodeDir, file);
|
||||
try {
|
||||
const raw = fs.readFileSync(full, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as EpisodicEntry;
|
||||
if (parsed.id === episodeId) {
|
||||
parsed.promoted = true;
|
||||
parsed.promotedToLongTermId = longTermDigestId;
|
||||
fs.writeFileSync(full, JSON.stringify(parsed, null, 2), 'utf-8');
|
||||
this._episodeCache = null; // dir mtime bump → cache 다음 호출에 갱신
|
||||
return true;
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Distillation 후보 — 지정 일수보다 오래되고 아직 promoted 되지 않은 episodes. */
|
||||
public findStaleEpisodes(ageThresholdDays: number): EpisodicEntry[] {
|
||||
const cutoff = Date.now() - ageThresholdDays * 24 * 60 * 60 * 1000;
|
||||
return this.loadAllEpisodes()
|
||||
.filter((ep) => !ep.promoted)
|
||||
.filter((ep) => ep.timestamp < cutoff);
|
||||
}
|
||||
|
||||
private pruneOldEpisodes(): void {
|
||||
try {
|
||||
const files = fs.readdirSync(this.episodeDir)
|
||||
|
||||
@@ -53,7 +53,13 @@ export class LongTermMemory {
|
||||
|
||||
// ─── CRUD ───
|
||||
|
||||
public addEntry(category: LongTermCategory, content: string, source: string, confidence = 0.8): LongTermEntry {
|
||||
public addEntry(
|
||||
category: LongTermCategory,
|
||||
content: string,
|
||||
source: string,
|
||||
confidence = 0.8,
|
||||
opts: { expiresAt?: number } = {},
|
||||
): LongTermEntry {
|
||||
const entry: LongTermEntry = {
|
||||
id: crypto.randomUUID(),
|
||||
category,
|
||||
@@ -62,7 +68,8 @@ export class LongTermMemory {
|
||||
confidence,
|
||||
createdAt: Date.now(),
|
||||
lastReferencedAt: Date.now(),
|
||||
referenceCount: 0
|
||||
referenceCount: 0,
|
||||
...(opts.expiresAt ? { expiresAt: opts.expiresAt } : {}),
|
||||
};
|
||||
this.store.entries.push(entry);
|
||||
// Enforce the retention cap — drop the oldest entries (by createdAt) once
|
||||
@@ -87,12 +94,32 @@ export class LongTermMemory {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getAllEntries(): LongTermEntry[] {
|
||||
return [...this.store.entries];
|
||||
/** 만료된 entry 를 필터링한 활성 entries (검색·context build 가 보는 것). */
|
||||
private getActiveEntries(): LongTermEntry[] {
|
||||
const now = Date.now();
|
||||
return this.store.entries.filter((e) => !e.expiresAt || e.expiresAt > now);
|
||||
}
|
||||
|
||||
public getAllEntries(opts: { includeExpired?: boolean } = {}): LongTermEntry[] {
|
||||
return opts.includeExpired ? [...this.store.entries] : this.getActiveEntries();
|
||||
}
|
||||
|
||||
public getEntriesByCategory(category: LongTermCategory): LongTermEntry[] {
|
||||
return this.store.entries.filter((e) => e.category === category);
|
||||
return this.getActiveEntries().filter((e) => e.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 entry 의 expiresAt 갱신. id 또는 id prefix (8자 이상) 로 식별.
|
||||
* 반환: 갱신된 entry 또는 null (못 찾음).
|
||||
*/
|
||||
public setExpiration(idOrPrefix: string, expiresAt: number): LongTermEntry | null {
|
||||
const match = this.store.entries.find((e) => e.id === idOrPrefix)
|
||||
|| (idOrPrefix.length >= 8 ? this.store.entries.find((e) => e.id.startsWith(idOrPrefix)) : undefined);
|
||||
if (!match) return null;
|
||||
match.expiresAt = expiresAt;
|
||||
this.dirty = true;
|
||||
this.save();
|
||||
return match;
|
||||
}
|
||||
|
||||
// ─── Context Building ───
|
||||
@@ -101,7 +128,9 @@ export class LongTermMemory {
|
||||
* 프롬프트와 관련성이 높은 Long-Term 기억을 반환합니다.
|
||||
*/
|
||||
public buildContext(currentPrompt: string, maxEntries = 10): MemoryContextResult | null {
|
||||
if (this.store.entries.length === 0) return null;
|
||||
// 만료된 entry 는 검색에서 자동 제외 — Temporal Markers 의 핵심.
|
||||
const activeEntries = this.getActiveEntries();
|
||||
if (activeEntries.length === 0) return null;
|
||||
|
||||
const promptLower = currentPrompt.toLowerCase();
|
||||
const terms = promptLower
|
||||
@@ -109,7 +138,7 @@ export class LongTermMemory {
|
||||
.filter((t) => t.length >= 2);
|
||||
|
||||
// Score entries by relevance to prompt
|
||||
const scored = this.store.entries.map((entry) => {
|
||||
const scored = activeEntries.map((entry) => {
|
||||
let score = 0;
|
||||
const contentLower = entry.content.toLowerCase();
|
||||
|
||||
@@ -134,8 +163,8 @@ export class LongTermMemory {
|
||||
.slice(0, maxEntries);
|
||||
|
||||
if (relevant.length === 0) {
|
||||
// Still include all rules and goals even without prompt match
|
||||
const alwaysInclude = this.store.entries
|
||||
// Still include all rules and goals even without prompt match — 만료 제외.
|
||||
const alwaysInclude = activeEntries
|
||||
.filter((e) => e.category === 'rule' || e.category === 'goal')
|
||||
.slice(0, 5);
|
||||
if (alwaysInclude.length === 0) return null;
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Distillation Loop — stale Episodic Memory → Long-Term "episode-digest" 승급.
|
||||
*
|
||||
* 배경: Episodic Memory 가 무한히 누적되면 검색 노이즈. 30일+ 지난 에피소드는
|
||||
* "지금 이 순간 관련 가능성" 보다 "역사적 패턴" 가치가 커서, 디테일을 압축해
|
||||
* Long-Term 으로 옮기고 원본은 archive 하는 게 효율적.
|
||||
*
|
||||
* v1 설계 (LLM-less, 예측 가능):
|
||||
* - LLM 호출 없이 기존 EpisodicEntry 의 title/summary/keyDecisions/topics 를
|
||||
* 구조적으로 결합해 LongTerm 'episode-digest' content 생성
|
||||
* - 장점: 비용 0, 결정적·재현 가능, LM Studio 다운 시에도 동작
|
||||
* - 단점: LLM 요약보다 농축도 낮음 — 추후 strict 모드에서 LLM 패스 추가 가능
|
||||
*
|
||||
* 원본 episode 처리: 두 가지 옵션 — 사용자 설정으로 결정.
|
||||
* - 'mark-promoted' (기본): promoted=true 마킹만, 파일 보존. 검색에서 제외되나
|
||||
* 히스토리·디버깅용으로 디스크에 남음.
|
||||
* - 'archive-file': promoted 마킹 + 파일을 memory/episodes/archive/ 로 이동.
|
||||
* 디스크 정리에 더 깔끔하나 복구 시 수동.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { EpisodicMemory } from './EpisodicMemory';
|
||||
import { LongTermMemory } from './LongTermMemory';
|
||||
import { EpisodicEntry } from './types';
|
||||
|
||||
export type DistillationArchiveMode = 'mark-promoted' | 'archive-file';
|
||||
|
||||
export interface DistillationOptions {
|
||||
/** 며칠 이상 지난 episode 를 대상으로. 기본 30. */
|
||||
ageThresholdDays: number;
|
||||
/** Archive 처리 방식. 기본 'mark-promoted'. */
|
||||
archiveMode: DistillationArchiveMode;
|
||||
/** 한 번에 처리할 최대 episode 수 (안전장치). 기본 50. */
|
||||
maxBatchSize: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_DISTILLATION_OPTIONS: DistillationOptions = {
|
||||
ageThresholdDays: 30,
|
||||
archiveMode: 'mark-promoted',
|
||||
maxBatchSize: 50,
|
||||
};
|
||||
|
||||
export interface DistillationReport {
|
||||
candidateCount: number;
|
||||
promotedCount: number;
|
||||
archivedCount: number;
|
||||
longTermDigestIds: string[];
|
||||
skipped: { episodeId: string; reason: string }[];
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Episode → LongTerm 'episode-digest' content 변환. 결정적·LLM 없음.
|
||||
*/
|
||||
function episodeToDigestContent(ep: EpisodicEntry): string {
|
||||
const date = new Date(ep.timestamp).toISOString().slice(0, 10);
|
||||
const parts: string[] = [];
|
||||
parts.push(`[${date}] ${ep.title}`);
|
||||
if (ep.summary && ep.summary.trim()) parts.push(`요약: ${ep.summary.trim()}`);
|
||||
if (ep.keyDecisions && ep.keyDecisions.length > 0) {
|
||||
parts.push(`결정: ${ep.keyDecisions.slice(0, 5).join(' · ')}`);
|
||||
}
|
||||
if (ep.topics && ep.topics.length > 0) {
|
||||
parts.push(`토픽: ${ep.topics.slice(0, 8).join(', ')}`);
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Distillation 실행 — stale episodes 를 LongTerm digest 로 승급 + archive.
|
||||
*
|
||||
* 호출자: `/memory distill` 슬래시 명령 + 세션 종료 시 auto-trigger (선택).
|
||||
*/
|
||||
export function distillStaleEpisodes(
|
||||
episodicMemory: EpisodicMemory,
|
||||
longTermMemory: LongTermMemory,
|
||||
brainPath: string,
|
||||
options: Partial<DistillationOptions> = {},
|
||||
): DistillationReport {
|
||||
const opts: DistillationOptions = { ...DEFAULT_DISTILLATION_OPTIONS, ...options };
|
||||
const start = Date.now();
|
||||
const report: DistillationReport = {
|
||||
candidateCount: 0,
|
||||
promotedCount: 0,
|
||||
archivedCount: 0,
|
||||
longTermDigestIds: [],
|
||||
skipped: [],
|
||||
durationMs: 0,
|
||||
};
|
||||
|
||||
const candidates = episodicMemory.findStaleEpisodes(opts.ageThresholdDays).slice(0, opts.maxBatchSize);
|
||||
report.candidateCount = candidates.length;
|
||||
|
||||
if (candidates.length === 0) {
|
||||
report.durationMs = Date.now() - start;
|
||||
return report;
|
||||
}
|
||||
|
||||
const archiveDir = path.join(brainPath, 'memory', 'episodes', 'archive');
|
||||
if (opts.archiveMode === 'archive-file') {
|
||||
try { fs.mkdirSync(archiveDir, { recursive: true }); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
for (const ep of candidates) {
|
||||
try {
|
||||
// 1. LongTerm digest entry 생성. confidence 약간 낮춤 (압축 손실 반영).
|
||||
const digestContent = episodeToDigestContent(ep);
|
||||
const digest = longTermMemory.addEntry(
|
||||
'episode-digest',
|
||||
digestContent,
|
||||
`episodic:${ep.id}`,
|
||||
0.7,
|
||||
);
|
||||
report.longTermDigestIds.push(digest.id);
|
||||
|
||||
// 2. 원본 episode 처리.
|
||||
const marked = episodicMemory.markPromoted(ep.id, digest.id);
|
||||
if (!marked) {
|
||||
report.skipped.push({ episodeId: ep.id, reason: 'markPromoted failed (file not found)' });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (opts.archiveMode === 'archive-file') {
|
||||
// 파일 위치 찾아서 archive 디렉터리로 이동.
|
||||
const moved = tryMoveEpisodeFileToArchive(ep.id, path.join(brainPath, 'memory', 'episodes'), archiveDir);
|
||||
if (moved) report.archivedCount++;
|
||||
}
|
||||
report.promotedCount++;
|
||||
} catch (e: any) {
|
||||
report.skipped.push({ episodeId: ep.id, reason: e?.message || String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
report.durationMs = Date.now() - start;
|
||||
return report;
|
||||
}
|
||||
|
||||
function tryMoveEpisodeFileToArchive(episodeId: string, episodeDir: string, archiveDir: string): boolean {
|
||||
try {
|
||||
const files = fs.readdirSync(episodeDir).filter((f) => f.endsWith('.json'));
|
||||
for (const file of files) {
|
||||
const full = path.join(episodeDir, file);
|
||||
try {
|
||||
const raw = fs.readFileSync(full, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as EpisodicEntry;
|
||||
if (parsed.id === episodeId) {
|
||||
fs.renameSync(full, path.join(archiveDir, file));
|
||||
return true;
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Distillation 마지막 실행 시각을 저장·조회 — 자동 트리거가 *너무 자주* 안 돌도록.
|
||||
* brainPath 의 marker 파일 사용 (vscode.globalState 안 쓰는 이유: 메모리 인프라가
|
||||
* BrainProfile-scoped 라 brain 디렉터리에 두는 게 일관성 있음).
|
||||
*/
|
||||
const MARKER_FILE = 'distillation_last_run.json';
|
||||
|
||||
export interface DistillationMarker {
|
||||
timestamp: number;
|
||||
report?: Partial<DistillationReport>;
|
||||
}
|
||||
|
||||
export function getLastDistillationRun(brainPath: string): DistillationMarker | null {
|
||||
try {
|
||||
const fp = path.join(brainPath, 'memory', MARKER_FILE);
|
||||
if (!fs.existsSync(fp)) return null;
|
||||
return JSON.parse(fs.readFileSync(fp, 'utf-8')) as DistillationMarker;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export function recordDistillationRun(brainPath: string, report: DistillationReport): void {
|
||||
try {
|
||||
const dir = path.join(brainPath, 'memory');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const marker: DistillationMarker = {
|
||||
timestamp: Date.now(),
|
||||
report: {
|
||||
candidateCount: report.candidateCount,
|
||||
promotedCount: report.promotedCount,
|
||||
archivedCount: report.archivedCount,
|
||||
durationMs: report.durationMs,
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(path.join(dir, MARKER_FILE), JSON.stringify(marker, null, 2), 'utf-8');
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
/** 자동 트리거 게이트 — 마지막 실행 후 N일 경과 시 true. */
|
||||
export function shouldAutoDistill(brainPath: string, intervalDays: number): boolean {
|
||||
const last = getLastDistillationRun(brainPath);
|
||||
if (!last) return true;
|
||||
const elapsed = (Date.now() - last.timestamp) / (1000 * 60 * 60 * 24);
|
||||
return elapsed >= intervalDays;
|
||||
}
|
||||
+39
-2
@@ -20,6 +20,12 @@ import { ProceduralMemory } from './ProceduralMemory';
|
||||
import { EpisodicMemory } from './EpisodicMemory';
|
||||
import { MemoryExtractor } from './MemoryExtractor';
|
||||
import { MemoryContextResult, MemoryConfig } from './types';
|
||||
import {
|
||||
distillStaleEpisodes,
|
||||
shouldAutoDistill,
|
||||
recordDistillationRun,
|
||||
type DistillationArchiveMode,
|
||||
} from './distillation';
|
||||
|
||||
export { ShortTermMemory } from './ShortTermMemory';
|
||||
export { LongTermMemory } from './LongTermMemory';
|
||||
@@ -27,6 +33,17 @@ export { ProjectMemory } from './ProjectMemory';
|
||||
export { ProceduralMemory } from './ProceduralMemory';
|
||||
export { EpisodicMemory } from './EpisodicMemory';
|
||||
export { MemoryExtractor } from './MemoryExtractor';
|
||||
export {
|
||||
distillStaleEpisodes,
|
||||
getLastDistillationRun,
|
||||
recordDistillationRun,
|
||||
shouldAutoDistill,
|
||||
DEFAULT_DISTILLATION_OPTIONS,
|
||||
type DistillationOptions,
|
||||
type DistillationReport,
|
||||
type DistillationMarker,
|
||||
type DistillationArchiveMode,
|
||||
} from './distillation';
|
||||
export * from './types';
|
||||
|
||||
export class MemoryManager {
|
||||
@@ -132,11 +149,18 @@ export class MemoryManager {
|
||||
public onSessionEnd(
|
||||
sessionId: string,
|
||||
messages: Array<{ role: string; content: string; timestamp?: number }>,
|
||||
workspacePath?: string
|
||||
workspacePath?: string,
|
||||
distillationOpts?: {
|
||||
enabled: boolean;
|
||||
ageThresholdDays: number;
|
||||
intervalDays: number;
|
||||
archiveMode: DistillationArchiveMode;
|
||||
brainPath: string;
|
||||
},
|
||||
): void {
|
||||
if (!this.config.enabled) return;
|
||||
|
||||
const projectMemory = workspacePath
|
||||
const projectMemory = workspacePath
|
||||
? this.getProjectMemory(workspacePath)
|
||||
: null;
|
||||
|
||||
@@ -153,6 +177,19 @@ export class MemoryManager {
|
||||
|
||||
// Persist long-term memory
|
||||
this.longTerm.save();
|
||||
|
||||
// Auto-distillation — Distillation Loop 가 enabled 이고 interval 충족 시 stale
|
||||
// episodes 를 LongTerm digest 로 승급. 세션 종료 시점이 자연스러움 — 사용자가
|
||||
// 다음 세션 시작 전 한 번 cleanup.
|
||||
if (distillationOpts?.enabled && shouldAutoDistill(distillationOpts.brainPath, distillationOpts.intervalDays)) {
|
||||
try {
|
||||
const report = distillStaleEpisodes(this.episodic, this.longTerm, distillationOpts.brainPath, {
|
||||
ageThresholdDays: distillationOpts.ageThresholdDays,
|
||||
archiveMode: distillationOpts.archiveMode,
|
||||
});
|
||||
recordDistillationRun(distillationOpts.brainPath, report);
|
||||
} catch { /* distillation should never break session end */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Direct Access (for UI & advanced features) ───
|
||||
|
||||
+26
-1
@@ -28,7 +28,12 @@ export interface ShortTermMessage {
|
||||
|
||||
// ─── ② Long-Term Memory ───
|
||||
|
||||
export type LongTermCategory = 'preference' | 'rule' | 'decision' | 'goal';
|
||||
/**
|
||||
* Long-term category.
|
||||
* - 'episode-digest' 는 Distillation Loop 가 stale episodic memory 를 long-term 으로
|
||||
* 승급시킬 때 사용. 사용자가 직접 만드는 'decision' / 'rule' 등과 시각적으로 구분.
|
||||
*/
|
||||
export type LongTermCategory = 'preference' | 'rule' | 'decision' | 'goal' | 'episode-digest';
|
||||
|
||||
export interface LongTermEntry {
|
||||
id: string;
|
||||
@@ -39,6 +44,14 @@ export interface LongTermEntry {
|
||||
createdAt: number;
|
||||
lastReferencedAt: number;
|
||||
referenceCount: number;
|
||||
/**
|
||||
* Temporal marker — 이 사실이 *유효한 마지막 시점* (epoch ms).
|
||||
* 검색·context build 단계에서 expiresAt < now 인 entry 는 자동 제외.
|
||||
* undefined 면 영구 유효 (legacy 동작).
|
||||
*
|
||||
* 사용 예: "Q3 2026 마케팅 계획은 9월 30일까지만 유효" → expiresAt = 2026-09-30 epoch.
|
||||
*/
|
||||
expiresAt?: number;
|
||||
}
|
||||
|
||||
export interface LongTermStore {
|
||||
@@ -105,6 +118,18 @@ export interface EpisodicEntry {
|
||||
timestamp: number;
|
||||
duration: number; // 세션 길이 (ms)
|
||||
messageCount: number;
|
||||
/**
|
||||
* Temporal marker — 에피소드의 *유효 마지막 시점* (epoch ms). 검색에서 자동 제외.
|
||||
* undefined 면 영구 (Distillation 이 archive 할 때까지).
|
||||
*/
|
||||
expiresAt?: number;
|
||||
/**
|
||||
* Distillation Loop 가 이 episode 를 LongTerm digest 로 promote 했음을 표시.
|
||||
* promoted=true 면 검색·context build 에서 제외 (LongTerm 에 digest 가 있으니).
|
||||
*/
|
||||
promoted?: boolean;
|
||||
/** promoted 인 경우 — 생성된 LongTerm digest entry id (역참조용). */
|
||||
promotedToLongTermId?: string;
|
||||
}
|
||||
|
||||
export interface EpisodicStore {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Citation Trace — 답변 *끝* 에 "출처:" 한 줄 명시 지시.
|
||||
*
|
||||
* CoVe Strict 모드 (v2.2.184) 와 차이:
|
||||
* - CoVe Strict: 모든 사실 주장 뒤에 inline `[S1]` 인용 강제 — verbose, 학술적
|
||||
* - Citation Trace: 답변 끝에 *사용된 출처* 한 줄 정리 — 가벼움, 항상 ON 권장
|
||||
*
|
||||
* 둘은 함께 동작 가능. CoVe 가 [S1]..[SN] 라벨을 system prompt 에 노출하면,
|
||||
* Citation Trace 는 LLM 에게 "그 라벨들 중 답변에 *실제로 사용된* 것을 끝에 한 줄
|
||||
* 정리" 라고 지시.
|
||||
*
|
||||
* 효과: 사용자가 답변 검증 가능 — "이 답변이 어느 출처에 기반했나" 명시.
|
||||
* 할루시네이션 억제 — LLM 이 출처 없는 주장 줄임.
|
||||
*
|
||||
* 비용: 시스템 프롬프트 ~10줄 추가. LLM 출력에 1줄 추가.
|
||||
*/
|
||||
|
||||
import { RetrievalChunk } from './types';
|
||||
|
||||
export interface CitationTraceOptions {
|
||||
/** 답변 끝 *출처 한 줄* 형식. 'tail' 만 v1 지원. */
|
||||
format: 'tail';
|
||||
}
|
||||
|
||||
/**
|
||||
* Citation Trace 블록 — chunks 가 *있어야* 의미 있으니 비어 있으면 빈 문자열.
|
||||
* Casual conversation 모드는 호출자가 미리 걸러야.
|
||||
*/
|
||||
export function buildCitationTraceBlock(
|
||||
chunks: RetrievalChunk[],
|
||||
options: Partial<CitationTraceOptions> = {},
|
||||
): string {
|
||||
if (!chunks || chunks.length === 0) return '';
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push('[CITATION TRACE]');
|
||||
lines.push('답변에서 *검색된 출처를 사용했다면*, 답변 끝에 다음 형식으로 *한 줄* 정리:');
|
||||
lines.push('');
|
||||
lines.push('*출처:* `파일명.md` · `chunk-title` · `chunk-title2`');
|
||||
lines.push('');
|
||||
lines.push('[규칙]');
|
||||
lines.push('1. 실제 답변 작성에 *사용한* 출처만 나열. 검색됐지만 안 쓴 출처는 제외.');
|
||||
lines.push('2. 출처 라벨은 파일명(있으면) 또는 chunk title 그대로 — 임의 변형 금지.');
|
||||
lines.push('3. 일반 모델 지식만 사용했다면: *출처: 모델 지식 (검색 출처 미사용)*');
|
||||
lines.push('4. 답변이 검증 가능하도록 — 사용자가 그 파일을 열면 답변 근거를 확인할 수 있어야.');
|
||||
lines.push('5. *출처:* 라인은 답변 *맨 끝* 한 번만 — 본문 중간에 흩어 놓지 말 것.');
|
||||
lines.push('[/CITATION TRACE]');
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Conflict Surface — [CONFLICT WARNINGS] 시스템 프롬프트 블록 생성.
|
||||
*
|
||||
* 기존 scoring.ts 가 문서당 conflictSeverity(NONE/LOW/MEDIUM/HIGH) 를 *이미*
|
||||
* 계산하지만(반대/논란/vs 등 indicator 단어 카운트), LLM 은 그 사실을 모름.
|
||||
* buildAstraModeSystemPrompt 의 v4 정책 텍스트는 이미 "[CONFLICT WARNING] 플래그"
|
||||
* 를 *언급*하나, 실제 어떤 문서가 충돌인지 LLM 에게 *전달되지 않음* — 정책이
|
||||
* 명시되어 있지만 데이터가 없어 무용한 상태.
|
||||
*
|
||||
* 이 모듈이 그 갭을 메움:
|
||||
* 1. 자기-신호(self-flag) — chunk.metadata.conflictSeverity ≥ threshold
|
||||
* 2. 교차-문서 발산(cross-divergence) — 같은 주제 2 chunks, Jaccard < 임계
|
||||
*
|
||||
* 둘을 합쳐 마크다운 블록 한 개로. 결과가 비면 빈 문자열 반환 — 호출자가
|
||||
* 안전하게 무조건 join 가능.
|
||||
*/
|
||||
|
||||
import { RetrievalChunk, ConflictSeverity } from './types';
|
||||
import { tokenize } from './scoring';
|
||||
|
||||
/** 사용자 설정 임계값. 'low' = LOW 부터, 'medium' = MEDIUM 부터, 'high' = HIGH 만. */
|
||||
export type ConflictThresholdSetting = 'low' | 'medium' | 'high';
|
||||
|
||||
export interface ConflictBlockOptions {
|
||||
/** 자기-신호 surface 시 최소 severity. 기본 'medium'. */
|
||||
selfFlagThreshold: ConflictThresholdSetting;
|
||||
/** 교차 발산 감지 enable. 기본 true. */
|
||||
crossDivergenceEnabled: boolean;
|
||||
/** 자기-신호 / 교차 발산 각각 표시 최대 건수. 기본 5. */
|
||||
maxPerSection: number;
|
||||
/** Chunk 미리보기 길이. 기본 220 chars. */
|
||||
excerptLength: number;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: ConflictBlockOptions = {
|
||||
selfFlagThreshold: 'medium',
|
||||
crossDivergenceEnabled: true,
|
||||
maxPerSection: 5,
|
||||
excerptLength: 220,
|
||||
};
|
||||
|
||||
function severityRank(s: ConflictSeverity | undefined): number {
|
||||
switch (s) {
|
||||
case 'HIGH': return 3;
|
||||
case 'MEDIUM': return 2;
|
||||
case 'LOW': return 1;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function thresholdRank(t: ConflictThresholdSetting): number {
|
||||
switch (t) {
|
||||
case 'high': return 3;
|
||||
case 'medium': return 2;
|
||||
case 'low': return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function severityEmoji(s: ConflictSeverity | undefined): string {
|
||||
switch (s) {
|
||||
case 'HIGH': return '🔴';
|
||||
case 'MEDIUM': return '🟡';
|
||||
case 'LOW': return '🟠';
|
||||
default: return '⚪';
|
||||
}
|
||||
}
|
||||
|
||||
function shortExcerpt(text: string, n: number): string {
|
||||
if (!text) return '';
|
||||
const cleaned = text.replace(/\s+/g, ' ').trim();
|
||||
return cleaned.length <= n ? cleaned : cleaned.slice(0, n) + '…';
|
||||
}
|
||||
|
||||
/** 두 토큰 집합의 Jaccard 유사도. */
|
||||
function jaccard(a: Set<string>, b: Set<string>): number {
|
||||
if (a.size === 0 || b.size === 0) return 0;
|
||||
let intersect = 0;
|
||||
for (const t of a) if (b.has(t)) intersect++;
|
||||
const union = a.size + b.size - intersect;
|
||||
return union === 0 ? 0 : intersect / union;
|
||||
}
|
||||
|
||||
/**
|
||||
* 교차-문서 발산 후보 쌍 찾기.
|
||||
*
|
||||
* 휴리스틱:
|
||||
* 1. 각 chunk 의 title 토큰(최대 5개) 으로 "주제 키" 생성
|
||||
* 2. 동일 주제 키 2개 이상 공유하는 chunk 쌍을 후보로
|
||||
* 3. 본문 토큰 Jaccard < 0.30 이면 발산으로 판정 (같은 주제 다른 내용)
|
||||
* 4. 점수 = (공유 토픽 토큰 수) × (1 - Jaccard) — 발산이 클수록 우선
|
||||
*
|
||||
* 한 chunk 가 여러 쌍에 등장 가능 — 상위 N 쌍만 반환.
|
||||
*/
|
||||
interface DivergencePair {
|
||||
a: RetrievalChunk;
|
||||
b: RetrievalChunk;
|
||||
sharedTopicTokens: string[];
|
||||
contentJaccard: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
function findCrossDivergence(chunks: RetrievalChunk[], topicJaccardMax: number = 0.30): DivergencePair[] {
|
||||
if (chunks.length < 2) return [];
|
||||
|
||||
// Pre-compute title topic tokens + content token sets — n^2 비교 전에 한 번만.
|
||||
const titleTokenSets: Set<string>[] = [];
|
||||
const contentTokenSets: Set<string>[] = [];
|
||||
for (const c of chunks) {
|
||||
const titleTokens = tokenize(c.title || '').filter((t) => t.length >= 2);
|
||||
titleTokenSets.push(new Set(titleTokens.slice(0, 8)));
|
||||
contentTokenSets.push(new Set(tokenize(c.content || '')));
|
||||
}
|
||||
|
||||
const pairs: DivergencePair[] = [];
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
for (let j = i + 1; j < chunks.length; j++) {
|
||||
// 1. 같은 주제 — title 토큰 공유 ≥ 2
|
||||
const shared: string[] = [];
|
||||
for (const t of titleTokenSets[i]) if (titleTokenSets[j].has(t)) shared.push(t);
|
||||
if (shared.length < 2) continue;
|
||||
|
||||
// 2. 본문 발산 — Jaccard < 임계
|
||||
const cj = jaccard(contentTokenSets[i], contentTokenSets[j]);
|
||||
if (cj >= topicJaccardMax) continue;
|
||||
|
||||
pairs.push({
|
||||
a: chunks[i],
|
||||
b: chunks[j],
|
||||
sharedTopicTokens: shared,
|
||||
contentJaccard: cj,
|
||||
score: shared.length * (1 - cj),
|
||||
});
|
||||
}
|
||||
}
|
||||
pairs.sort((p, q) => q.score - p.score);
|
||||
return pairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 프롬프트용 [CONFLICT WARNINGS] 블록 생성. 충돌 없으면 빈 문자열 반환.
|
||||
*
|
||||
* 호출 측은 무조건 join 해도 안전 — 빈 문자열이면 프롬프트에 추가 줄바꿈 없음.
|
||||
*/
|
||||
export function buildConflictWarningsBlock(
|
||||
chunks: RetrievalChunk[],
|
||||
options: Partial<ConflictBlockOptions> = {},
|
||||
): string {
|
||||
const opts: ConflictBlockOptions = { ...DEFAULT_OPTIONS, ...options };
|
||||
if (!chunks || chunks.length === 0) return '';
|
||||
|
||||
// ─── Section 1: self-flag ───
|
||||
const threshold = thresholdRank(opts.selfFlagThreshold);
|
||||
const selfFlagged = chunks
|
||||
.filter((c) => severityRank(c.metadata?.conflictSeverity) >= threshold)
|
||||
.sort((a, b) => severityRank(b.metadata?.conflictSeverity) - severityRank(a.metadata?.conflictSeverity))
|
||||
.slice(0, opts.maxPerSection);
|
||||
|
||||
// ─── Section 2: cross-doc divergence ───
|
||||
const divergence = opts.crossDivergenceEnabled
|
||||
? findCrossDivergence(chunks).slice(0, opts.maxPerSection)
|
||||
: [];
|
||||
|
||||
if (selfFlagged.length === 0 && divergence.length === 0) return '';
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push('[CONFLICT WARNINGS]');
|
||||
lines.push('다음 검색된 출처에서 충돌 신호 감지. 단일 결론을 강요하지 말고, 상충되는 관점을 명시하고 사용자 판단에 위임할 것.');
|
||||
lines.push('');
|
||||
|
||||
if (selfFlagged.length > 0) {
|
||||
lines.push('## 자기-신호 (출처 내부에서 충돌/논란 키워드 감지)');
|
||||
for (const c of selfFlagged) {
|
||||
const sev = c.metadata?.conflictSeverity || 'NONE';
|
||||
const emoji = severityEmoji(sev);
|
||||
const src = c.source;
|
||||
const title = c.title || '(제목 없음)';
|
||||
lines.push(`- ${emoji} **[${sev}]** \`${src}\` · ${title}`);
|
||||
lines.push(` > ${shortExcerpt(c.content, opts.excerptLength)}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (divergence.length > 0) {
|
||||
lines.push('## 교차-문서 발산 (같은 주제·다른 내용 ─ 잠재적 모순)');
|
||||
for (const p of divergence) {
|
||||
const topics = p.sharedTopicTokens.slice(0, 5).join(' · ');
|
||||
const cjPct = (p.contentJaccard * 100).toFixed(0);
|
||||
lines.push(`- 🔀 **공유 주제**: ${topics} _(본문 중복 ${cjPct}%)_`);
|
||||
lines.push(` - A: \`${p.a.source}\` · ${p.a.title || '(제목 없음)'}`);
|
||||
lines.push(` > ${shortExcerpt(p.a.content, opts.excerptLength)}`);
|
||||
lines.push(` - B: \`${p.b.source}\` · ${p.b.title || '(제목 없음)'}`);
|
||||
lines.push(` > ${shortExcerpt(p.b.content, opts.excerptLength)}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('[지침]');
|
||||
lines.push('1. 답변에 위 출처 중 하나라도 사용한다면, 충돌 가능성을 명시 (예: "출처 A 는 X 라 하나 출처 B 는 Y").');
|
||||
lines.push('2. 어느 쪽이 옳다고 단정하지 말고, 사용자가 판단할 수 있도록 근거를 분리해 제시.');
|
||||
lines.push('3. 충돌이 답변과 무관하면 무시 가능 — 다만 무관 판단 자체도 한 줄로 기록.');
|
||||
lines.push('[/CONFLICT WARNINGS]');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Chain-of-Verification (CoVe) — [VERIFICATION CHECKLIST] 시스템 프롬프트 블록 생성.
|
||||
*
|
||||
* 배경: 사용자 피드백 "추론 결과가 나오기 직전, 이 결론이 확보된 지식(제2뇌)에만
|
||||
* 근거하고 있는가? 를 스스로 질문하고 검증하는 로직" — 할루시네이션 방지 + 그라운딩
|
||||
* 명확화.
|
||||
*
|
||||
* 원논문 CoVe 는 2-pass (draft → verify → revise). 하지만 ASTRA 는 local-first 라
|
||||
* 추가 LLM 호출 비용이 크고, 같은 모델이 self-verify 하는 효과도 제한적. 그래서
|
||||
* v1 은 *instructional* CoVe — 시스템 프롬프트에 명시적 검증 체크리스트를 주입해
|
||||
* 모델이 한 패스 안에서 "답변 작성 전" 그라운딩 점검을 *내재화* 하도록.
|
||||
*
|
||||
* 향후 strict 모드에서 두 번째 verification pass 추가 가능 (config knob 준비).
|
||||
*
|
||||
* Conflict Surface 와의 관계: [CONFLICT WARNINGS] 가 "충돌 출처 데이터" 제공,
|
||||
* 이 CoVe 블록이 "그 데이터를 어떻게 verify 할지" 지시. 둘은 서로 보완.
|
||||
*/
|
||||
|
||||
import { RetrievalChunk } from './types';
|
||||
|
||||
export interface CoveBlockOptions {
|
||||
/** 체크리스트에 나열할 최상위 출처 수. 기본 5. */
|
||||
topSourcesCount: number;
|
||||
/**
|
||||
* Strict 모드 — 켜면 LLM 에게 *모든 주장에 출처 ID 를 inline 으로 인용*하라고 지시.
|
||||
* 끄면 일반 가이드만. 기본 off (자연스러운 답변 유지).
|
||||
*/
|
||||
strictMode: boolean;
|
||||
/** 출처 미리보기 길이. 기본 140 chars. */
|
||||
excerptLength: number;
|
||||
/** 사용자 query 일부를 체크리스트에 echo 할지. 기본 true — 모델이 vague answer 방지. */
|
||||
echoQuery: boolean;
|
||||
/** Query echo 최대 길이. 기본 180. */
|
||||
queryEchoMaxLength: number;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: CoveBlockOptions = {
|
||||
topSourcesCount: 5,
|
||||
strictMode: false,
|
||||
excerptLength: 140,
|
||||
echoQuery: true,
|
||||
queryEchoMaxLength: 180,
|
||||
};
|
||||
|
||||
function shortExcerpt(text: string, n: number): string {
|
||||
if (!text) return '';
|
||||
const cleaned = text.replace(/\s+/g, ' ').trim();
|
||||
return cleaned.length <= n ? cleaned : cleaned.slice(0, n) + '…';
|
||||
}
|
||||
|
||||
/**
|
||||
* CoVe 블록 생성. 검색된 chunks 가 없으면 빈 문자열 — 그라운딩할 출처가 없는 상태에서
|
||||
* CoVe 를 강요하면 모델이 "출처 없음" 으로 답변 거부할 수 있음. 단, 사용자 query 가
|
||||
* 사실 검증 류일 때만 의미가 있으므로 호출자가 enable/disable 결정 가능.
|
||||
*/
|
||||
export function buildCoveChecklistBlock(
|
||||
chunks: RetrievalChunk[],
|
||||
userPrompt: string,
|
||||
options: Partial<CoveBlockOptions> = {},
|
||||
): string {
|
||||
const opts: CoveBlockOptions = { ...DEFAULT_OPTIONS, ...options };
|
||||
if (!chunks || chunks.length === 0) return '';
|
||||
|
||||
// 점수 순 상위 N — 다양한 source 가 섞이도록 source 별로 1개씩 round-robin 도 고려했으나,
|
||||
// CoVe 는 *근거 강한 출처* 가 더 중요해서 score 단순 정렬 채택.
|
||||
const top = chunks
|
||||
.filter((c) => c.source !== 'brain-trace') // brain-trace 는 trace 표시용, 본문 없음
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, opts.topSourcesCount);
|
||||
|
||||
if (top.length === 0) return '';
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push('[VERIFICATION CHECKLIST — Chain-of-Verification]');
|
||||
lines.push('답변을 *작성하기 전* 다음을 점검. 검증 통과한 주장만 답변에 포함할 것.');
|
||||
|
||||
if (opts.echoQuery && userPrompt && userPrompt.trim()) {
|
||||
const q = userPrompt.replace(/\s+/g, ' ').trim();
|
||||
const echo = q.length > opts.queryEchoMaxLength ? q.slice(0, opts.queryEchoMaxLength) + '…' : q;
|
||||
lines.push('');
|
||||
lines.push(`> **사용자 질의**: ${echo}`);
|
||||
}
|
||||
|
||||
// ─── Section 1: 근거 매핑 ───
|
||||
lines.push('');
|
||||
lines.push('## 1. 근거 매핑 (Grounding Inventory)');
|
||||
lines.push('이 답변의 핵심 주장 각각이 *어느 출처* 에서 왔는지 명시 가능한가?');
|
||||
lines.push('');
|
||||
for (let i = 0; i < top.length; i++) {
|
||||
const c = top[i];
|
||||
const scoreFmt = c.score.toFixed(2);
|
||||
const sev = c.metadata?.conflictSeverity && c.metadata.conflictSeverity !== 'NONE'
|
||||
? ` ⚠️${c.metadata.conflictSeverity}` : '';
|
||||
lines.push(`- **[S${i + 1}]** \`${c.source}\` · ${c.title || '(제목 없음)'} _(score ${scoreFmt})_${sev}`);
|
||||
lines.push(` > ${shortExcerpt(c.content, opts.excerptLength)}`);
|
||||
}
|
||||
lines.push('');
|
||||
lines.push('출처 미매핑(=어느 S 도 직접 지지하지 않음) 주장은 *모델 일반 지식*. 그 사실을 답변에 명시.');
|
||||
|
||||
// ─── Section 2: 자기 질문 ───
|
||||
lines.push('');
|
||||
lines.push('## 2. 답변 직전 자기 질문 (Pre-Output Self-Check)');
|
||||
lines.push('답변 보내기 전 *반드시* 답하라:');
|
||||
lines.push('- (a) 이 답변의 결론이 위 [S1..SN] 중 어디에 직접 근거하나? 매핑 안 되는 결론 = 일반 지식 → 명시.');
|
||||
lines.push('- (b) "확실하다", "반드시", "이미 결정됨" 같은 단정적 표현을 쓴다면 출처가 그 강도를 지지하는가? 아니면 톤 완화.');
|
||||
lines.push('- (c) 사용자에게 다음 *구체적 액션* 을 제시했는가, 아니면 추상적 조언만 했는가?');
|
||||
lines.push('- (d) [CONFLICT WARNINGS] 블록과 결합 — 충돌 출처 사용 시 양측 명시했는가?');
|
||||
|
||||
// ─── Section 3: Strict 모드 (옵션) ───
|
||||
if (opts.strictMode) {
|
||||
lines.push('');
|
||||
lines.push('## 3. ⚙️ STRICT 모드 — Inline Citation 강제');
|
||||
lines.push('각 사실 주장 뒤에 `[S1]`, `[S2]` 형식으로 출처 ID 를 *반드시* 인용. 인용 없으면 모델 지식으로 간주되어 답변 신뢰도 감점.');
|
||||
lines.push('예) "큐브앤코는 enterprise 요금제다 [S2]." / "일반적으로 SaaS B2B 는 ~ (모델 지식, 직접 출처 없음)."');
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('[/VERIFICATION CHECKLIST]');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -24,6 +24,8 @@ import { selectWithinBudget, assembleContext, estimateTokens } from './contextBu
|
||||
import { getBrainTokenIndex, getBrainEmbeddings } from './brainIndex';
|
||||
import { extractLessonEssence } from './lessonHelpers';
|
||||
import { cosineSimilarity } from './embeddings';
|
||||
import { applyActionabilityBoost, WorkStateSignals, ActionabilityWeights } from './actionabilityScoring';
|
||||
import { applyHierarchicalReweight, classifyQueryLevel, AbstractionLevel, HierarchicalWeights } from './hierarchicalLevel';
|
||||
|
||||
export { tokenize, expandQuery, scoreTfIdf, scoreTfIdfPreTokenized, extractBestExcerpt } from './scoring';
|
||||
export { selectWithinBudget, assembleContext, estimateTokens } from './contextBudget';
|
||||
@@ -81,6 +83,20 @@ interface RetrievalOptions {
|
||||
embeddingModel?: string;
|
||||
/** Blend weight: 0 = TF-IDF only, 1 = cosine only. Default 0.5. */
|
||||
embeddingBlendAlpha?: number;
|
||||
/**
|
||||
* Actionability — "현재 작업 상태" 신호(최근 슬래시 명령 + 열린 파일) 로 검색 결과 재가중.
|
||||
* undefined 면 actionability re-rank 안 함 (legacy 동작).
|
||||
*/
|
||||
workStateSignals?: WorkStateSignals;
|
||||
/** Actionability 결합 가중치. undefined 면 default. */
|
||||
actionabilityWeights?: ActionabilityWeights;
|
||||
/**
|
||||
* Hierarchical Context Window — 질의·문서 추상도 매칭 재가중.
|
||||
* true 면 query 추상도 분류 후 chunks 재가중. false / undefined 면 skip.
|
||||
*/
|
||||
hierarchicalReweightEnabled?: boolean;
|
||||
/** Hierarchical 가중치 override. undefined 면 default. */
|
||||
hierarchicalWeights?: HierarchicalWeights;
|
||||
}
|
||||
|
||||
export class RetrievalOrchestrator {
|
||||
@@ -148,6 +164,25 @@ export class RetrievalOrchestrator {
|
||||
this.normalizeScores(allChunks);
|
||||
fusionLog.push(`Total chunks before budget: ${allChunks.length}`);
|
||||
|
||||
// ── ③-b Actionability Re-rank — work-state 신호로 점수 boost ──
|
||||
// normalize 직후, budget 전 — actionability 가 어떤 chunk 가 살아남는지에 영향.
|
||||
if (options.workStateSignals) {
|
||||
applyActionabilityBoost(allChunks, options.workStateSignals, options.actionabilityWeights);
|
||||
const boosted = allChunks.filter((c) => (c.metadata as any).actionabilityScore > 0).length;
|
||||
const cmds = options.workStateSignals.recentSlashCommands.slice(0, 3).join(',');
|
||||
const openFile = options.workStateSignals.openFilePath ? path.basename(options.workStateSignals.openFilePath) : '-';
|
||||
fusionLog.push(`Actionability re-rank: ${boosted} chunks boosted (cmds=[${cmds}], openFile=${openFile})`);
|
||||
}
|
||||
|
||||
// ── ③-c Hierarchical Context Window — 추상도 레벨 매칭 ──
|
||||
// 질의·문서 추상도 매칭 점수 조정. 같은 레벨 bonus, 양 끝 mismatch penalty.
|
||||
// Actionability 직후 — 두 재가중을 합쳐 한 번의 budget selection.
|
||||
if (options.hierarchicalReweightEnabled) {
|
||||
const queryLevel = classifyQueryLevel(query);
|
||||
const { sameLevel, farMismatch } = applyHierarchicalReweight(allChunks, queryLevel, options.hierarchicalWeights);
|
||||
fusionLog.push(`Hierarchical re-rank (query=${queryLevel}): ${sameLevel} same-level (+), ${farMismatch} far-mismatch (-)`);
|
||||
}
|
||||
|
||||
// ── ④ Context Budget Selection ──
|
||||
const { selected, dropped, tokensUsed } = selectWithinBudget(
|
||||
allChunks,
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Intent Clarification — 모호한 질의에서 *추측 답변 대신 질문 던지기* 지시.
|
||||
*
|
||||
* 사용자 피드백: "ASTRA 는 질문을 받으면 즉시 답변을 생성하려 함. 하지만 '날카로운'
|
||||
* 파악을 위해서는 질문 뒤의 '실행 목적' 을 먼저 정의해야". 예: "배포해줘" → 환경
|
||||
* (dev/prod) / 태그 묻기. "그 부분 고쳐줘" → 어느 파일/모듈인지 묻기.
|
||||
*
|
||||
* 현재 ASTRA: 모호 감지 메커니즘 없음. CoVe(v2.2.184) 가 *답변 작성 시* 출처 매핑
|
||||
* 검증하지만, *질문 자체가 모호한 경우* 는 다루지 않음. 이 모듈이 그 갭.
|
||||
*
|
||||
* 설계:
|
||||
* - 휴리스틱 차원(환경/대상/범위/포맷/마감) 별로 *trigger 키워드 + 명시 키워드* 정의
|
||||
* - trigger 가 있는데 명시가 없으면 missing
|
||||
* - missing 차원이 strictness 임계 이상이면 ambiguous → 시스템 프롬프트에 질문 지시
|
||||
*
|
||||
* 위험: false positive → 사용자가 "그냥 답해" 짜증. strictness 로 조절.
|
||||
*/
|
||||
|
||||
export type IntentStrictness = 'low' | 'medium' | 'high';
|
||||
|
||||
interface AmbiguityDimensionDef {
|
||||
key: string;
|
||||
label: string; // 한국어 표시명
|
||||
/** 이 차원이 *문제 되는 지* 판정하는 trigger 단어들 (있으면 의심 시작). */
|
||||
triggers: string[];
|
||||
/** 차원이 *명시* 됐다고 보는 단어들 (있으면 ambiguity 해소). */
|
||||
specifiers: string[];
|
||||
/** missing 일 때 사용자에게 권장 질문 예시. */
|
||||
suggestedQuestion: string;
|
||||
}
|
||||
|
||||
const DIMENSIONS: AmbiguityDimensionDef[] = [
|
||||
{
|
||||
key: 'environment',
|
||||
label: '환경 (dev/prod/staging)',
|
||||
triggers: ['배포', '롤백', 'deploy', 'rollback', 'release', '릴리스', '릴리즈', '띄워', '재시작', 'restart'],
|
||||
specifiers: ['dev', 'prod', 'staging', 'local', '로컬', '개발', '운영', '프로덕션', '스테이징', '본번', '본 번', '본번에', '운영에'],
|
||||
suggestedQuestion: '어느 환경에 작업할지 (dev/prod/staging) 명시해 주실 수 있나요?',
|
||||
},
|
||||
{
|
||||
key: 'target',
|
||||
label: '대상 (파일/모듈/멤버)',
|
||||
triggers: ['고쳐', '고처', '수정', '바꿔', '추가', '제거', '리팩토', '리팩터', '리팩터링', '리팩토링', '개선', '정리', '리뷰', '검토해'],
|
||||
specifiers: ['.ts', '.tsx', '.js', '.py', '.md', '.json', '.go', '.rs', '파일', '함수', '클래스', '모듈', '@', 'src/', 'lib/', 'features/', '폴더'],
|
||||
suggestedQuestion: '어느 파일/모듈/함수를 대상으로 할지 명시해 주실 수 있나요?',
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: '범위 (전체/부분)',
|
||||
triggers: ['리팩토', '리팩터', '리팩터링', '리팩토링', '정리해', '개선', '최적화', '튜닝', '청소', '정비'],
|
||||
specifiers: ['전체', '전부', '모두', '일부', '특정', '하나만', '이것만', '여기만', '단', '단지'],
|
||||
suggestedQuestion: '범위가 전체인지 특정 부분인지 알려 주실 수 있나요?',
|
||||
},
|
||||
{
|
||||
key: 'format',
|
||||
label: '출력 포맷',
|
||||
triggers: ['요약', '보고서', '리포트', '정리', '문서', '카드', '발표', '슬라이드', '프레젠테이션'],
|
||||
specifiers: ['표', '리스트', 'json', 'markdown', '마크다운', '단락', 'bullet', '글머리표', '한장', '한 장', '슬라이드', 'pdf', '문장으로', '항목별', '단계별'],
|
||||
suggestedQuestion: '어떤 형식 (표/리스트/단락 등) 으로 받고 싶은지 알려 주실 수 있나요?',
|
||||
},
|
||||
{
|
||||
key: 'deadline',
|
||||
label: '마감/긴급도',
|
||||
triggers: ['언제까지', '마감', '빨리', '급함', '오늘 안에', '내일까지'],
|
||||
specifiers: ['오늘', '내일', '이번 주', '다음 주', '월', '일', '시', '분'],
|
||||
suggestedQuestion: '마감일이나 긴급도를 알려 주실 수 있나요?',
|
||||
},
|
||||
];
|
||||
|
||||
export interface AmbiguityResult {
|
||||
ambiguous: boolean;
|
||||
missingDimensions: { key: string; label: string; suggestedQuestion: string }[];
|
||||
triggerCount: number;
|
||||
promptLength: number;
|
||||
}
|
||||
|
||||
function hasAnyKeyword(text: string, keywords: string[]): boolean {
|
||||
const lower = text.toLowerCase();
|
||||
return keywords.some((k) => lower.includes(k.toLowerCase()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 모호 감지. strictness 에 따라 threshold 변동:
|
||||
* - low: 2개 이상 missing → ambiguous
|
||||
* - medium: 1개 이상 missing → ambiguous (기본)
|
||||
* - high: 1개 이상 missing OR 프롬프트 짧음 (<20 chars) → ambiguous
|
||||
*/
|
||||
export function detectAmbiguity(prompt: string, strictness: IntentStrictness = 'medium'): AmbiguityResult {
|
||||
const result: AmbiguityResult = {
|
||||
ambiguous: false,
|
||||
missingDimensions: [],
|
||||
triggerCount: 0,
|
||||
promptLength: (prompt || '').length,
|
||||
};
|
||||
if (!prompt || !prompt.trim()) return result;
|
||||
|
||||
for (const dim of DIMENSIONS) {
|
||||
const hasTrigger = hasAnyKeyword(prompt, dim.triggers);
|
||||
if (!hasTrigger) continue;
|
||||
result.triggerCount++;
|
||||
const hasSpecifier = hasAnyKeyword(prompt, dim.specifiers);
|
||||
if (!hasSpecifier) {
|
||||
result.missingDimensions.push({
|
||||
key: dim.key,
|
||||
label: dim.label,
|
||||
suggestedQuestion: dim.suggestedQuestion,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const missingCount = result.missingDimensions.length;
|
||||
if (strictness === 'low') result.ambiguous = missingCount >= 2;
|
||||
else if (strictness === 'medium') result.ambiguous = missingCount >= 1;
|
||||
else result.ambiguous = missingCount >= 1 || (result.promptLength < 20 && result.triggerCount > 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 프롬프트용 [INTENT CLARIFICATION GUIDANCE] 블록.
|
||||
* ambiguous=false 면 빈 문자열 반환.
|
||||
*/
|
||||
export function buildIntentClarificationBlock(result: AmbiguityResult): string {
|
||||
if (!result.ambiguous || result.missingDimensions.length === 0) return '';
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push('[INTENT CLARIFICATION GUIDANCE]');
|
||||
lines.push('사용자 질의에서 다음 의도 차원이 *명시되지 않음* — 추측 답변보다 *짧은 역질문* 우선:');
|
||||
lines.push('');
|
||||
for (const d of result.missingDimensions) {
|
||||
lines.push(`- **${d.label}** — 예: "${d.suggestedQuestion}"`);
|
||||
}
|
||||
lines.push('');
|
||||
lines.push('[지침]');
|
||||
lines.push('1. 모호 차원이 답변의 *방향* 을 좌우하는 경우, 1~2개 핵심 질문을 *먼저* 던질 것 (전체 답변 미리 만들지 말 것).');
|
||||
lines.push('2. 사용자가 이미 "추정해도 OK", "그냥 진행", "알아서" 같은 표현을 했으면 합리적 가정 + *가정 명시* 후 진행.');
|
||||
lines.push('3. 모호 차원이 답변과 *무관* 한 정보성/탐색성 질의면 그대로 답변 OK.');
|
||||
lines.push('4. 질문 던질 때 사용자가 다시 입력하기 쉽도록 *선택지 2~3개* 또는 *기대 형식* 같이 명시.');
|
||||
lines.push('[/INTENT CLARIFICATION GUIDANCE]');
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* LLM Semantic Re-ranking — TF-IDF / 임베딩이 놓치는 *의도* 매치를 작은 LLM 호출
|
||||
* 한 번으로 잡는다.
|
||||
*
|
||||
* 동작:
|
||||
* 1. 1차 검색(TF-IDF + embedding + 부스트들) 결과의 *상위 K* (기본 15) 후보를 추출
|
||||
* 2. 가벼운 프롬프트로 LLM 에게 "이 중 query 의도에 가장 부합하는 순서로 ID 나열" 요청
|
||||
* 3. LLM 응답을 파싱해 순서 적용 — 응답 실패/누락 ID 는 원순서 유지
|
||||
*
|
||||
* 비용·위험 관리:
|
||||
* - 기본 OFF (g1nation.semanticRerankEnabled). 사용자가 latency 감수할 의지 있을 때만.
|
||||
* - 짧은 timeout (기본 8초) — 초과 시 원순서 그대로 반환, 검색 실패 안 됨.
|
||||
* - 후보 K 제한 — 토큰 비용 cap.
|
||||
* - 별도 빠른 모델 지정 가능 (`g1nation.semanticRerankModel`) — 메인 모델 외 작은 모델.
|
||||
*
|
||||
* 인터페이스: input chunks 순서는 *원본 score 내림차순* 으로 들어와야 함.
|
||||
* 반환: re-rank 가 성공하면 새 순서의 RetrievalChunk[], 실패하면 원순서.
|
||||
*/
|
||||
|
||||
import { RetrievalChunk } from './types';
|
||||
|
||||
export interface SemanticRerankOptions {
|
||||
ollamaUrl: string;
|
||||
/** Re-rank 전용 모델 ID. 비면 fallback model 사용. */
|
||||
model: string;
|
||||
/** 후보로 LLM 에 넘길 최대 chunk 개수. 기본 15. */
|
||||
candidateK: number;
|
||||
/** LLM 호출 타임아웃 (ms). 기본 8000. */
|
||||
timeoutMs: number;
|
||||
/** 각 chunk 미리보기 길이. 기본 240 chars. */
|
||||
excerptLength: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_SEMANTIC_RERANK_OPTIONS: Omit<SemanticRerankOptions, 'ollamaUrl' | 'model'> = {
|
||||
candidateK: 15,
|
||||
timeoutMs: 8000,
|
||||
excerptLength: 240,
|
||||
};
|
||||
|
||||
export interface SemanticRerankResult {
|
||||
rerankedChunks: RetrievalChunk[];
|
||||
/** true 면 LLM 응답으로 순서 변경됨. false 면 원순서 (실패/타임아웃/파싱 실패). */
|
||||
success: boolean;
|
||||
durationMs: number;
|
||||
/** 디버그·footer 표시용 — re-rank 가 어떻게 동작했는지. */
|
||||
note: string;
|
||||
}
|
||||
|
||||
function shortExcerpt(text: string, n: number): string {
|
||||
if (!text) return '';
|
||||
const cleaned = text.replace(/\s+/g, ' ').trim();
|
||||
return cleaned.length <= n ? cleaned : cleaned.slice(0, n) + '…';
|
||||
}
|
||||
|
||||
function buildRerankPrompt(query: string, candidates: RetrievalChunk[], excerptLength: number): { system: string; user: string } {
|
||||
const lines: string[] = [];
|
||||
for (let i = 0; i < candidates.length; i++) {
|
||||
const c = candidates[i];
|
||||
lines.push(`[C${i + 1}] (${c.source}) ${c.title || '(제목 없음)'}`);
|
||||
lines.push(` ${shortExcerpt(c.content, excerptLength)}`);
|
||||
}
|
||||
|
||||
const system = [
|
||||
'당신은 검색 결과 재정렬기 (re-ranker). 사용자 질의의 *의도* 와 각 후보 문서의 *내용 부합도* 를 평가해 가장 유용한 순서로 정렬.',
|
||||
'',
|
||||
'[규칙]',
|
||||
'1. 응답은 *반드시* 한 줄의 JSON: `{"ranking":[3,1,5,2,4,...]}` 형식.',
|
||||
'2. ranking 배열 원소 = 입력 [C1], [C2] 의 *번호* (1-based).',
|
||||
'3. 모든 입력 후보를 한 번씩만 포함. 누락·중복·번호 외 값 금지.',
|
||||
'4. 다른 설명·코드 블록·텍스트 출력 절대 금지 — JSON 한 줄만.',
|
||||
'5. 평가 기준: (a) 질의 의도와의 직접 부합도 > (b) 키워드 매치 > (c) 문맥 풍부도.',
|
||||
].join('\n');
|
||||
|
||||
const user = [
|
||||
`[사용자 질의]\n${query}`,
|
||||
'',
|
||||
`[후보 ${candidates.length}개]`,
|
||||
...lines,
|
||||
'',
|
||||
'위 후보를 가장 부합도 높은 순서로 정렬한 ranking 배열만 JSON 한 줄로 출력.',
|
||||
].join('\n');
|
||||
|
||||
return { system, user };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ollama / OpenAI 호환 endpoint 로 단발 호출. agents/factory.ts 의 BaseAgent.callLLM
|
||||
* 패턴 단순화. timeout, retry 1회만.
|
||||
*/
|
||||
async function callLlmForRerank(
|
||||
ollamaUrl: string,
|
||||
model: string,
|
||||
system: string,
|
||||
user: string,
|
||||
timeoutMs: number,
|
||||
): Promise<string> {
|
||||
const isOllama = ollamaUrl.includes(':11434') || ollamaUrl.includes('ollama');
|
||||
const endpoint = isOllama ? `${ollamaUrl}/api/chat` : `${ollamaUrl}/v1/chat/completions`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const body = isOllama
|
||||
? {
|
||||
model, stream: false,
|
||||
messages: [
|
||||
{ role: 'system', content: system },
|
||||
{ role: 'user', content: user },
|
||||
],
|
||||
options: { temperature: 0.0, num_predict: 256 },
|
||||
}
|
||||
: {
|
||||
model,
|
||||
messages: [
|
||||
{ role: 'system', content: system },
|
||||
{ role: 'user', content: user },
|
||||
],
|
||||
stream: false, temperature: 0.0, max_tokens: 256,
|
||||
};
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data: any = await res.json();
|
||||
const content =
|
||||
data?.message?.content ??
|
||||
data?.choices?.[0]?.message?.content ??
|
||||
data?.choices?.[0]?.text ??
|
||||
data?.response ??
|
||||
'';
|
||||
return String(content || '');
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/** LLM 응답에서 ranking 배열 추출 + 검증. 실패 시 null. */
|
||||
function parseRanking(raw: string, expectedSize: number): number[] | null {
|
||||
if (!raw) return null;
|
||||
// JSON 한 줄 추출 — { ... } 안에 ranking
|
||||
const match = raw.match(/\{[\s\S]*?\}/);
|
||||
if (!match) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(match[0]);
|
||||
const arr = parsed?.ranking;
|
||||
if (!Array.isArray(arr)) return null;
|
||||
const seen = new Set<number>();
|
||||
const out: number[] = [];
|
||||
for (const v of arr) {
|
||||
const n = typeof v === 'number' ? v : parseInt(String(v), 10);
|
||||
if (!Number.isFinite(n) || n < 1 || n > expectedSize) continue;
|
||||
if (seen.has(n)) continue;
|
||||
seen.add(n);
|
||||
out.push(n);
|
||||
}
|
||||
// 누락 보충 — LLM 이 일부 빠뜨렸으면 원순서로 뒤에 붙임.
|
||||
for (let i = 1; i <= expectedSize; i++) {
|
||||
if (!seen.has(i)) out.push(i);
|
||||
}
|
||||
return out.length === expectedSize ? out : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function semanticRerank(
|
||||
query: string,
|
||||
chunks: RetrievalChunk[],
|
||||
options: SemanticRerankOptions,
|
||||
): Promise<SemanticRerankResult> {
|
||||
const start = Date.now();
|
||||
const k = Math.max(2, Math.min(options.candidateK, chunks.length));
|
||||
if (chunks.length < 2 || k < 2) {
|
||||
return { rerankedChunks: chunks, success: false, durationMs: 0, note: 'too few candidates' };
|
||||
}
|
||||
// 입력은 score 내림차순 가정 — 상위 K 가 re-rank 대상, 나머지는 그대로 꼬리.
|
||||
const candidates = chunks.slice(0, k);
|
||||
const tail = chunks.slice(k);
|
||||
|
||||
const { system, user } = buildRerankPrompt(query, candidates, options.excerptLength);
|
||||
|
||||
let raw = '';
|
||||
try {
|
||||
raw = await callLlmForRerank(options.ollamaUrl, options.model, system, user, options.timeoutMs);
|
||||
} catch (e: any) {
|
||||
return {
|
||||
rerankedChunks: chunks,
|
||||
success: false,
|
||||
durationMs: Date.now() - start,
|
||||
note: `LLM call failed: ${e?.name || e?.message || 'unknown'}`,
|
||||
};
|
||||
}
|
||||
|
||||
const ranking = parseRanking(raw, candidates.length);
|
||||
if (!ranking) {
|
||||
return {
|
||||
rerankedChunks: chunks,
|
||||
success: false,
|
||||
durationMs: Date.now() - start,
|
||||
note: 'unparseable LLM response',
|
||||
};
|
||||
}
|
||||
|
||||
const reranked = ranking.map((i) => candidates[i - 1]);
|
||||
return {
|
||||
rerankedChunks: [...reranked, ...tail],
|
||||
success: true,
|
||||
durationMs: Date.now() - start,
|
||||
note: `re-ranked top ${k} (changed positions: ${ranking.filter((v, i) => v !== i + 1).length})`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Terminology Dictionary — 프로젝트 표준 용어집을 시스템 프롬프트에 주입.
|
||||
*
|
||||
* 사용자 제안: "표준 표기 강제 + 답변 내 표기 일관성 검증". 예: `runway` vs `런웨이`,
|
||||
* `P-Reinforce` vs `p-reinforce`, `Chronicle` vs `크로니클`.
|
||||
*
|
||||
* 설계 — 사용자 편집 markdown 파일:
|
||||
* - 위치: `<workspace>/.astra/glossary.md`
|
||||
* - 형식: 자유 markdown. ASTRA 는 *형식을 강제하지 않고* 통째로 주입
|
||||
* - 권장 컨벤션: H2/H3 섹션으로 표준 표기 / 영-한 컨벤션 / 금지 용어 등 그룹핑
|
||||
*
|
||||
* 시스템 프롬프트 블록 `[TERMINOLOGY DICTIONARY]`:
|
||||
* - 글로서리 본문 + Term Check 지침 (#1 typo/용어 self-check 사용자 제안 통합)
|
||||
* - 답변 작성 시 표준 표기 우선 + 답변 직전 자기 점검 + 새 용어 도입 시 명시
|
||||
*
|
||||
* 캐시: 파일 mtime 기반 — 매 turn 디스크 read 안 함.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const DEFAULT_GLOSSARY_REL_PATH = '.astra/glossary.md';
|
||||
|
||||
/** mtime-keyed cache — 사용자가 편집할 때만 다시 읽음. */
|
||||
const _cache = new Map<string, { mtime: number; content: string }>();
|
||||
|
||||
export function getGlossaryFilePath(relPath: string = DEFAULT_GLOSSARY_REL_PATH): string | null {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (!folders || folders.length === 0) return null;
|
||||
return path.join(folders[0].uri.fsPath, relPath);
|
||||
}
|
||||
|
||||
function readGlossary(relPath: string): string {
|
||||
const fp = getGlossaryFilePath(relPath);
|
||||
if (!fp) return '';
|
||||
try {
|
||||
if (!fs.existsSync(fp)) return '';
|
||||
const st = fs.statSync(fp);
|
||||
const cached = _cache.get(fp);
|
||||
if (cached && cached.mtime === st.mtimeMs) return cached.content;
|
||||
const content = fs.readFileSync(fp, 'utf-8').trim();
|
||||
_cache.set(fp, { mtime: st.mtimeMs, content });
|
||||
return content;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function clearGlossaryCache(): void {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
export interface TerminologyBlockOptions {
|
||||
/** Glossary 파일 상대 경로. 기본 '.astra/glossary.md'. */
|
||||
relPath: string;
|
||||
/** 본문 최대 길이 (chars). 너무 큰 글로서리는 시스템 프롬프트 비대 — cap. 기본 4000. */
|
||||
maxBodyLength: number;
|
||||
/** 길이 초과 시 잘릴 안내 표시 여부. */
|
||||
showTruncationNote: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_TERMINOLOGY_OPTIONS: TerminologyBlockOptions = {
|
||||
relPath: DEFAULT_GLOSSARY_REL_PATH,
|
||||
maxBodyLength: 4000,
|
||||
showTruncationNote: true,
|
||||
};
|
||||
|
||||
export function buildTerminologyBlock(options: Partial<TerminologyBlockOptions> = {}): string {
|
||||
const opts: TerminologyBlockOptions = { ...DEFAULT_TERMINOLOGY_OPTIONS, ...options };
|
||||
const raw = readGlossary(opts.relPath);
|
||||
if (!raw) return ''; // 파일 없음 → 블록 안 만듦 (no-op)
|
||||
|
||||
let body = raw;
|
||||
let truncated = false;
|
||||
if (body.length > opts.maxBodyLength) {
|
||||
body = body.slice(0, opts.maxBodyLength);
|
||||
truncated = true;
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push('[TERMINOLOGY DICTIONARY]');
|
||||
lines.push('프로젝트 표준 용어집. 답변 생성 시 다음 표기·컨벤션을 *최우선* 으로 사용.');
|
||||
lines.push('');
|
||||
lines.push('---');
|
||||
lines.push(body);
|
||||
if (truncated && opts.showTruncationNote) {
|
||||
lines.push('');
|
||||
lines.push(`_…(글로서리 ${raw.length - opts.maxBodyLength}자 잘림 — 핵심 용어를 앞쪽에 배치해 주세요)_`);
|
||||
}
|
||||
lines.push('---');
|
||||
|
||||
// Term Check 지침 — 사용자 제안 #1 (typo/용어 self-check) 통합.
|
||||
lines.push('');
|
||||
lines.push('[Term Check — 답변 직전 자기 점검]');
|
||||
lines.push('1. **표준 표기 우선**: 위 용어가 답변에 등장하면 *글로서리의 표기를 그대로* 사용. 변형·번역 임의 적용 금지.');
|
||||
lines.push('2. **표기 흔들림 방지**: 같은 용어를 한 답변 안에서 *동일 표기* 로 일관 사용 (예: "Chronicle" 과 "크로니클" 섞지 말 것).');
|
||||
lines.push('3. **새 용어 도입 시**: 글로서리에 없는 고유 명사·약어 처음 사용 시 *"새 용어: X"* 라고 한 번 명시.');
|
||||
lines.push('4. **금지 표기 검증**: 답변 직전, 글로서리의 *금지·비추* 항목이 답변에 들어가지 않았는지 검토. 들어갔으면 *재작성*.');
|
||||
lines.push('5. **모르겠으면 글로서리**: 표기 확신 없을 때 "글로서리에 없어 일반 표기 사용" 한 줄 명시 후 진행.');
|
||||
lines.push('[/TERMINOLOGY DICTIONARY]');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 글로서리 파일 작성 도우미 — 처음 사용자가 만들 때 권장 컨벤션 템플릿.
|
||||
* 슬래시 명령 `/glossary init` 등에서 호출.
|
||||
*/
|
||||
export const GLOSSARY_TEMPLATE = `# 프로젝트 용어집
|
||||
|
||||
ASTRA 가 답변 시 표준 표기로 사용. 사용자가 자유롭게 편집 가능.
|
||||
파일 저장 후 다음 채팅 turn 부터 자동 반영.
|
||||
|
||||
## 표준 표기
|
||||
|
||||
- **ASTRA** (X: astra, Astra 외) — 본 VS Code extension 이름
|
||||
- **P-Reinforce v3.0** (X: p-reinforce, p reinforce) — 지식 압축 규칙
|
||||
- **Chronicle ADR** (X: chronicle, ADR 단독) — 의사결정 기록
|
||||
|
||||
## 영-한 표기 컨벤션
|
||||
|
||||
- Performance → 성능
|
||||
- Bug → 버그
|
||||
- Memory → 메모리
|
||||
|
||||
## 금지·비추 표현
|
||||
|
||||
- ❌ "절대적", "반드시" (단정적 표현 — 정책 충돌 위험)
|
||||
- ❌ "에이전트가 알아서" (그라운딩 위반)
|
||||
- ❌ 한·영 깨짐 (예: "결ently", "p-rein동")
|
||||
|
||||
## 슬래시 명령 표기
|
||||
|
||||
원문 그대로 — 한국어 번역 금지:
|
||||
- /runway, /customers, /hire, /morning, /evening, /weekly, /cohort, /memory, /glossary
|
||||
`;
|
||||
Reference in New Issue
Block a user