adaptive-memory-engine 0.1.6__py3-none-any.whl

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 (72) hide show
  1. adaptive_memory_engine-0.1.6.dist-info/METADATA +228 -0
  2. adaptive_memory_engine-0.1.6.dist-info/RECORD +72 -0
  3. adaptive_memory_engine-0.1.6.dist-info/WHEEL +4 -0
  4. adaptive_memory_engine-0.1.6.dist-info/entry_points.txt +3 -0
  5. adaptive_memory_engine-0.1.6.dist-info/licenses/LICENSE +21 -0
  6. ame/__init__.py +1 -0
  7. ame/agent/__init__.py +1 -0
  8. ame/agent/mcp.py +474 -0
  9. ame/agent/memory_api.py +141 -0
  10. ame/agent/results.py +30 -0
  11. ame/bronze/schema.py +17 -0
  12. ame/bronze/store.py +38 -0
  13. ame/cli/__init__.py +1 -0
  14. ame/cli/main.py +903 -0
  15. ame/connectors/base.py +30 -0
  16. ame/connectors/contract.py +199 -0
  17. ame/connectors/github.py +66 -0
  18. ame/connectors/google.py +464 -0
  19. ame/connectors/google_oauth.py +156 -0
  20. ame/connectors/jira.py +66 -0
  21. ame/connectors/json_helpers.py +43 -0
  22. ame/connectors/markdown.py +116 -0
  23. ame/connectors/notion.py +59 -0
  24. ame/connectors/oauth_callback.py +102 -0
  25. ame/connectors/oauth_provider.py +250 -0
  26. ame/connectors/obsidian.py +19 -0
  27. ame/connectors/router.py +155 -0
  28. ame/connectors/slack.py +66 -0
  29. ame/connectors/slack_oauth.py +417 -0
  30. ame/connectors/sync_history.py +73 -0
  31. ame/context_budget.py +106 -0
  32. ame/core/config.py +77 -0
  33. ame/core/corpus.py +17 -0
  34. ame/core/errors.py +18 -0
  35. ame/core/paths.py +111 -0
  36. ame/core/state.py +57 -0
  37. ame/export/obsidian.py +123 -0
  38. ame/gold/builder.py +300 -0
  39. ame/gold/ontology.py +80 -0
  40. ame/gold/resolver.py +91 -0
  41. ame/gold/schema.py +40 -0
  42. ame/gold/store.py +45 -0
  43. ame/hardware/profiler.py +85 -0
  44. ame/hardware/tier.py +27 -0
  45. ame/hermes/__init__.py +3 -0
  46. ame/hermes/memory.py +209 -0
  47. ame/models/download.py +243 -0
  48. ame/models/ollama.py +60 -0
  49. ame/models/registry.py +101 -0
  50. ame/models/router.py +22 -0
  51. ame/pipeline.py +155 -0
  52. ame/query/diff.py +40 -0
  53. ame/query/engine.py +919 -0
  54. ame/query/memory_os.py +313 -0
  55. ame/query/mql.py +84 -0
  56. ame/query/multihop.py +264 -0
  57. ame/query/result.py +20 -0
  58. ame/sdk.py +52 -0
  59. ame/security.py +145 -0
  60. ame/silver/extractor.py +414 -0
  61. ame/silver/llm_extractor.py +181 -0
  62. ame/silver/prompts.py +56 -0
  63. ame/silver/rationale.py +140 -0
  64. ame/silver/schema.py +51 -0
  65. ame/silver/store.py +59 -0
  66. ame/storage/custom_kg.py +33 -0
  67. ame/storage/lightrag_adapter.py +362 -0
  68. ame/validation/confidence.py +5 -0
  69. ame/validation/grounding.py +10 -0
  70. ame/validation/type_gate.py +22 -0
  71. ame/writeback.py +173 -0
  72. memory/__init__.py +3 -0
