ltcai 10.9.0 → 11.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +46 -64
  2. package/docs/CHANGELOG.md +72 -237
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/ONBOARDING.md +1 -1
  6. package/docs/OPERATIONS.md +1 -1
  7. package/docs/PERFORMANCE.md +78 -7
  8. package/docs/TRUST_MODEL.md +1 -1
  9. package/docs/WHY_LATTICE.md +1 -1
  10. package/docs/kg-schema.md +1 -1
  11. package/docs/v11.1.0_PRODUCT_INTELLIGENCE_PLAN.md +313 -0
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/graph/_kg_contract.py +11 -0
  14. package/lattice_brain/graph/curator.py +1 -1
  15. package/lattice_brain/graph/fusion.py +184 -3
  16. package/lattice_brain/graph/proactive.py +138 -1
  17. package/lattice_brain/graph/projection.py +10 -2
  18. package/lattice_brain/graph/retrieval.py +137 -7
  19. package/lattice_brain/graph/retrieval_docgen.py +20 -20
  20. package/lattice_brain/graph/retrieval_policy.py +6 -0
  21. package/lattice_brain/graph/retrieval_reads.py +188 -1
  22. package/lattice_brain/graph/retrieval_vector.py +474 -110
  23. package/lattice_brain/graph/schema.py +125 -2
  24. package/lattice_brain/graph/vector_index/__init__.py +85 -0
  25. package/lattice_brain/graph/vector_index/base.py +170 -0
  26. package/lattice_brain/graph/vector_index/brute_force.py +114 -0
  27. package/lattice_brain/graph/vector_index/hnsw.py +293 -0
  28. package/lattice_brain/graph/vector_index/jobs.py +287 -0
  29. package/lattice_brain/graph/vector_index/quantized.py +151 -0
  30. package/lattice_brain/graph/vector_index/selector.py +131 -0
  31. package/lattice_brain/ingestion.py +50 -3
  32. package/lattice_brain/portability.py +654 -2
  33. package/lattice_brain/runtime/agent_runtime.py +1 -1
  34. package/lattice_brain/runtime/contracts.py +1 -1
  35. package/lattice_brain/runtime/multi_agent.py +1 -1
  36. package/lattice_brain/self_model.py +620 -0
  37. package/lattice_brain/synthesis.py +801 -0
  38. package/latticeai/__init__.py +1 -1
  39. package/latticeai/api/brain_intelligence.py +83 -1
  40. package/latticeai/api/chat_stream.py +4 -1
  41. package/latticeai/api/local_files.py +62 -0
  42. package/latticeai/api/models.py +1 -1
  43. package/latticeai/api/portability.py +130 -1
  44. package/latticeai/api/security_dashboard.py +48 -13
  45. package/latticeai/api/voice_capture.py +4 -1
  46. package/latticeai/api/workspace.py +11 -5
  47. package/latticeai/core/embedding_providers.py +20 -1
  48. package/latticeai/core/legacy_compatibility.py +1 -1
  49. package/latticeai/core/marketplace.py +1 -1
  50. package/latticeai/core/messages.py +14 -0
  51. package/latticeai/core/model_compat.py +2 -2
  52. package/latticeai/core/tool_registry.py +0 -7
  53. package/latticeai/core/workspace_os_constants.py +1 -1
  54. package/latticeai/core/workspace_os_utils.py +4 -50
  55. package/latticeai/core/workspace_review_items.py +12 -1
  56. package/latticeai/integrations/telegram_bot.py +28 -10
  57. package/latticeai/models/router.py +1 -1
  58. package/latticeai/runtime/access_runtime.py +1 -1
  59. package/latticeai/runtime/network_boundary_wiring.py +9 -5
  60. package/latticeai/runtime/permission_mode_wiring.py +9 -6
  61. package/latticeai/runtime/router_registration.py +3 -0
  62. package/latticeai/services/architecture_readiness.py +1 -1
  63. package/latticeai/services/brain_intelligence.py +253 -0
  64. package/latticeai/services/memory_service.py +1 -1
  65. package/latticeai/services/model_catalog.py +4 -3
  66. package/latticeai/services/model_engines.py +28 -14
  67. package/latticeai/services/obsidian_bridge.py +618 -0
  68. package/latticeai/services/product_readiness.py +5 -3
  69. package/latticeai/tools/filesystem.py +4 -1
  70. package/package.json +1 -1
  71. package/scripts/bench_vector_index.py +295 -0
  72. package/scripts/check_current_release_docs.mjs +4 -2
  73. package/scripts/release_screen_claims.json +31 -0
  74. package/src-tauri/Cargo.lock +1 -1
  75. package/src-tauri/Cargo.toml +1 -1
  76. package/src-tauri/tauri.conf.json +1 -1
  77. package/static/app/asset-manifest.json +37 -37
  78. package/static/app/assets/{Act-CS9IeqUX.js → Act-D4zSxFR-.js} +1 -1
  79. package/static/app/assets/{AdminConsole-3UkIEWGA.js → AdminConsole-w5jBfPt2.js} +1 -1
  80. package/static/app/assets/{Brain-B22EmNqS.js → Brain-C2EqQg74.js} +2 -2
  81. package/static/app/assets/BrainHome-CvXS6XiQ.js +2 -0
  82. package/static/app/assets/BrainSignals-DOE_KhOU.js +1 -0
  83. package/static/app/assets/Capture-DPqpGK8d.js +1 -0
  84. package/static/app/assets/{CommandPalette-86m4FCcN.js → CommandPalette-CNf7h5fp.js} +1 -1
  85. package/static/app/assets/Library-BN0HYOfc.js +1 -0
  86. package/static/app/assets/LivingBrain-Dfq_wEDI.js +1 -0
  87. package/static/app/assets/ProductFlow-B-3O0rNV.js +1 -0
  88. package/static/app/assets/{ReviewCard-BepjSDpN.js → ReviewCard-gZ-tdqFM.js} +1 -1
  89. package/static/app/assets/System-BElUcSSw.js +1 -0
  90. package/static/app/assets/arrow-left-CFNIMjhv.js +1 -0
  91. package/static/app/assets/{bot-DQj0-LkM.js → bot--qYHMtkP.js} +1 -1
  92. package/static/app/assets/brain-DDCLjRqO.js +1 -0
  93. package/static/app/assets/{button-CmTknyAP.js → button-51Z3rsuv.js} +1 -1
  94. package/static/app/assets/{circle-pause-yTCWRziJ.js → circle-pause-CMIiMaQl.js} +1 -1
  95. package/static/app/assets/{circle-play-Ccrva84R.js → circle-play-DZoO_cfG.js} +1 -1
  96. package/static/app/assets/{cpu-CbJqWTlS.js → cpu-Bs6uc9W9.js} +1 -1
  97. package/static/app/assets/{download-CkSzbzU-.js → download-G-2olkWz.js} +1 -1
  98. package/static/app/assets/{folder-open-CKyjQ4PU.js → folder-open-CTOspnmb.js} +1 -1
  99. package/static/app/assets/{hard-drive-DAzk9um0.js → hard-drive-CewHWJhn.js} +1 -1
  100. package/static/app/assets/index-CkzokZAj.css +2 -0
  101. package/static/app/assets/{index-CxOcwsHV.js → index-D7Rr-J2Y.js} +3 -3
  102. package/static/app/assets/{input-DcMETmZ7.js → input-D4w_BZWl.js} +1 -1
  103. package/static/app/assets/{permissionCopy-BVf13_25.js → permissionCopy-CosBEXAZ.js} +1 -1
  104. package/static/app/assets/primitives-d0g9pvzS.js +1 -0
  105. package/static/app/assets/search-BLCYt75v.js +1 -0
  106. package/static/app/assets/{share-2-COWCHNZm.js → share-2-NmD7e_oV.js} +1 -1
  107. package/static/app/assets/{shield-alert-BDrvilyK.js → shield-alert-CcQeMuju.js} +1 -1
  108. package/static/app/assets/{textarea-rUmsc8cP.js → textarea-BPAJDc-0.js} +1 -1
  109. package/static/app/assets/{useFocusTrap-Bi5UY_8v.js → useFocusTrap-C7YLdTBC.js} +1 -1
  110. package/static/app/assets/{useQuery-C-AicB-3.js → useQuery-DRyD9opW.js} +1 -1
  111. package/static/app/assets/{utils-BMwWg78e.js → utils-DG1_ExrP.js} +3 -3
  112. package/static/app/assets/{workspace-Y93tls8P.js → workspace-CWVf3gsI.js} +1 -1
  113. package/static/app/index.html +4 -4
  114. package/static/sw.js +1 -1
  115. package/static/app/assets/BrainHome-CMDqgJF4.js +0 -2
  116. package/static/app/assets/BrainSignals-BeE8RJo3.js +0 -1
  117. package/static/app/assets/Capture-ZX9bQh68.js +0 -1
  118. package/static/app/assets/Library-Bhz5LUca.js +0 -1
  119. package/static/app/assets/LivingBrain-BSa0wpFG.js +0 -1
  120. package/static/app/assets/ProductFlow-IP4Q-5aQ.js +0 -1
  121. package/static/app/assets/System-Bwx1h_jT.js +0 -1
  122. package/static/app/assets/arrow-left-ig7AZU8B.js +0 -1
  123. package/static/app/assets/brain-D8OEwmVj.js +0 -1
  124. package/static/app/assets/index-BfD-jhA9.css +0 -2
  125. package/static/app/assets/primitives-CQV9Q2YM.js +0 -1
  126. package/static/app/assets/search-CXGASMMH.js +0 -1