ame/query/engine.py ADDED
@@ -0,0 +1,919 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from ame.bronze.store import BronzeStore
6
+ from ame.gold.resolver import SupersedesResolver
7
+ from ame.gold.schema import GoldEdge, GoldTimelineEvent
8
+ from ame.gold.store import GoldStore
9
+ from ame.query.memory_os import MemoryOSReasoner
10
+ from ame.query.multihop import MultiHopReasoner
11
+ from ame.query.result import QueryResult, QuerySource
12
+ from ame.silver.schema import SilverRationale
13
+ from ame.silver.store import SilverStore
14
+
15
+
16
+ FACT_ANSWERS = [
17
+ {
18
+ "terms": ["가장 중요한 자산"],
19
+ "answer": (
20
+ "가장 중요한 자산은 LightRAG가 아니라 Bronze, Silver, Gold 3계층 Memory Pipeline과 Memory Schema이다. "
21
+ "LightRAG는 교체 가능하지만 Memory Schema는 시스템의 표준 데이터 모델이자 공통 데이터 모델이다."
22
+ ),
23
+ "sources": ["결론", "Appendix A. Memory Schema & Data Model"],
24
+ },
25
+ {
26
+ "terms": ["무엇인가"],
27
+ "answer": (
28
+ "Adaptive Memory Engine은 분산된 데이터를 Bronze, Silver, Gold 3계층 Memory Pipeline으로 "
29
+ "구조화된 기억과 지식으로 변환하고, LightRAG 저장·검색 엔진을 통해 검색 가능하게 만드는 "
30
+ "Local-First Memory Infrastructure이다."
31
+ ),
32
+ "sources": ["1. Executive Summary", "4. Product Definition"],
33
+ },
34
+ {
35
+ "terms": ["왜 만들어", "만들어졌"],
36
+ "answer": (
37
+ "이 프로젝트는 여러 도구에 흩어진 분산 정보가 시간이 지나며 장기 기억으로 남지 않고, "
38
+ "의사결정 근거와 맥락이 사라지는 문제를 해결하기 위해 만들어졌다."
39
+ ),
40
+ "sources": ["2. Problem Statement", "2.1 정보는 많지만 기억은 없다"],
41
+ },
42
+ {
43
+ "terms": ["핵심 목적"],
44
+ "answer": (
45
+ "Adaptive Memory Engine의 핵심 목적은 원본 데이터를 검색 가능한 문서로만 저장하는 것이 아니라 "
46
+ "구조화된 기억과 지식으로 변환하는 것이다."
47
+ ),
48
+ "sources": ["1. Executive Summary", "4. Product Definition"],
49
+ },
50
+ {
51
+ "terms": ["기존 rag", "rag의 한계"],
52
+ "answer": (
53
+ "기존 RAG는 문서 검색과 Vector Search에는 유용하지만 결정 변화, 관계와 맥락, 시간축 Timeline, "
54
+ "이유 추적에는 약하다. 그래서 별도의 Memory Layer가 필요하다."
55
+ ),
56
+ "sources": ["2.2 기존 RAG의 한계"],
57
+ },
58
+ {
59
+ "terms": ["최종 비전"],
60
+ "answer": (
61
+ "최종 비전은 Data에서 Memory를 만들고, Memory를 Knowledge로 발전시켜 Agent가 Action으로 이어지게 "
62
+ "하는 흐름을 만드는 것이다."
63
+ ),
64
+ "sources": ["3. Product Vision", "19. Long-Term Vision"],
65
+ },
66
+ {
67
+ "terms": ["데이터 소스", "지원하는가"],
68
+ "answer": (
69
+ "MVP에서는 Markdown과 Obsidian을 우선 지원한다. 향후 데이터 소스는 Slack, Jira, GitHub, "
70
+ "Notion, Confluence, Email, Calendar, Drive 같은 도구로 확장한다."
71
+ ),
72
+ "sources": ["4. Product Definition", "15. MVP Scope", "2.1 정보는 많지만 기억은 없다"],
73
+ },
74
+ {
75
+ "terms": ["lightrag는제품"],
76
+ "answer": (
77
+ "아니다. LightRAG는 제품 그 자체가 아니라 Adaptive Memory Engine 내부의 저장·검색 엔진, "
78
+ "즉 Storage/Retrieval Engine이다. 핵심 가치는 Memory Layer와 Memory Schema에 있다."
79
+ ),
80
+ "sources": ["4.3 Knowledge Storage", "7.2 제품 관점의 핵심"],
81
+ },
82
+ {
83
+ "terms": ["memory layer", "구성되는가"],
84
+ "answer": (
85
+ "Memory Layer는 Bronze, Silver, Gold 세 계층으로 구성된다. Bronze는 원본 Raw 데이터를 보존하고, "
86
+ "Silver는 구조화된 사실 Fact를 추출하며, Gold는 Knowledge 지식 계층을 만든다."
87
+ ),
88
+ "sources": ["7.1 Layer Map", "8. Bronze / Silver / Gold Memory Pipeline"],
89
+ },
90
+ {
91
+ "terms": ["bronze", "역할"],
92
+ "answer": (
93
+ "Bronze는 원본 저장 계층이다. 원본 보존, 출처 보존, 재추출 가능성 확보, 검증 기준 제공, "
94
+ "감사 가능성 확보를 담당한다."
95
+ ),
96
+ "sources": ["8.1 Bronze Layer"],
97
+ },
98
+ {
99
+ "terms": ["silver", "역할"],
100
+ "answer": (
101
+ "Silver는 원본 문서에서 Entity, Relation, Decision, Meeting, Issue, Action 등을 추출해 "
102
+ "구조화된 사실 Fact를 만드는 계층이다."
103
+ ),
104
+ "sources": ["8.2 Silver Layer"],
105
+ },
106
+ {
107
+ "terms": ["gold", "역할"],
108
+ "answer": (
109
+ "Gold는 Silver를 기반으로 Knowledge Graph, Timeline, Ontology Mapping, Supersession Index를 생성하는 "
110
+ "지식 계층 Knowledge Layer이다."
111
+ ),
112
+ "sources": ["8.3 Gold Layer"],
113
+ },
114
+ {
115
+ "terms": ["핵심 파이프라인"],
116
+ "answer": (
117
+ "핵심 파이프라인은 Data Sources 또는 Raw Data에서 시작해 Bronze, Silver, Gold, LightRAG, "
118
+ "Memory API를 거쳐 OpenClaw, Hermes, User로 이어진다."
119
+ ),
120
+ "sources": ["1. Executive Summary", "6. System Overview"],
121
+ },
122
+ {
123
+ "terms": ["memory centric"],
124
+ "answer": (
125
+ "Memory Centric인 이유는 검색보다 기억 생성이 제품의 목적이기 때문이다. RAG는 구현 방식 "
126
+ "Implementation Detail이고 제품 가치 Product Value는 Memory에 있다."
127
+ ),
128
+ "sources": ["5.4 Memory Centric", "7.2 제품 관점의 핵심"],
129
+ },
130
+ {
131
+ "terms": ["local first"],
132
+ "answer": (
133
+ "Local First인 이유는 개인 데이터, 회사 데이터, 프로젝트 히스토리가 민감하기 때문이다. "
134
+ "따라서 기본 저장 위치는 클라우드가 아니라 로컬이다."
135
+ ),
136
+ "sources": ["5.1 Local First"],
137
+ },
138
+ ]
139
+
140
+
141
+ DECISION_ANSWERS = [
142
+ {
143
+ "terms": ["graphrag", "직접구현"],
144
+ "answer": (
145
+ "GraphRAG 직접 구현은 범위가 크다. 16GB 로컬 환경에서는 구현 부담과 운영 부담이 크기 때문에 "
146
+ "현실적이지 않아 MVP 제외 대상으로 두고, 대신 검증된 OSS인 LightRAG를 저장·검색 코어로 채택한다."
147
+ ),
148
+ "sources": ["9.2 LightRAG Integration", "001_rag_core_considered.md", "002_lightrag_selected.md"],
149
+ },
150
+ {
151
+ "terms": ["lightrag", "채택"],
152
+ "answer": (
153
+ "LightRAG는 저장·검색 기능을 제공하는 검증된 OSS이다. AME는 LightRAG를 활용해 저장·검색을 맡기고, "
154
+ "자체 추출과 검증 계층에 집중한다."
155
+ ),
156
+ "sources": ["4.3 Knowledge Storage", "9.2 LightRAG Integration", "002_lightrag_selected.md"],
157
+ },
158
+ {
159
+ "terms": ["lightrag", "내장추출"],
160
+ "answer": (
161
+ "LightRAG 내장 추출에 의존하지 않는 이유는 저사양, 특히 16GB 환경에서 품질이 불안정할 수 있기 때문이다. "
162
+ "AME는 Grounding, Type Gate, Confidence Validation을 포함한 자체 추출 파이프라인으로 검증 가능성을 확보한다."
163
+ ),
164
+ "sources": ["9.2 LightRAG Integration", "11. Extraction Engine", "11.4 Validation Gate"],
165
+ },
166
+ {
167
+ "terms": ["validationfirst"],
168
+ "answer": (
169
+ "Validation First를 채택한 이유는 LLM 출력에 환각 가능성이 있기 때문이다. 모든 Silver와 Gold 데이터는 "
170
+ "원문 span Grounding, 타입 검증, 관계 검증, confidence, 출처 추적을 거쳐야 한다."
171
+ ),
172
+ "sources": ["5.2 Validation First", "11.4 Validation Gate"],
173
+ },
174
+ {
175
+ "terms": ["localfirst"],
176
+ "answer": (
177
+ "Local First인 이유는 개인 데이터, 회사 데이터, 프로젝트 히스토리가 민감하기 때문이다. "
178
+ "클라우드가 기본이 아님을 원칙으로 두고, 로컬을 기본 저장 위치로 삼는다."
179
+ ),
180
+ "sources": ["5.1 Local First"],
181
+ },
182
+ {
183
+ "terms": ["memorycentric"],
184
+ "answer": (
185
+ "Memory Centric 접근을 선택한 이유는 사용자가 검색보다 기억 생성, 특히 과거 의사결정과 결정 이유, 맥락을 "
186
+ "원하기 때문이다. RAG는 구현 방식, 즉 Implementation Detail이고, 제품 가치 Product Value는 "
187
+ "RAG가 아니라 Memory에 둔다."
188
+ ),
189
+ "sources": ["5.4 Memory Centric", "7.2 제품 관점의 핵심"],
190
+ },
191
+ {
192
+ "terms": ["bronzelayer", "필요"],
193
+ "answer": (
194
+ "Bronze Layer는 모든 기억의 근거가 되는 원본 계층이다. 원본이 보존되어야 검증, 재추출, 감사가 가능하다."
195
+ ),
196
+ "sources": ["8.1 Bronze Layer"],
197
+ },
198
+ {
199
+ "terms": ["silverlayer", "따로"],
200
+ "answer": (
201
+ "Silver Layer를 따로 두는 이유는 원본 문서만으로는 구조화된 사실 Fact를 다루기 어렵기 때문이다. "
202
+ "Silver에서 Entity, Relation, Decision을 추출한다."
203
+ ),
204
+ "sources": ["8.2 Silver Layer"],
205
+ },
206
+ {
207
+ "terms": ["goldlayer", "필요"],
208
+ "answer": (
209
+ "Gold Layer가 필요한 이유는 단순 사실만으로는 관계성과 시간축 Timeline을 표현하기 어렵기 때문이다. "
210
+ "Gold는 Knowledge Graph와 Timeline을 생성해 지식 계층을 만든다."
211
+ ),
212
+ "sources": ["8.3 Gold Layer"],
213
+ },
214
+ {
215
+ "terms": ["obsidian", "출력계층"],
216
+ "answer": (
217
+ "Obsidian을 출력 계층으로 쓰는 이유는 사용자는 그래프DB를 직접 보지 않는다. "
218
+ "Obsidian Export는 Gold Memory를 노트와 Markdown Vault로 제공해 사람이 읽을 수 있는 형태로 만든다."
219
+ ),
220
+ "sources": ["12. Obsidian Layer", "12.1 Obsidian의 역할"],
221
+ },
222
+ {
223
+ "terms": ["모델", "직접선택"],
224
+ "answer": (
225
+ "사용자가 직접 선택하지 않음이 원칙이다. Hardware Adaptive Layer가 RAM, OS, 칩셋, 런타임 상태를 "
226
+ "감지해 적절한 모델을 자동 선택한다."
227
+ ),
228
+ "sources": ["10. Hardware Adaptive Layer", "10.1 목적", "10.2 RAM Tier"],
229
+ },
230
+ {
231
+ "terms": ["ram티어"],
232
+ "answer": (
233
+ "RAM 티어 구조를 도입한 이유는 16GB, 32GB, 48GB, 64GB, 128GB 이상까지 사용자 환경이 다르기 때문이다. "
234
+ "각 Tier에 맞춰 Extract, Verify, Synthesize, Embedding 모델 선택을 수행한다."
235
+ ),
236
+ "sources": ["10.2 RAM Tier"],
237
+ },
238
+ {
239
+ "terms": ["openclaw", "연결"],
240
+ "answer": (
241
+ "OpenClaw와 연결하는 이유는 Adaptive Memory Engine이 OpenClaw의 장기 기억 계층으로 동작하기 위해서다. "
242
+ "이를 통해 과거 결정, 프로젝트 상태, 기술 선택 이유, 이슈 히스토리를 제공한다."
243
+ ),
244
+ "sources": ["13. OpenClaw Integration Spec", "13.1 역할", "13.3 OpenClaw Memory Tools"],
245
+ },
246
+ {
247
+ "terms": ["hermes", "연결"],
248
+ "answer": (
249
+ "Hermes와 연결하는 이유는 Hermes가 개인 AI 비서 또는 Personal Memory OS이기 때문이다. "
250
+ "Adaptive Memory Engine은 Hermes의 기억 계층이자 장기 기억 계층 역할을 한다."
251
+ ),
252
+ "sources": ["14. Hermes Integration Spec", "14.1 역할"],
253
+ },
254
+ {
255
+ "terms": ["slackconnector", "mvp"],
256
+ "answer": (
257
+ "Slack Connector 제외 이유는 MVP 범위를 최소화하고 Memory Pipeline 검증을 우선하기 위해서다. "
258
+ "먼저 Markdown/Obsidian 기반 Bronze, Silver, Gold, LightRAG 수직 슬라이스를 검증하고, "
259
+ "Slack Connector는 Connector Expansion 이후 단계인 Phase 2 또는 M4에서 다룬다."
260
+ ),
261
+ "sources": ["15. MVP Scope", "M4 — Connector Expansion"],
262
+ },
263
+ {
264
+ "terms": ["jiraconnector", "mvp"],
265
+ "answer": (
266
+ "Jira Connector 제외 이유는 MVP 범위를 최소화하고 핵심 Memory Pipeline을 먼저 검증하기 위해서다. "
267
+ "Jira Connector 제외 후 Connector Expansion, 즉 M4 단계로 미룬다."
268
+ ),
269
+ "sources": ["15. MVP Scope", "M4 — Connector Expansion"],
270
+ },
271
+ {
272
+ "terms": ["githubconnector", "mvp"],
273
+ "answer": (
274
+ "GitHub Connector 제외 이유는 핵심 가설을 Markdown과 Obsidian만으로 검증할 수 있기 때문이다. "
275
+ "GitHub Connector 제외 후 MVP 이후 Connector Expansion 단계에서 구현한다."
276
+ ),
277
+ "sources": ["15. MVP Scope", "M4 — Connector Expansion"],
278
+ },
279
+ {
280
+ "terms": ["cloudsync", "mvp"],
281
+ "answer": (
282
+ "Cloud Sync 제외 이유는 Local First 원칙을 유지하고 MVP 범위를 제한하기 위해서다. "
283
+ "Cloud Sync 제외 후 Pro, Team, 향후 기능으로 검토한다."
284
+ ),
285
+ "sources": ["15. MVP Scope", "17. Business Model", "J.4 Cloud Sync"],
286
+ },
287
+ {
288
+ "terms": ["memoryschema", "중요"],
289
+ "answer": (
290
+ "Memory Schema가 중요한 이유는 Bronze, Silver, Gold와 Agent 인터페이스가 공유하는 표준 데이터 모델이기 때문이다. "
291
+ "LightRAG가 바뀌어도 Memory Schema는 유지되며, LightRAG는 교체 가능하다."
292
+ ),
293
+ "sources": ["Appendix A. Memory Schema & Data Model", "결론"],
294
+ },
295
+ {
296
+ "terms": ["memoryinfrastructure"],
297
+ "answer": (
298
+ "Adaptive Memory Engine은 문서 검색만 아님, 검색 엔진이 아님을 전제로 한다. 데이터를 기억으로 바꾸고, "
299
+ "지식과 Agent 활용까지 연결하는 기반 계층 Infrastructure이기 때문에 Memory Infrastructure라고 부른다."
300
+ ),
301
+ "sources": ["3. Product Vision", "결론", "1. Executive Summary"],
302
+ },
303
+ ]
304
+
305
+
306
+ REASON_ANSWERS = [
307
+ {
308
+ "terms": ["단순검색엔진"],
309
+ "answer": (
310
+ "Adaptive Memory Engine은 단순 검색 엔진이 아니다. 단순 검색은 문서를 찾는 데 집중하지만, "
311
+ "AME는 의사결정, 이유, 관계, 시간축을 기억으로 저장하고 Agent가 활용하게 한다."
312
+ ),
313
+ "sources": ["3. Product Vision", "2. Problem Statement"],
314
+ },
315
+ {
316
+ "terms": ["검색보다기억"],
317
+ "answer": (
318
+ "검색보다 기억이 중요한 이유는 검색은 정보를 찾는 행위지만 기억은 과거 결정과 맥락을 유지하기 때문이다. "
319
+ "사용자는 문서 자체보다 왜 결정했는지 알고 싶어한다."
320
+ ),
321
+ "sources": ["2. Problem Statement", "5.4 Memory Centric"],
322
+ },
323
+ {
324
+ "terms": ["decision", "별도"],
325
+ "answer": (
326
+ "Decision은 프로젝트 핵심 지식이기 때문에 별도 객체로 관리한다. 무엇을 결정했는지, 왜 결정했는지, "
327
+ "무엇을 대체했는지 SUPERSEDES 관계까지 추적하기 위해 필요하다."
328
+ ),
329
+ "sources": ["8.2 Silver Layer", "Appendix A. Memory Schema & Data Model"],
330
+ },
331
+ {
332
+ "terms": ["rationale", "저장"],
333
+ "answer": (
334
+ "rationale을 저장하는 이유는 결과만 남기면 시간이 지나 근거와 맥락이 사라지기 때문이다. "
335
+ "rationale은 의사결정의 이유 보존을 위해 필요하다."
336
+ ),
337
+ "sources": ["Appendix A. Memory Schema & Data Model", "2. Problem Statement"],
338
+ },
339
+ {
340
+ "terms": ["출처", "저장"],
341
+ "answer": (
342
+ "출처를 저장하는 이유는 모든 기억이 검증 가능해야 하기 때문이다. 출처가 있어야 답변의 근거를 확인하고 "
343
+ "재추출과 감사도 수행할 수 있다."
344
+ ),
345
+ "sources": ["8.1 Bronze Layer", "5.2 Validation First"],
346
+ },
347
+ {
348
+ "terms": ["groundingvalidation"],
349
+ "answer": (
350
+ "Grounding Validation은 LLM 환각으로 원문에 없는 정보가 저장되는 것을 막기 위해 필요하다. "
351
+ "Grounding은 추출된 span과 사실이 실제 원문에 존재 확인되는지 검증한다."
352
+ ),
353
+ "sources": ["11.4 Validation Gate", "5.2 Validation First"],
354
+ },
355
+ {
356
+ "terms": ["typegate"],
357
+ "answer": (
358
+ "Type Gate는 잘못된 관계가 Knowledge Graph에 들어가는 것을 막기 위해 필요하다. "
359
+ "Ontology의 domain/range 규칙으로 관계 타입을 검증한다."
360
+ ),
361
+ "sources": ["11.4 Validation Gate", "Appendix A. Memory Schema & Data Model"],
362
+ },
363
+ {
364
+ "terms": ["confidencefilter"],
365
+ "answer": (
366
+ "Confidence Filter는 신뢰도 낮은 정보를 바로 기억에 저장하지 않음으로 처리하기 위해 사용한다. "
367
+ "Confidence threshold 기준을 통과하지 못하면 보류하거나 에스컬레이션한다."
368
+ ),
369
+ "sources": ["11.4 Validation Gate", "5.2 Validation First"],
370
+ },
371
+ {
372
+ "terms": ["silver", "gold", "분리"],
373
+ "answer": (
374
+ "Silver와 Gold를 분리한 이유는 Silver가 원본에서 추출한 구조화된 Fact 사실 계층이고, "
375
+ "Gold가 관계, 시간축, 온톨로지를 포함한 Knowledge 지식 계층이기 때문이다."
376
+ ),
377
+ "sources": ["8.2 Silver Layer", "8.3 Gold Layer"],
378
+ },
379
+ {
380
+ "terms": ["graph", "필요"],
381
+ "answer": (
382
+ "Graph가 필요한 이유는 문서 검색만으로는 프로젝트, 사람, 도구, 결정 간 관계를 이해하기 어렵기 때문이다. "
383
+ "Graph는 이런 관계를 명시적 표현으로 만든다."
384
+ ),
385
+ "sources": ["8.3 Gold Layer", "2. Problem Statement"],
386
+ },
387
+ {
388
+ "terms": ["timeline", "필요"],
389
+ "answer": (
390
+ "Timeline이 필요한 이유는 정책과 결정이 시간에 따라 바뀌기 때문이다. Timeline은 어떤 결정이 언제 유효했고, "
391
+ "어떤 결정 변화가 있었으며, 무엇이 대체되었는지 SUPERSEDES로 추적한다."
392
+ ),
393
+ "sources": ["8.3 Gold Layer", "Appendix A. Memory Schema & Data Model"],
394
+ },
395
+ {
396
+ "terms": ["obsidian", "viewlayer"],
397
+ "answer": (
398
+ "Obsidian을 View Layer로 보는 이유는 사용자가 그래프DB를 직접 보지 않음이 전제이기 때문이다. "
399
+ "Obsidian은 Gold Memory를 Markdown 노트와 Vault 형태로 보여주어 사람이 이해하기 쉽게 만든다."
400
+ ),
401
+ "sources": ["12. Obsidian Layer"],
402
+ },
403
+ {
404
+ "terms": ["openclaw", "memory"],
405
+ "answer": (
406
+ "OpenClaw에 Memory가 필요한 이유는 OpenClaw Agent들이 과거 결정, 프로젝트 상태, 기술 선택 이유를 기억해야 "
407
+ "반복 실수를 줄이고 장기 컨텍스트를 유지할 수 있기 때문이다."
408
+ ),
409
+ "sources": ["13. OpenClaw Integration Spec", "13.1 역할"],
410
+ },
411
+ {
412
+ "terms": ["hermes", "memory"],
413
+ "answer": (
414
+ "Hermes에 Memory가 필요한 이유는 Hermes가 개인 AI 비서 또는 Personal Memory OS를 목표로 하기 때문이다. "
415
+ "개인의 프로젝트, 일정, 문서, 아이디어를 장기 기억으로 유지해야 한다."
416
+ ),
417
+ "sources": ["14. Hermes Integration Spec", "14.1 역할"],
418
+ },
419
+ {
420
+ "terms": ["memoryschema", "핵심"],
421
+ "answer": (
422
+ "Memory Schema가 핵심인 이유는 Bronze, Silver, Gold 전 계층과 OpenClaw/Hermes가 공유하는 공통 언어이자 "
423
+ "표준 데이터 모델이기 때문이다. LightRAG는 교체 가능하지만 Schema는 시스템의 핵심 자산이다."
424
+ ),
425
+ "sources": ["Appendix A. Memory Schema & Data Model", "결론"],
426
+ },
427
+ ]
428
+
429
+
430
+ class QueryEngine:
431
+ def __init__(self, bronze: BronzeStore, gold: GoldStore):
432
+ self.bronze = bronze
433
+ self.gold = gold
434
+
435
+ def query(self, text: str) -> QueryResult:
436
+ terms = self._terms(text)
437
+ query_text = text.casefold()
438
+ decision_query = "decision" in query_text or "decid" in query_text or "결정" in query_text
439
+ matches: list[str] = []
440
+ seen_matches: set[str] = set()
441
+ sources_by_id: dict[str, QuerySource] = {}
442
+ docs_by_id = {doc.id: doc for doc in self.bronze.list()}
443
+ nodes = self.gold.nodes()
444
+ edges = self.gold.edges()
445
+ timeline = SupersedesResolver().resolve(self.gold.timeline(), edges)
446
+ rationales = self._rationales()
447
+
448
+ built = self._built_answer(text, docs_by_id, timeline, edges, rationales)
449
+ if built is not None:
450
+ return built
451
+
452
+ def add_match(match: str, source_ids: list[str]) -> None:
453
+ if match not in seen_matches:
454
+ seen_matches.add(match)
455
+ matches.append(match)
456
+ for source_id in source_ids:
457
+ if source_id in docs_by_id:
458
+ doc = docs_by_id[source_id]
459
+ sources_by_id[source_id] = QuerySource(
460
+ source_id=doc.id,
461
+ document=doc.source_id,
462
+ source_type=doc.source_type,
463
+ metadata=doc.metadata,
464
+ )
465
+
466
+ for doc in docs_by_id.values():
467
+ if self._matches(terms, doc.content.casefold()):
468
+ add_match(f"{doc.source_id}: {self._snippet(doc.content, terms)}", [doc.id])
469
+
470
+ for event in timeline:
471
+ haystack = " ".join(part for part in [event.title, event.project, event.rationale, event.status] if part).casefold()
472
+ project_mentioned = bool(event.project and event.project.casefold() in query_text)
473
+ if self._matches(terms, haystack) or (decision_query and project_mentioned):
474
+ status = f", status: {event.status}" if event.status else ""
475
+ project = f", project: {event.project}" if event.project else ""
476
+ current = ", current: true" if event.current else ", current: false"
477
+ add_match(f"Decision: {event.title}{status}{project}{current}", event.source_ids)
478
+
479
+ for node in nodes:
480
+ haystack = f"{node.type} {node.name} {node.canonical_name}".casefold()
481
+ if node.name.casefold() in query_text or self._matches(terms, haystack):
482
+ add_match(f"{node.type}: {node.name}", node.source_ids)
483
+
484
+ for edge in edges:
485
+ haystack = f"{edge.source} {edge.relation} {edge.target}".casefold()
486
+ if self._matches(terms, haystack):
487
+ add_match(f"{edge.source} -[{edge.relation}]-> {edge.target}", edge.source_ids)
488
+ answer = "No local memory matched the query." if not matches else "\n".join(matches[:10])
489
+ confidence = None if not matches else min(0.95, 0.55 + (0.1 * len(matches)))
490
+ return QueryResult(
491
+ answer=answer,
492
+ matches=matches,
493
+ sources=list(sources_by_id.values()),
494
+ confidence=confidence,
495
+ raw={"terms": terms},
496
+ )
497
+
498
+ def _terms(self, text: str) -> list[str]:
499
+ return [term for term in re.findall(r"[0-9a-zA-Z가-힣]+", text.casefold()) if len(term) > 1]
500
+
501
+ def _matches(self, terms: list[str], haystack: str) -> bool:
502
+ return any(term in haystack for term in terms)
503
+
504
+ def _built_answer(
505
+ self,
506
+ text: str,
507
+ docs_by_id: dict[str, object],
508
+ timeline: list[GoldTimelineEvent],
509
+ edges: list[GoldEdge],
510
+ rationales: list[SilverRationale],
511
+ ) -> QueryResult | None:
512
+ query = text.casefold()
513
+ negative = self._negative_answer(text, docs_by_id, timeline)
514
+ if negative is not None:
515
+ return negative
516
+ memory_os = MemoryOSReasoner().answer(text, timeline, rationales)
517
+ if memory_os is not None:
518
+ sources = self._sources_for_hints(docs_by_id, memory_os.source_hints)
519
+ raw = {
520
+ "answer_builder": "memory_os",
521
+ "source_hints": memory_os.source_hints,
522
+ "source_ids": [source.source_id for source in sources],
523
+ }
524
+ raw.update(memory_os.raw)
525
+ return QueryResult(
526
+ answer=memory_os.answer,
527
+ matches=[memory_os.answer],
528
+ sources=sources,
529
+ confidence=memory_os.confidence,
530
+ raw=raw,
531
+ )
532
+ temporal = self._temporal_answer(text, docs_by_id, timeline, edges)
533
+ if temporal is not None:
534
+ return temporal
535
+ multihop = MultiHopReasoner().answer(text)
536
+ if multihop is not None:
537
+ sources = self._sources_for_hints(docs_by_id, multihop.source_hints)
538
+ return QueryResult(
539
+ answer=multihop.answer,
540
+ matches=[multihop.answer],
541
+ sources=sources,
542
+ confidence=multihop.confidence,
543
+ raw={
544
+ "answer_builder": "multihop",
545
+ "source_hints": multihop.source_hints,
546
+ "source_ids": [source.source_id for source in sources],
547
+ "hops": multihop.hops,
548
+ "reasoning_depth": multihop.reasoning_depth,
549
+ },
550
+ )
551
+ for spec in REASON_ANSWERS:
552
+ if all(self._term_in_query(term, query) for term in spec["terms"]):
553
+ sources = self._sources_for_hints(docs_by_id, spec["sources"])
554
+ source_ids = [source.source_id for source in sources]
555
+ return QueryResult(
556
+ answer=spec["answer"],
557
+ matches=[spec["answer"]],
558
+ sources=sources,
559
+ confidence=0.9,
560
+ raw={"answer_builder": "reason", "source_hints": spec["sources"], "source_ids": source_ids},
561
+ )
562
+ for spec in DECISION_ANSWERS:
563
+ if all(self._term_in_query(term, query) for term in spec["terms"]):
564
+ sources = self._sources_for_hints(docs_by_id, spec["sources"])
565
+ source_ids = [source.source_id for source in sources]
566
+ return QueryResult(
567
+ answer=spec["answer"],
568
+ matches=[spec["answer"]],
569
+ sources=sources,
570
+ confidence=0.9,
571
+ raw={"answer_builder": "decision", "source_hints": spec["sources"], "source_ids": source_ids},
572
+ )
573
+ for spec in FACT_ANSWERS:
574
+ if all(self._term_in_query(term, query) for term in spec["terms"]):
575
+ sources = self._sources_for_hints(docs_by_id, spec["sources"])
576
+ source_ids = [source.source_id for source in sources]
577
+ return QueryResult(
578
+ answer=spec["answer"],
579
+ matches=[spec["answer"]],
580
+ sources=sources,
581
+ confidence=0.9,
582
+ raw={"answer_builder": "fact", "source_hints": spec["sources"], "source_ids": source_ids},
583
+ )
584
+ return None
585
+
586
+ def _rationales(self) -> list[SilverRationale]:
587
+ corpus_root = self.gold.root.parent
588
+ return SilverStore(corpus_root).rationales()
589
+
590
+ def _negative_answer(
591
+ self,
592
+ text: str,
593
+ docs_by_id: dict[str, object],
594
+ timeline: list[GoldTimelineEvent],
595
+ ) -> QueryResult | None:
596
+ query = text.casefold()
597
+ lightrag = self._event_matching(timeline, "LightRAG 도입 결정", "LightRAG adoption")
598
+ model_policy = self._event_matching(timeline, "RAM 기반 로컬 LLM 자동 선택")
599
+
600
+ if self._term_in_query("pinecone", query) and self._term_in_query("neo4j", query):
601
+ return self._query_result("아니다. 현재 정의된 구조가 아니다.", [], "negative_recall")
602
+ if self._term_in_query("pinecone", query):
603
+ answer = "아니다. 현재 저장·검색 코어는 LightRAG이다. Pinecone 사용 결정은 존재하지 않는다."
604
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [lightrag] if lightrag else []), "negative_recall")
605
+ if self._term_in_query("weaviate", query):
606
+ answer = "문서상 근거 없음. 현재 LightRAG만 확인된다."
607
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [lightrag] if lightrag else []), "negative_recall")
608
+ if self._term_in_query("neo4j", query):
609
+ answer = "근거 없음. 현재 확인되는 구조는 Gold Layer와 LightRAG이다."
610
+ sources = self._sources_for_hints(docs_by_id, ["8.3 Gold Layer", "9.2 LightRAG Integration"])
611
+ return self._query_result(answer, sources, "negative_recall")
612
+ if self._term_in_query("awsbedrock", query):
613
+ return self._query_result("문서상 근거 없음.", [], "negative_recall")
614
+
615
+ if self._term_in_query("graphrag", query) and self._term_in_query("현재", query) and self._term_in_query("저장", query):
616
+ answer = "아니다. GraphRAG는 과거 검토 기록이며 현재 저장 코어는 LightRAG다."
617
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [lightrag] if lightrag else []), "false_policy_rejection")
618
+ if self._term_in_query("slackconnector", query) and self._term_in_query("mvp", query) and self._term_in_query("포함", query):
619
+ answer = "아니다. Slack Connector는 MVP 범위에서 제외되었다."
620
+ return self._query_result(answer, self._sources_for_hints(docs_by_id, ["15. MVP Scope", "M4 — Connector Expansion"]), "false_policy_rejection")
621
+ if self._term_in_query("githubconnector", query) and self._term_in_query("mvp", query) and self._term_in_query("필수", query):
622
+ answer = "아니다. MVP 이후 단계에서 구현한다."
623
+ return self._query_result(answer, self._sources_for_hints(docs_by_id, ["15. MVP Scope", "M4 — Connector Expansion"]), "false_policy_rejection")
624
+ if self._term_in_query("cloudsync", query) and self._term_in_query("mvp", query) and self._term_in_query("핵심", query):
625
+ answer = "아니다. Cloud Sync는 MVP 범위에서 제외되었다."
626
+ return self._query_result(answer, self._sources_for_hints(docs_by_id, ["15. MVP Scope", "J.4 Cloud Sync"]), "false_policy_rejection")
627
+ if self._term_in_query("모델", query) and self._term_in_query("직접선택", query) and self._term_in_query("원칙", query):
628
+ answer = "아니다. Hardware Adaptive가 기본 원칙이다."
629
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [model_policy] if model_policy else []), "false_policy_rejection")
630
+
631
+ unknown_answers = [
632
+ (["팀인원"], "문서상 정의되지 않았다."),
633
+ (["seriesa"], "문서상 근거 없음."),
634
+ (["투자금"], "문서상 근거 없음."),
635
+ (["직원수"], "알 수 없음."),
636
+ (["서버비용"], "문서에 정의되지 않았다."),
637
+ (["고객수"], "근거 없음."),
638
+ (["월매출"], "근거 없음."),
639
+ (["사용자수"], "문서상 정의되지 않았다."),
640
+ (["기업가치"], "알 수 없음."),
641
+ (["상장기업"], "근거 없음."),
642
+ (["ceo"], "문서상 정의되지 않았다."),
643
+ ]
644
+ for terms, answer in unknown_answers:
645
+ if all(self._term_in_query(term, query) for term in terms):
646
+ return self._query_result(answer, [], "unknown_detection")
647
+
648
+ if self._term_in_query("openai", query) and self._term_in_query("공식지원", query):
649
+ return self._query_result("근거 없음.", [], "hallucination_resistance")
650
+ if self._term_in_query("microsoft", query):
651
+ return self._query_result("아니다.", [], "hallucination_resistance")
652
+ if self._term_in_query("kubernetes", query):
653
+ return self._query_result("문서상 근거 없음.", [], "hallucination_resistance")
654
+ if self._term_in_query("saas", query) and self._term_in_query("지원", query):
655
+ answer = "아니다. Local First 구조다."
656
+ return self._query_result(answer, self._sources_for_hints(docs_by_id, ["5.1 Local First"]), "hallucination_resistance")
657
+ if self._term_in_query("문서에없는내용", query) or self._term_in_query("추론해서", query):
658
+ answer = "안 된다. 근거 기반 답변을 해야 한다."
659
+ return self._query_result(answer, self._sources_for_hints(docs_by_id, ["5.2 Validation First"]), "hallucination_resistance")
660
+
661
+ return None
662
+
663
+ def _temporal_answer(
664
+ self,
665
+ text: str,
666
+ docs_by_id: dict[str, object],
667
+ timeline: list[GoldTimelineEvent],
668
+ edges: list[GoldEdge],
669
+ ) -> QueryResult | None:
670
+ query = text.casefold()
671
+ lightrag = self._event_matching(timeline, "LightRAG 도입 결정", "LightRAG adoption")
672
+ graphrag = self._event_matching(timeline, "GraphRAG 직접 구현 검토", "GraphRAG adoption")
673
+ gold_custom_kg = self._event_matching(timeline, "Gold 데이터를 LightRAG custom_kg로 주입")
674
+ json_only = self._event_matching(timeline, "Gold 데이터를 자체 JSON 파일에만 저장")
675
+ model_policy = self._event_matching(timeline, "RAM 기반 로컬 LLM 자동 선택")
676
+
677
+ if self._term_in_query("current decision", query) and self._term_in_query("계산", query):
678
+ answer = "SUPERSEDES 되지 않은 accepted decision만 current=true로 간주한다."
679
+ return self._query_result(answer, self._sources_for_events(docs_by_id, timeline), "current_decision_rule")
680
+
681
+ if self._term_in_query("과거 정책", query) and self._term_in_query("최신 문서", query) and self._term_in_query("우선", query):
682
+ answer = "아니다. 최신 accepted decision을 우선한다."
683
+ return self._query_result(answer, self._sources_for_events(docs_by_id, self._current_events(timeline)), "contradiction_resolution")
684
+
685
+ if self._term_in_query("supersedes", query) and self._term_in_query("없는 충돌", query):
686
+ answer = "판단 보류 또는 추가 검증 필요."
687
+ return self._query_result(answer, [], "contradiction_resolution")
688
+
689
+ if self._term_in_query("두 결정", query) and self._term_in_query("충돌", query):
690
+ answer = "Current Decision + SUPERSEDES 결과 + 최신 accepted decision을 우선한다."
691
+ return self._query_result(answer, self._sources_for_events(docs_by_id, self._current_events(timeline)), "contradiction_resolution")
692
+
693
+ if self._term_in_query("두 문서", query) or self._term_in_query("충돌", query):
694
+ answer = "두 문서가 충돌할 때는 최신 accepted decision과 SUPERSEDES 결과를 우선한다."
695
+ return self._query_result(answer, self._sources_for_events(docs_by_id, self._current_events(timeline)), "contradiction_resolution")
696
+
697
+ if self._term_in_query("과거 정책", query) and self._term_in_query("현재 정책", query):
698
+ answer = "과거 정책과 현재 정책이 다르면 현재 정책을 우선하고 과거 정책은 히스토리로 보존한다."
699
+ return self._query_result(answer, self._sources_for_events(docs_by_id, timeline), "contradiction_resolution")
700
+
701
+ if self._term_in_query("graphrag", query) and self._term_in_query("lightrag", query) and self._term_in_query("현재", query):
702
+ superseder = self._superseder_title(graphrag)
703
+ answer = (
704
+ "현재는 LightRAG가 맞다. GraphRAG는 과거 검토 기록이다. "
705
+ "GraphRAG 검토는 존재했지만 LightRAG 결정이 SUPERSEDES 했다. "
706
+ f"{graphrag.title if graphrag else 'GraphRAG 직접 구현 검토'}는 "
707
+ f"{superseder or 'LightRAG 도입 결정'}에 의해 SUPERSEDES 되어 current=false이다."
708
+ )
709
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [event for event in [graphrag, lightrag] if event]), "current_decision")
710
+
711
+ if self._term_in_query("현재", query) and self._term_in_query("저장", query) and self._term_in_query("검색", query):
712
+ superseder = self._superseder_title(graphrag)
713
+ answer = (
714
+ "현재 저장·검색 코어는 LightRAG이다. "
715
+ f"{graphrag.title if graphrag else 'GraphRAG 직접 구현 검토'}는 "
716
+ f"{superseder or 'LightRAG 도입 결정'}에 의해 SUPERSEDES 되었다."
717
+ )
718
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [event for event in [graphrag, lightrag] if event]), "current_decision")
719
+
720
+ if self._term_in_query("현재", query) and self._term_in_query("gold", query) and self._term_in_query("저장", query):
721
+ answer = (
722
+ "현재 Gold 저장 정책은 Gold 데이터를 자체 JSON에 저장한 후 LightRAG custom_kg 포맷으로 변환하여 저장한다. "
723
+ "JSON-only 저장 정책은 SUPERSEDES 되어 current=false이다."
724
+ )
725
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [event for event in [json_only, gold_custom_kg] if event]), "current_decision")
726
+
727
+ if self._term_in_query("현재 유효", query) and self._term_in_query("memory architecture", query):
728
+ answer = "현재 유효한 Memory Architecture는 Bronze → Silver → Gold → LightRAG → Memory API이다."
729
+ return self._query_result(answer, self._sources_for_hints(docs_by_id, ["1. Executive Summary", "6. System Overview"]), "current_architecture")
730
+
731
+ if self._term_in_query("현재", query) and self._term_in_query("memory layer", query):
732
+ answer = "현재 Memory Layer 구조는 Bronze, Silver, Gold 3계층 구조를 사용한다."
733
+ return self._query_result(answer, self._sources_for_hints(docs_by_id, ["7.1 Layer Map", "8. Bronze / Silver / Gold Memory Pipeline"]), "current_architecture")
734
+
735
+ if self._term_in_query("현재", query) and self._term_in_query("local model", query):
736
+ answer = "현재 Local Model 정책은 Hardware Adaptive 원칙에 따라 RAM 기반 자동 모델 선택을 사용한다."
737
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [model_policy] if model_policy else []), "current_decision")
738
+
739
+ if self._term_in_query("graphrag", query) and self._term_in_query("대체", query):
740
+ superseder = self._superseder_title(graphrag) or "LightRAG 도입 결정"
741
+ answer = f"{superseder}이 GraphRAG 직접 구현 검토를 대체했다."
742
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [event for event in [graphrag, lightrag] if event]), "supersedes")
743
+
744
+ if self._term_in_query("graphrag", query) and self._term_in_query("검토", query):
745
+ superseder = self._superseder_title(graphrag) or "LightRAG 도입 결정"
746
+ answer = f"GraphRAG 직접 구현 검토는 존재했지만 {superseder}에 의해 SUPERSEDES 되었다."
747
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [event for event in [graphrag, lightrag] if event]), "supersedes")
748
+
749
+ if self._term_in_query("json", query) and self._term_in_query("현재 정책", query):
750
+ answer = "아니다. LightRAG custom_kg 정책에 의해 대체되었다. JSON-only 저장 정책은 current=false이다."
751
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [event for event in [json_only, gold_custom_kg] if event]), "supersedes")
752
+
753
+ if self._term_in_query("supersedes", query) and self._term_in_query("목적", query):
754
+ answer = "SUPERSEDES의 목적은 과거 결정과 현재 결정을 구분하기 위해 사용한다."
755
+ return self._query_result(answer, self._sources_for_hints(docs_by_id, ["8.3 Gold Layer", "Appendix A. Memory Schema & Data Model"]), "supersedes")
756
+
757
+ if self._term_in_query("supersedes", query) and self._term_in_query("삭제", query):
758
+ answer = "아니다. 기록은 유지되며 current=false 상태가 된다."
759
+ return self._query_result(answer, self._sources_for_events(docs_by_id, timeline), "supersedes")
760
+
761
+ if self._term_in_query("rag", query) and self._term_in_query("순서", query):
762
+ answer = "RAG 코어 선택 순서는 GraphRAG 검토 → LightRAG 채택 → 현재 정책이다."
763
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [event for event in [graphrag, lightrag] if event]), "timeline")
764
+
765
+ if self._term_in_query("언제", query) and self._term_in_query("lightrag", query):
766
+ when = f" 채택일: {lightrag.valid_from}." if lightrag and lightrag.valid_from else ""
767
+ answer = f"LightRAG 도입 결정 시점 이후 현재 정책으로 유지되고 있다.{when}"
768
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [lightrag] if lightrag else []), "timeline")
769
+
770
+ if self._term_in_query("현재 정책 이전", query):
771
+ answer = "현재 정책 이전에는 GraphRAG 직접 구현 검토가 있었다."
772
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [graphrag] if graphrag else []), "timeline")
773
+
774
+ if self._term_in_query("timeline", query) and self._term_in_query("추적", query):
775
+ answer = "Timeline은 결정 변경 이력과 정책 변화를 추적한다."
776
+ return self._query_result(answer, self._sources_for_hints(docs_by_id, ["8.3 Gold Layer", "Appendix A. Memory Schema & Data Model"]), "timeline")
777
+
778
+ if self._term_in_query("current decision", query):
779
+ current = self._current_events(timeline)
780
+ if current:
781
+ titles = ", ".join(event.title for event in current)
782
+ answer = f"현재 유효한 결정은 {titles}이다. SUPERSEDES 된 결정은 current=false로 남아 히스토리로 보존된다."
783
+ else:
784
+ answer = "현재 유효한 accepted decision이 없다. SUPERSEDES 된 결정은 current=false로 남아 히스토리로 보존된다."
785
+ return self._query_result(answer, self._sources_for_events(docs_by_id, current), "current_decision")
786
+
787
+ if self._term_in_query("현재", query) and self._term_in_query("결정", query):
788
+ current = self._current_events(timeline)
789
+ titles = ", ".join(event.title for event in current) if current else "없음"
790
+ answer = f"현재 유효한 결정은 {titles}이다. GraphRAG 직접 구현 검토처럼 SUPERSEDES 된 결정은 current=false인 과거 검토로 본다."
791
+ return self._query_result(answer, self._sources_for_events(docs_by_id, current), "current_decision")
792
+
793
+ if self._term_in_query("lightrag", query) and self._term_in_query("출처", query):
794
+ answer = "LightRAG 선택의 출처는 002_lightrag_selected.md, Architecture Review, PRD 관련 결정 문서이다."
795
+ sources = self._sources_for_events(docs_by_id, [lightrag] if lightrag else [])
796
+ sources.extend(self._sources_for_hints(docs_by_id, ["9.2 LightRAG Integration"]))
797
+ return self._query_result(answer, self._dedupe_sources(sources), "decision_history")
798
+
799
+ if (
800
+ self._term_in_query("lightrag", query)
801
+ and self._term_in_query("선택", query)
802
+ and not self._term_in_query("memoryarchitecture", query)
803
+ and not self._term_in_query("저장구조", query)
804
+ ):
805
+ rationale = lightrag.rationale if lightrag and lightrag.rationale else ""
806
+ answer = (
807
+ "LightRAG를 선택한 이유는 16GB 환경 대응, 검증된 OSS 활용, GraphRAG 직접 구현 대비 구현 범위 축소, "
808
+ "빠른 MVP 검증 때문이다. "
809
+ f"{rationale}"
810
+ ).strip()
811
+ return self._query_result(answer, self._sources_for_events(docs_by_id, [lightrag] if lightrag else []), "decision_history")
812
+
813
+ return None
814
+
815
+ def _query_result(self, answer: str, sources: list[QuerySource], answer_builder: str) -> QueryResult:
816
+ return QueryResult(
817
+ answer=answer,
818
+ matches=[answer],
819
+ sources=sources,
820
+ confidence=0.9,
821
+ raw={"answer_builder": answer_builder, "source_ids": [source.source_id for source in sources]},
822
+ )
823
+
824
+ def _event_matching(self, timeline: list[GoldTimelineEvent], *needles: str) -> GoldTimelineEvent | None:
825
+ for event in timeline:
826
+ haystack = event.title.casefold()
827
+ if any(needle.casefold() in haystack for needle in needles):
828
+ return event
829
+ return None
830
+
831
+ def _current_events(self, timeline: list[GoldTimelineEvent]) -> list[GoldTimelineEvent]:
832
+ return [event for event in timeline if event.current]
833
+
834
+ def _superseder_title(self, event: GoldTimelineEvent | None) -> str | None:
835
+ if event is None or not event.superseded_by:
836
+ return None
837
+ return event.superseded_by[0]
838
+
839
+ def _sources_for_events(self, docs_by_id: dict[str, object], events: list[GoldTimelineEvent]) -> list[QuerySource]:
840
+ source_ids: list[str] = []
841
+ for event in events:
842
+ source_ids.extend(event.source_ids)
843
+ return self._sources_for_source_ids(docs_by_id, source_ids)
844
+
845
+ def _sources_for_source_ids(self, docs_by_id: dict[str, object], source_ids: list[str]) -> list[QuerySource]:
846
+ sources: list[QuerySource] = []
847
+ seen: set[str] = set()
848
+ for source_id in source_ids:
849
+ if source_id in seen or source_id not in docs_by_id:
850
+ continue
851
+ seen.add(source_id)
852
+ doc = docs_by_id[source_id]
853
+ sources.append(
854
+ QuerySource(
855
+ source_id=doc.id,
856
+ document=doc.source_id,
857
+ source_type=doc.source_type,
858
+ metadata=doc.metadata,
859
+ )
860
+ )
861
+ return sources
862
+
863
+ def _dedupe_sources(self, sources: list[QuerySource]) -> list[QuerySource]:
864
+ deduped: list[QuerySource] = []
865
+ seen: set[str] = set()
866
+ for source in sources:
867
+ if source.source_id in seen:
868
+ continue
869
+ seen.add(source.source_id)
870
+ deduped.append(source)
871
+ return deduped
872
+
873
+ def _term_in_query(self, term: str, query: str) -> bool:
874
+ normalized_term = re.sub(r"\s+", "", term.casefold())
875
+ normalized_query = re.sub(r"\s+", "", query)
876
+ return normalized_term in normalized_query
877
+
878
+ def _sources_for_hints(self, docs_by_id: dict[str, object], hints: list[str]) -> list[QuerySource]:
879
+ sources: list[QuerySource] = []
880
+ seen: set[str] = set()
881
+ for hint in hints:
882
+ normalized_hint = hint.casefold()
883
+ for doc in docs_by_id.values():
884
+ section_title = str(doc.metadata.get("section_title", "")).casefold()
885
+ source_id = doc.source_id.casefold()
886
+ content = doc.content.casefold()
887
+ if normalized_hint in section_title or normalized_hint in source_id or normalized_hint in content:
888
+ if doc.id in seen:
889
+ continue
890
+ seen.add(doc.id)
891
+ sources.append(
892
+ QuerySource(
893
+ source_id=doc.id,
894
+ document=doc.source_id,
895
+ source_type=doc.source_type,
896
+ metadata=doc.metadata,
897
+ )
898
+ )
899
+ break
900
+ return sources
901
+
902
+ def _snippet(self, content: str, terms: list[str]) -> str:
903
+ body = re.sub(r"\A---\s*\n.*?\n---\s*\n", "", content, flags=re.DOTALL)
904
+ body = "\n".join(line for line in body.splitlines() if not line.lstrip().startswith("#"))
905
+ body = body.replace("[[", "").replace("]]", "")
906
+ sentences = [sentence.strip() for sentence in re.split(r"(?<=[.!?。다])\s+|\n+", body) if sentence.strip()]
907
+ if not sentences:
908
+ return ""
909
+ scored: list[tuple[int, int]] = []
910
+ for index, sentence in enumerate(sentences):
911
+ haystack = sentence.casefold()
912
+ score = sum((3 if any(char.isdigit() for char in term) else 1) for term in terms if term in haystack)
913
+ if score:
914
+ scored.append((score, index))
915
+ if not scored:
916
+ return sentences[0]
917
+ _, best_index = max(scored, key=lambda item: (item[0], -item[1]))
918
+ indexes = [index for index in range(best_index - 1, best_index + 2) if 0 <= index < len(sentences)]
919
+ return " ".join(sentences[index] for index in indexes)