@@ -0,0 +1,313 @@
1
+ # LatticeAI v11.1.0 — Full Feature Sprint Plan
2
+
3
+ **목표 버전**: `11.1.0`
4
+ **포지셔닝**: Foundation Hardening (v9–v11.0) → **Product Intelligence Layer**
5
+ **원칙**: 보안/커버리지 게이트는 회귀만 유지. 새로운 기능 개발에 집중.
6
+ **범위**: 아래 5개 트랙을 **한 릴리즈에서 모두** 완료한다.
7
+ **작성일**: 2026-08-10
8
+
9
+ ---
10
+
11
+ ## 0. 한 줄 요약
12
+
13
+ > **벡터 검색을 스케일 가능하게 만들고 → Brain이 스스로 생각하고 모순을 감지하게 만들고 → 멀티모달을 1등 시민으로 올리고 → 개인 온톨로지 + 에이전트 워크스페이스를 붙이고 → 외부 지식 도구와 선택적 공유까지 연결한다.**
14
+
15
+ 이 순서는 **의존성**과 **제품 체감 가치**를 동시에 고려한 것이다.
16
+ 앞 단계가 없으면 뒤 단계가 의미가 없거나 성능/품질이 붕괴한다.
17
+
18
+ ---
19
+
20
+ ## 1. 전체 의존성 & 실행 순서 (강제)
21
+
22
+ ```
23
+ [1] High-Perf Vector + Incremental/Background Indexing
24
+ ↓ (스케일·속도 확보)
25
+ [2] Proactive Synthesis + Contradiction + Temporal Reasoning
26
+ ↓ (Brain이 "살아있다"는 지능 레이어)
27
+ [3] Multi-modal as First-Class Graph Citizen
28
+ ↓ (이미지/오디오가 진짜 기억이 됨)
29
+ [4] Personal Ontology / Self-Model + Agent-Native Workspace OS
30
+ ↓ (개인화 + 장기 자율성)
31
+ [5] Interop Bridges + Selective Encrypted Brain Network
32
+ ```
33
+
34
+ **왜 이 순서인가**
35
+
36
+ | 순서 | 이유 |
37
+ |------|------|
38
+ | 1 | 현재 brute-force 벡터 검색이 5k 노드에서 ~1.7초. 이 병목을 제거하지 않으면 2~5번 기능을 붙여도 체감이 안 난다. |
39
+ | 2 | `proactive.py`가 이미 읽기 전용으로 존재. 벡터 품질이 올라가야 모순 감지·합성이 신뢰 가능해진다. |
40
+ | 3 | 스키마(IMAGE 등)는 이미 있다. 하지만 retrieval/embedding 파이프라인이 텍스트 중심이라 멀티모달이 붙으면 검색 품질이 떨어질 수 있다 → 1번 이후. |
41
+ | 4 | AgentRuntime + Change Governor가 이미 있다. 개인 온톨로지와 워크스페이스 OS는 2·3번이 만든 “풍부한 기억” 위에 올려야 의미 있다. |
42
+ | 5 | 인제스트 게이트와 포트어빌리티가 안정된 뒤에야 외부 브릿지와 네트워크 공유가 안전하다. |
43
+
44
+ **병렬 가능 구간**
45
+ - 1번 진행 중에도 2번의 **스키마/데이터 모델 설계**와 4번의 **온톨로지 스키마 설계**는 병행 가능.
46
+ - 3번의 비전 임베딩 프로바이더 실험은 1번과 거의 독립적으로 가능.
47
+ - 5번의 Obsidian/Notion 파서 프로토타입은 인제스트 파이프라인만 안정되면 초반부터 가능.
48
+
49
+ ---
50
+
51
+ ## 2. 공통 가드레일 (모든 트랙에 적용)
52
+
53
+ 1. **SQLite는 여전히 Source of Truth**. 벡터 인덱스, 파생 그래프, 캐시는 모두 파생물. 깨져도 재구축 가능해야 한다.
54
+ 2. **Change Governor / Proposal-first** 유지. 에이전트가 Brain을 직접 쓰면 안 되고, 제안 → 리뷰 → 적용 경로를 탄다.
55
+ 3. **Local-first 기본값**. 클라우드 모델·네트워크·공유는 전부 opt-in.
56
+ 4. **정직성(Honest by design)**. 검색 품질, 벡터 freshness, 모순 존재 여부, 멀티모달 추출 품질을 사용자에게 숨기지 않는다.
57
+ 5. **문서 동기화**. 기능이 들어가면 `ARCHITECTURE.md`, `FEATURE_STATUS.md`, `RELEASE_NOTES`, `AGENTS.md`의 현재 릴리즈 마커를 같은 커밋에서 업데이트.
58
+ 6. **테스트 전략 변경**. 새 기능은 단위 테스트 + 통합 시나리오 테스트만 추가. 커버리지 %를 억지로 올리지 않는다. 기존 100% 게이트는 회귀만 지킨다.
59
+
60
+ ---
61
+
62
+ ## 3. Track 1 — High-Performance Vector Search + Incremental Indexing
63
+
64
+ ### 목표
65
+ - 50k~100k 노드에서도 hybrid 검색 p50 < 40~60ms
66
+ - 배경 인덱싱으로 사용자가 인제스트 후 바로 검색 가능하게
67
+ - 기존 hybrid (FTS5 + vector) + `context_quality` 신호 유지
68
+
69
+ ### 현재 상태
70
+ - `lattice_brain/graph/retrieval_vector.py` : brute-force + vector freshness
71
+ - `hybrid_search` 존재
72
+ - PERFORMANCE.md 기준 5k에서 벡터 p50 ~1.7s
73
+
74
+ ### 구현 단계
75
+
76
+ #### 1.1 벡터 인덱스 추상화 레이어 도입
77
+ - `lattice_brain/graph/vector_index/` 패키지 신설
78
+ - Protocol 정의:
79
+ ```python
80
+ class VectorIndex(Protocol):
81
+ def add(self, id: str, vector: np.ndarray, metadata: dict) -> None: ...
82
+ def search(self, query: np.ndarray, top_k: int, filter: dict | None = None) -> list[ScoredId]: ...
83
+ def remove(self, id: str) -> None: ...
84
+ def rebuild(self) -> None: ...
85
+ def stats(self) -> IndexStats: ...
86
+ ```
87
+ - 기본 구현체 2개:
88
+ - `BruteForceIndex` (기존 로직 이전, fallback)
89
+ - `HnswIndex` (신규)
90
+
91
+ #### 1.2 HNSW 엔진 선택 & 통합
92
+ **권장 경로 (우선순위)**:
93
+ 1. **단기 (가장 빠름)**: `usearch` 또는 `hnswlib` Python 바인딩으로 프로토타입. Apple Silicon에서 잘 돌아가는지 확인.
94
+ 2. **중기 (권장 최종)**: 순수 Rust HNSW + Python binding (또는 Tauri 쪽 native).
95
+ 후보: `usearch`(이미 Rust 코어), `minimemory`, `leann-core`, `monavec` 계열.
96
+ SQLite 파일과 별도로 `.hnsw` 또는 메모리맵 인덱스를 Brain 디렉터리에 둔다.
97
+ 3. Quantization (int8 / binary) 옵션을 처음부터 넣어서 메모리 절약.
98
+
99
+ #### 1.3 Incremental / Background Indexing
100
+ - 인제스트 파이프라인에 `vector_job` 큐 추가 (이미 `/api/ingestion/jobs` 패턴 존재).
101
+ - 노드 upsert 시 “pending embed” 상태로 표시 → 백그라운드 워커가 임베딩 + 인덱스 추가.
102
+ - `vector_freshness` API를 더 세분화: `embedded / pending / stale / total`.
103
+ - 재인덱싱은 전체 rebuild와 incremental upsert 둘 다 지원.
104
+
105
+ #### 1.4 Hybrid Search 고도화
106
+ - Reciprocal Rank Fusion (RRF) 또는 학습 가능한 fusion weight를 query class별로 적용.
107
+ - graph traversal 결과를 candidate expansion으로 사용 (이미 neighbors/traverse 존재).
108
+ - `context_quality` 점수에 vector freshness와 ANN recall 추정치를 반영.
109
+
110
+ #### 1.5 완료 기준
111
+ - [ ] 10k 합성 노드에서 hybrid p50 < 50ms
112
+ - [ ] 배경 인덱싱이 켜진 상태에서 새 파일 인제스트 후 30초 이내 검색 가능
113
+ - [ ] 기존 hybrid_search API 시그니처 호환 (breaking change 없음)
114
+ - [ ] `docs/PERFORMANCE.md` 업데이트 + 벤치 스크립트 포함
115
+
116
+ ---
117
+
118
+ ## 4. Track 2 — Proactive Synthesis + Contradiction + Temporal
119
+
120
+ ### 목표
121
+ Brain이 수동 검색 도구가 아니라 **스스로 관찰하고 제안하는 존재**가 된다.
122
+
123
+ ### 현재 상태
124
+ - `lattice_brain/graph/proactive.py` : 읽기 전용 (duplicate, contradiction, quality report)
125
+ - Brain Brief, MultiAgentOrchestrator, workflow_engine 존재
126
+
127
+ ### 구현 단계
128
+
129
+ #### 2.1 Contradiction Detection을 쓰기 가능한 루프로
130
+ - 기존 읽기 전용 감지를 **제안 생성**까지 확장.
131
+ - 새 사실 인제스트 시 기존 관련 노드와 비교 → `ContradictionProposal` 생성.
132
+ - UI: “이 두 기억은 모순됩니다. 어떻게 할까요?” (유지 / 새 사실로 교체 / 둘 다 유지하고 시간 범위 표시)
133
+
134
+ #### 2.2 Temporal Model 도입
135
+ - 노드/엣지에 `valid_from`, `valid_to`, `superseded_by` 필드 추가 (스키마 확장).
136
+ - 쿼리 API: `as_of(timestamp)` → 그 시점의 유효 그래프 슬라이스.
137
+ - “2025년 6월에 내가 알고 있던 프로젝트 상태” 같은 질문 지원.
138
+
139
+ #### 2.3 Background Synthesis Jobs
140
+ - 주기적 또는 이벤트 기반 (새 노드 N개 추가 후) synthesis 실행.
141
+ - 출력물:
142
+ - Brain Brief 강화 (주간/월간 요약)
143
+ - 새로운 상위 개념 노드 제안
144
+ - “자주 같이 등장하지만 연결이 없는” 후보 엣지 제안
145
+ - 모두 Change Governor를 통해 제안으로만 들어온다.
146
+
147
+ #### 2.4 Memory Decay & Consolidation (선택적이지만 강력)
148
+ - 접근 빈도 + 시간 경과로 importance score 계산.
149
+ - 낮은 점수 에피소딕 기억은 상위 semantic 노드로 합쳐지는 “수면” 프로세스 시뮬레이션.
150
+ - 사용자에게 “Brain이 정리 중입니다” 신호를 정직하게 보여줌.
151
+
152
+ #### 2.5 완료 기준
153
+ - [ ] 모순이 발생하면 제안이 자동 생성되고 UI에서 리뷰 가능
154
+ - [ ] `as_of` 쿼리가 동작하고 테스트 커버
155
+ - [ ] Brain Brief가 proactive 제안을 포함
156
+ - [ ] 모든 쓰기는 proposal 경로
157
+
158
+ ---
159
+
160
+ ## 5. Track 3 — Multi-modal as First-Class Citizen
161
+
162
+ ### 목표
163
+ 이미지·오디오·(간단)비디오가 텍스트와 동등한 기억 단위가 된다.
164
+
165
+ ### 현재 상태
166
+ - 스키마에 IMAGE / IMAGE_TEXT / CONTAINS_IMAGE 존재
167
+ - discovery에 PIL + pytesseract OCR 일부
168
+ - `allow_multimodal` 플래그 기본 false
169
+ - vision embedding / retrieval 경로 미비
170
+
171
+ ### 구현 단계
172
+
173
+ #### 3.1 임베딩 파이프라인 확장
174
+ - `embedding_providers.py`에 vision 프로바이더 추가 (로컬: MLX 비전 모델 또는 CLIP 계열, 클라우드: 선택적).
175
+ - 텍스트 임베딩과 같은 차원으로 맞추거나, 멀티모달 공통 공간 사용.
176
+ - 이미지 노드에 `embedding` + `ocr_text` + `caption` (비전 LLM 생성) 저장.
177
+
178
+ #### 3.2 인제스트 라우팅
179
+ - 파일 MIME에 따라 IMAGE / AUDIO 노드 생성.
180
+ - 오디오: 로컬 whisper 계열 또는 시스템 전사 → 텍스트 + 타임스탬프 청크.
181
+ - 비디오: 키프레임 추출 + 자막/전사 (1차로 키프레임만 해도 충분).
182
+
183
+ #### 3.3 Retrieval 통합
184
+ - hybrid_search가 이미지 노드도 반환.
185
+ - “지난달 제주에서 찍은 사진 중 회의 관련” 같은 쿼리가 그래프 + 벡터로 동작.
186
+ - UI Evidence 패널에 이미지 썸네일 + 캡션 + 출처 표시.
187
+
188
+ #### 3.4 완료 기준
189
+ - [ ] 이미지 폴더 인제스트 → 검색 가능
190
+ - [ ] 비전 캡션이 그래프에 들어가고 hybrid에 기여
191
+ - [ ] `allow_multimodal=true`일 때만 동작 (기본값 유지 가능)
192
+ - [ ] 추출 품질 점수가 이미지에도 적용
193
+
194
+ ---
195
+
196
+ ## 6. Track 4 — Personal Ontology + Agent-Native Workspace OS
197
+
198
+ ### 목표
199
+ Brain이 “나”를 알고, 에이전트가 워크스페이스를 실제로 조작·진화시킬 수 있게 한다.
200
+
201
+ ### 구현 단계
202
+
203
+ #### 4.1 Personal Ontology / Self-Model
204
+ - 새로운 노드 타입 또는 특수 서브그래프: `Self`, `Preference`, `Decision`, `Habit`, `Relationship`.
205
+ - 대화와 파일에서 자동 추출 → 제안으로만 추가.
206
+ - 에이전트 컨텍스트에 “현재 Self-Model 요약”을 항상 주입할 수 있는 API.
207
+ - 사용자는 Self-Model을 직접 편집·삭제 가능 (ownership).
208
+
209
+ #### 4.2 Agent-Native Workspace
210
+ - AgentRuntime이 파일 생성/수정/이동/삭제 제안을 더 안정적으로 수행.
211
+ - 장기 실행 세션: “이 프로젝트를 정리해줘” → 여러 단계 proposal 체인.
212
+ - Workspace OS (`workspace_os.py`)와 더 깊은 통합: 에이전트가 폴더 구조를 Brain 관점으로 재구성 제안.
213
+ - 약한 로컬 모델(Gemma 등)에서도 “파일 생성까지” 가는 성공률을 높이는 프롬프트/툴 설계 (이미 관심 영역).
214
+
215
+ #### 4.3 완료 기준
216
+ - [ ] Self-Model이 생성되고 채팅 컨텍스트에 반영
217
+ - [ ] 에이전트가 파일 작업을 proposal로 안정적으로 수행
218
+ - [ ] 사용자가 Self-Model을 투명하게 볼 수 있음
219
+
220
+ ---
221
+
222
+ ## 7. Track 5 — Interop + Selective Brain Network
223
+
224
+ ### 목표
225
+ 기존 지식 도구와 연결하고, 선택적으로 다른 Brain과 안전하게 공유.
226
+
227
+ ### 구현 단계
228
+
229
+ #### 5.1 Interop Bridges (우선순위 높은 것부터)
230
+ 1. **Obsidian** — vault 폴더를 감시하거나 수동 동기. 마크다운 → 노드 + 백링크를 엣지로.
231
+ 2. **Notion** — export 또는 API (opt-in) 인제스트.
232
+ 3. **이메일/캘린더** — 로컬 파일 또는 시스템 권한으로 (macOS 중심).
233
+ 4. **Git** — 커밋 메시지 + 변경 파일을 프로젝트 기억으로.
234
+
235
+ 모두 기존 IngestionPipeline 단일 게이트를 통과.
236
+
237
+ #### 5.2 Selective Encrypted Brain Network
238
+ - `.latticebrain` 아카이브의 확장: 전체 Brain이 아니라 **선택적 subgraph + provenance** 내보내기.
239
+ - 수신 측은 “제안으로만” 받아들이고, 출처를 명확히 표시.
240
+ - 암호화는 기존 아카이브 메커니즘 재사용 + 수신자 공개키(선택).
241
+ - UI: “이 결정 그래프만 공유” 같은 단위.
242
+
243
+ #### 5.3 완료 기준
244
+ - [ ] Obsidian vault → Brain 인제스트가 동작
245
+ - [ ] subgraph 공유 → 상대방 Brain에 proposal로 들어옴
246
+ - [ ] 네트워크 기능은 기본 꺼짐 (opt-in)
247
+
248
+ ---
249
+
250
+ ## 8. 권장 실행 일정 (11.1.0 단일 릴리즈 기준)
251
+
252
+ 가정: 집중 작업 기준 3~5주 스프린트.
253
+
254
+ | 주차 | 주요 포커스 | 산출물 |
255
+ |------|-------------|--------|
256
+ | Week 1 | Track 1 전체 (벡터 추상화 + HNSW 프로토타입 + 배경 인덱싱) | 벤치 통과, hybrid API 호환 |
257
+ | Week 2 | Track 2 (모순 제안 + temporal 스키마 + synthesis job) + Track 1 안정화 | proactive UI 초안 |
258
+ | Week 3 | Track 3 (비전 임베딩 + 이미지 인제스트 + retrieval) + Track 4 스키마 | 이미지 검색 가능 |
259
+ | Week 4 | Track 4 구현 + Track 5 브릿지 (Obsidian 우선) | Self-Model + Obsidian |
260
+ | Week 5 | 통합, 폴리시, 문서, 릴리즈 노트, 벤치 재측정 | v11.1.0 태그 |
261
+
262
+ 병렬로 돌릴 수 있는 부분은 최대한 병렬.
263
+ 막히는 지점(특히 Rust 바인딩 성능)은 Python 바인딩으로 먼저 출시하고 다음 마이너에서 네이티브로 교체해도 된다.
264
+
265
+ ---
266
+
267
+ ## 9. 릴리즈 전략 & 문서 게이트
268
+
269
+ - 브랜치: `release/v11.1.0` 또는 `feature/product-intelligence`
270
+ - 모든 트랙이 머지된 후 **한 번의** 통합 테스트 + 성능 벤치 후 태그.
271
+ - `AGENTS.md`, `ARCHITECTURE.md`, `FEATURE_STATUS.md`, `RELEASE_NOTES_v11.1.0.md` 동시 업데이트.
272
+ - CHANGELOG에는 “Product Intelligence Layer”로 포지셔닝.
273
+ - 기존 100% 커버리지 게이트는 유지하되, 신규 코드에 대한 과도한 커버리지 강요는 하지 않는다 (회귀만).
274
+
275
+ ---
276
+
277
+ ## 10. 위험 요소와 완화
278
+
279
+ | 위험 | 완화 |
280
+ |------|------|
281
+ | HNSW 도입으로 검색 품질(recall) 하락 | 기본 fallback을 brute-force로 두고, ANN은 `approx=true` 옵션. recall 벤치 필수. |
282
+ | 멀티모달 임베딩 차원이 텍스트와 다름 | 공통 공간 모델 사용 또는 별도 인덱스 + late fusion. |
283
+ | Temporal 스키마 변경으로 기존 Brain 깨짐 | 마이그레이션 스크립트 + `valid_from` 기본값을 created_at으로. |
284
+ | 에이전트가 너무 공격적으로 제안 | PermissionMode + Change Governor 강화. 기본은 보수적. |
285
+ | 11.1.0 범위가 너무 큼 | Track 5의 Brain Network는 “프로토타입 + 플래그”로 축소 가능. Interop는 Obsidian만 필수. |
286
+
287
+ ---
288
+
289
+ ## 11. 성공의 정의 (v11.1.0 완료 시)
290
+
291
+ 사용자가 다음을 체감해야 한다:
292
+
293
+ 1. **빠르다** — 큰 Brain에서도 검색이 즉각적이다.
294
+ 2. **살아있다** — Brain이 모순을 알려주고, 요약을 해주고, 연결을 제안한다.
295
+ 3. **모든 것을 기억한다** — 사진도, 메모도, 대화도 같은 그래프에 있다.
296
+ 4. **나를 안다** — 내 선호와 결정 이력이 에이전트 행동에 반영된다.
297
+ 5. **연결된다** — Obsidian 등 기존 도구와 자연스럽게 이어지고, 원하면 선택적으로 공유할 수 있다.
298
+
299
+ 이것이 “Model is the voice, Brain is the lifelong asset”을 제품 수준에서 완성한 상태다.
300
+
301
+ ---
302
+
303
+ ## 12. 다음 액션 (바로 시작)
304
+
305
+ 1. 이 문서를 `docs/v11.1.0_PRODUCT_INTELLIGENCE_PLAN.md`로 커밋.
306
+ 2. Track 1부터 이슈/브랜치 생성.
307
+ 3. 벡터 인덱스 Protocol + BruteForce 이전 작업부터 착수.
308
+ 4. 매주 금요일에 “체감 데모” 기준으로 진행 상황 점검 (커버리지 %가 아니라).
309
+
310
+ ---
311
+
312
+ **문서 끝.**
313
+ 이 계획대로면 v11.1.0은 “테스트와 보안의 LatticeAI”에서 **“실제로 쓰는 지능형 Digital Brain”**으로의 전환점이 된다.
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "10.9.0"
29
+ __version__ = "11.0.1"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -173,6 +173,17 @@ class KnowledgeGraphCore:
173
173
  """``{status, pending_items, total_items, detail}`` — never raises."""
174
174
  raise NotImplementedError
175
175
 
176
+ # ── retrieval_reads.py: 1-hop walk (hybrid candidate expansion) ──────────
177
+ def neighbors(
178
+ self,
179
+ node_id: str,
180
+ *,
181
+ allowed_workspaces: Any = None,
182
+ include_legacy_global: bool = False,
183
+ as_of: Any = None,
184
+ ) -> Dict[str, Any]:
185
+ raise NotImplementedError
186
+
176
187
  # ── retrieval_reads.py: workspace scoping ────────────────────────────────
177
188
  def filter_scoped_nodes(
178
189
  self,
@@ -196,7 +196,7 @@ def extract_topic_candidates(
196
196
  seen_in_doc: Set[str] = set()
197
197
  for term in bag:
198
198
  if term in seen_in_doc:
199
- continue
199
+ continue # pragma: no cover — unreachable: bag is list(set(...)), already deduplicated
200
200
  seen_in_doc.add(term)
201
201
  counts[term] = counts.get(term, 0.0) + weight
202
202
  sources.setdefault(term, []).append(str(doc.get("id") or ""))
@@ -28,7 +28,7 @@ from __future__ import annotations
28
28
  import json
29
29
  import os
30
30
  import re
31
- from typing import Any, Dict, Mapping, Optional
31
+ from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple
32
32
 
33
33
  from ..quiet import quiet
34
34
 
@@ -36,6 +36,26 @@ QUERY_CLASSES = ("fact", "code", "person", "recency")
36
36
 
37
37
  FUSION_WEIGHTS_ENV = "LATTICEAI_FUSION_WEIGHTS"
38
38
 
39
+ # ── fusion strategy (v11.1.0) ────────────────────────────────────────────────
40
+ # Linear alpha fusion needs the two channels' scores to be comparable. They
41
+ # are not: the lexical channel scores 1/rank while the vector channel scores a
42
+ # max-normalized cosine, so a query where every cosine lands near 0.9 flattens
43
+ # the vector channel to noise. Reciprocal Rank Fusion ignores score magnitudes
44
+ # entirely and fuses *positions*, which is exactly the property that survives
45
+ # incomparable scales.
46
+ #
47
+ # It is opt-in, per query class, and off everywhere by default: alpha fusion
48
+ # is what every existing ranking assertion in this repo describes, and a
49
+ # ranking change dressed up as a bug fix is how retrieval quality regressions
50
+ # get shipped. Turn it on with LATTICEAI_FUSION_STRATEGY=rrf (all classes) or
51
+ # a JSON object like {"code": "rrf"} (per class).
52
+ FUSION_STRATEGY_ENV = "LATTICEAI_FUSION_STRATEGY"
53
+ FUSION_STRATEGIES = ("alpha", "rrf")
54
+ DEFAULT_FUSION_STRATEGY: Dict[str, str] = dict.fromkeys(QUERY_CLASSES, "alpha")
55
+ #: The smoothing constant from the original RRF paper (Cormack et al., 2009).
56
+ #: Larger k flattens the curve, so rank 1 wins by less.
57
+ DEFAULT_RRF_K = 60
58
+
39
59
  # Per-class fusion weights. "fact" mirrors the pre-fusion-gate defaults
40
60
  # (SearchService DEFAULT_HYBRID_WEIGHTS + graph-layer alpha=0.6) so the
41
61
  # fallback class introduces zero behavioral drift.
@@ -145,16 +165,163 @@ def fusion_weight_table(
145
165
  return table
146
166
 
147
167
 
168
+ def _env_strategy_overrides() -> Dict[str, str]:
169
+ """Parse ``LATTICEAI_FUSION_STRATEGY`` → per-class strategy overrides.
170
+
171
+ Two accepted forms: a bare strategy name (``rrf``) applies to every class,
172
+ a JSON object (``{"code": "rrf"}``) applies per class. Anything else —
173
+ a typo, malformed JSON, an unknown strategy — is ignored rather than
174
+ raising, so a bad env var degrades to the default fusion instead of
175
+ taking every search down.
176
+ """
177
+ raw = os.getenv(FUSION_STRATEGY_ENV, "").strip()
178
+ if not raw:
179
+ return {}
180
+ if raw.lower() in FUSION_STRATEGIES:
181
+ return dict.fromkeys(QUERY_CLASSES, raw.lower())
182
+ try:
183
+ payload = json.loads(raw)
184
+ except (ValueError, TypeError):
185
+ return {}
186
+ if not isinstance(payload, dict):
187
+ return {}
188
+ return {
189
+ str(cls): str(value).lower()
190
+ for cls, value in payload.items()
191
+ if cls in DEFAULT_FUSION_STRATEGY and str(value).lower() in FUSION_STRATEGIES
192
+ }
193
+
194
+
195
+ def fusion_strategy_table(
196
+ overrides: Optional[Mapping[str, str]] = None,
197
+ ) -> Dict[str, str]:
198
+ """Full per-class strategy table: defaults ← env override ← caller."""
199
+ table = dict(DEFAULT_FUSION_STRATEGY)
200
+ for source in (_env_strategy_overrides(), overrides or {}):
201
+ for cls, value in source.items():
202
+ if cls in table and str(value).lower() in FUSION_STRATEGIES:
203
+ table[cls] = str(value).lower()
204
+ return table
205
+
206
+
207
+ def rrf_score(rank: int, *, k: int = DEFAULT_RRF_K) -> float:
208
+ """One channel's contribution for an item it ranked at ``rank`` (1-based)."""
209
+ return 1.0 / (int(k) + max(1, int(rank)))
210
+
211
+
212
+ def rrf_fuse(
213
+ rankings: Mapping[str, Sequence[str]],
214
+ *,
215
+ k: int = DEFAULT_RRF_K,
216
+ weights: Optional[Mapping[str, float]] = None,
217
+ ) -> Dict[str, float]:
218
+ """Reciprocal Rank Fusion over per-channel id rankings.
219
+
220
+ ``rankings`` maps a channel name to its ids in rank order (best first).
221
+ An id absent from a channel simply contributes nothing from it — RRF has
222
+ no notion of a "missing score" to guess at, which is the other reason it
223
+ survives channels that fail independently.
224
+
225
+ ``weights`` optionally scales a channel's contribution (a channel missing
226
+ from the mapping weighs 1.0). Deterministic and pure.
227
+ """
228
+ fused: Dict[str, float] = {}
229
+ for channel, ordered in rankings.items():
230
+ weight = 1.0 if weights is None else float(weights.get(channel, 1.0))
231
+ for rank, item_id in enumerate(ordered, start=1):
232
+ fused[item_id] = fused.get(item_id, 0.0) + weight * rrf_score(rank, k=k)
233
+ return fused
234
+
235
+
236
+ # ── graph traversal candidate expansion (v11.1.0) ────────────────────────────
237
+ # Both retrieval channels can only return what they matched. A note that
238
+ # never says "postgres" but is one CONTAINS edge away from the one that does
239
+ # is unreachable by either — and it is often the answer. Expansion walks one
240
+ # hop out from the top hits and offers those neighbours as candidates.
241
+ #
242
+ # It is capped, off by default, and reports its own counts, because the
243
+ # failure mode is dilution: an unbounded expansion turns a precise answer into
244
+ # a tour of the graph.
245
+ GRAPH_EXPANSION_ENV = "LATTICEAI_GRAPH_EXPANSION"
246
+ DEFAULT_EXPANSION_SEEDS = 3
247
+ DEFAULT_EXPANSION_CAP = 5
248
+ #: Expanded candidates inherit a damped share of their seed's score: they are
249
+ #: related to a hit, they are not themselves a hit.
250
+ EXPANSION_DECAY = 0.5
251
+
252
+
253
+ def graph_expansion_enabled() -> bool:
254
+ """True when ``LATTICEAI_GRAPH_EXPANSION`` opts in (default: off)."""
255
+ raw = os.getenv(GRAPH_EXPANSION_ENV, "").strip().lower()
256
+ return raw in {"1", "true", "yes", "on"}
257
+
258
+
259
+ def expand_with_neighbors(
260
+ seeds: Sequence[Tuple[str, float]],
261
+ neighbors_fn: Callable[[str], Any],
262
+ *,
263
+ exclude: Optional[Sequence[str]] = None,
264
+ cap: int = DEFAULT_EXPANSION_CAP,
265
+ ) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
266
+ """One-hop neighbours of ``seeds`` as extra candidates + an honest report.
267
+
268
+ ``seeds`` is ``[(node_id, score)]`` best first; ``neighbors_fn`` is the
269
+ store's ``neighbors``. Returns ``(candidates, report)`` where each
270
+ candidate carries its originating seed and a decayed score, and the report
271
+ states how many seeds were walked, how many candidates were kept, and
272
+ whether the cap bit. A neighbour lookup that fails is skipped and counted,
273
+ never raised: expansion is an optimization, not a dependency.
274
+ """
275
+ known = {str(item) for item in (exclude or ())}
276
+ cap = max(0, int(cap))
277
+ candidates: List[Dict[str, Any]] = []
278
+ failures = 0
279
+ for seed_id, seed_score in seeds:
280
+ if len(candidates) >= cap:
281
+ break
282
+ try:
283
+ payload = neighbors_fn(seed_id) or {}
284
+ except Exception: # noqa: BLE001 — a bad seed must not fail the search
285
+ failures += 1
286
+ continue
287
+ for node in payload.get("neighbors") or []:
288
+ node_id = str(node.get("id") or "")
289
+ if not node_id or node_id in known:
290
+ continue
291
+ known.add(node_id)
292
+ candidates.append(
293
+ {
294
+ "node": node,
295
+ "seed": str(seed_id),
296
+ "score": round(float(seed_score) * EXPANSION_DECAY, 6),
297
+ }
298
+ )
299
+ if len(candidates) >= cap:
300
+ break
301
+ report = {
302
+ "enabled": True,
303
+ "seeds": len(seeds),
304
+ "added": len(candidates),
305
+ "cap": cap,
306
+ "truncated": len(candidates) >= cap,
307
+ "failed_seeds": failures,
308
+ }
309
+ return candidates, report
310
+
311
+
148
312
  def fusion_profile(
149
313
  query: Any,
150
314
  *,
151
315
  overrides: Optional[Mapping[str, Mapping[str, float]]] = None,
316
+ strategies: Optional[Mapping[str, str]] = None,
152
317
  ) -> Dict[str, Any]:
153
318
  """Classify ``query`` and return its resolved fusion weights.
154
319
 
155
320
  Returns ``{"query_class": str, "weights": {keyword, vector, graph},
156
- "alpha": float}``. ``weights`` feeds the three-channel service fusion;
157
- ``alpha`` feeds the two-channel graph-layer fusion.
321
+ "alpha": float, "strategy": "alpha"|"rrf"}``. ``weights`` feeds the
322
+ three-channel service fusion; ``alpha`` feeds the two-channel graph-layer
323
+ fusion; ``strategy`` selects *how* those channels combine and is
324
+ ``"alpha"`` for every class unless configured otherwise.
158
325
  """
159
326
  query_class = classify_query(query)
160
327
  table = fusion_weight_table(overrides)
@@ -170,14 +337,28 @@ def fusion_profile(
170
337
  "graph": resolved["graph"],
171
338
  },
172
339
  "alpha": resolved["alpha"],
340
+ "strategy": fusion_strategy_table(strategies)[query_class],
173
341
  }
174
342
 
175
343
 
176
344
  __all__ = [
345
+ "DEFAULT_EXPANSION_CAP",
346
+ "DEFAULT_EXPANSION_SEEDS",
347
+ "DEFAULT_FUSION_STRATEGY",
177
348
  "DEFAULT_FUSION_WEIGHTS",
349
+ "DEFAULT_RRF_K",
350
+ "EXPANSION_DECAY",
351
+ "FUSION_STRATEGIES",
352
+ "FUSION_STRATEGY_ENV",
178
353
  "FUSION_WEIGHTS_ENV",
354
+ "GRAPH_EXPANSION_ENV",
179
355
  "QUERY_CLASSES",
180
356
  "classify_query",
357
+ "expand_with_neighbors",
181
358
  "fusion_profile",
359
+ "fusion_strategy_table",
182
360
  "fusion_weight_table",
361
+ "graph_expansion_enabled",
362
+ "rrf_fuse",
363
+ "rrf_score",
183
364
  ]