structverify 0.3.0__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 (168) hide show
  1. structverify/__init__.py +83 -0
  2. structverify/adaptation/__init__.py +0 -0
  3. structverify/adaptation/adapter_trainer.py +341 -0
  4. structverify/adaptation/feedback_store.py +31 -0
  5. structverify/adaptation/kosis_crawler.py +317 -0
  6. structverify/adaptation/sample_builder.py +149 -0
  7. structverify/adaptation/synthetic_generator.py +320 -0
  8. structverify/adaptation/update_embeddings.py +178 -0
  9. structverify/agent/__init__.py +21 -0
  10. structverify/agent/builder_agent.py +226 -0
  11. structverify/agent/conformance_agent.py +171 -0
  12. structverify/agent/dependency_planner.py +151 -0
  13. structverify/agent/indexing_agent.py +153 -0
  14. structverify/agent/indexing_planner.py +169 -0
  15. structverify/agent/integration_example.py +182 -0
  16. structverify/agent/loop.py +1165 -0
  17. structverify/agent/memory.py +207 -0
  18. structverify/agent/planner.py +817 -0
  19. structverify/agent/prompts/__init__.py +15 -0
  20. structverify/agent/prompts/planner_prompts.py +219 -0
  21. structverify/agent/prompts/reflect_prompts.py +387 -0
  22. structverify/agent/reflect.py +227 -0
  23. structverify/agent/runtime_agent.py +1272 -0
  24. structverify/agent/schemas.py +262 -0
  25. structverify/agent/source_profiler.py +229 -0
  26. structverify/agent/tools/__init__.py +64 -0
  27. structverify/agent/tools/base.py +222 -0
  28. structverify/agent/tools/calculate.py +244 -0
  29. structverify/agent/tools/catalog_search.py +859 -0
  30. structverify/agent/tools/deep_explore.py +293 -0
  31. structverify/agent/tools/explore_catalog.py +423 -0
  32. structverify/agent/tools/fetch_evidence.py +922 -0
  33. structverify/agent/tools/finish.py +423 -0
  34. structverify/agent/tools/meta_explore.py +267 -0
  35. structverify/agent/tools/query_rewriter.py +134 -0
  36. structverify/agent/tools/read_original.py +144 -0
  37. structverify/agent/tools/replan.py +365 -0
  38. structverify/agent/workspace.py +958 -0
  39. structverify/api.py +804 -0
  40. structverify/config/default.yaml +350 -0
  41. structverify/core/__init__.py +0 -0
  42. structverify/core/config_loader.py +30 -0
  43. structverify/core/pipeline.py +280 -0
  44. structverify/core/schemas.py +362 -0
  45. structverify/detection/__init__.py +26 -0
  46. structverify/detection/_config.py +163 -0
  47. structverify/detection/_llm.py +24 -0
  48. structverify/detection/candidate/__init__.py +1 -0
  49. structverify/detection/candidate/heuristic.py +60 -0
  50. structverify/detection/candidate/llm.py +51 -0
  51. structverify/detection/candidate_scorer.py +81 -0
  52. structverify/detection/claim_detector.py +164 -0
  53. structverify/detection/claims/__init__.py +1 -0
  54. structverify/detection/claims/worthiness.py +142 -0
  55. structverify/detection/domain/__init__.py +1 -0
  56. structverify/detection/domain/classify.py +84 -0
  57. structverify/detection/domain/preview.py +36 -0
  58. structverify/detection/domain/registry.py +99 -0
  59. structverify/detection/domain_classifier.py +75 -0
  60. structverify/detection/prompts/__init__.py +1 -0
  61. structverify/detection/prompts/candidate.py +38 -0
  62. structverify/detection/prompts/claim_worthiness.py +48 -0
  63. structverify/detection/prompts/domain.py +41 -0
  64. structverify/detection/prompts/schema.py +508 -0
  65. structverify/detection/prompts_loader.py +167 -0
  66. structverify/detection/schema/__init__.py +1 -0
  67. structverify/detection/schema/expand.py +83 -0
  68. structverify/detection/schema/induce.py +441 -0
  69. structverify/detection/schema/regenerate.py +162 -0
  70. structverify/detection/schema/temporal_hints.py +130 -0
  71. structverify/detection/schema/validate.py +193 -0
  72. structverify/detection/schema_inductor.py +112 -0
  73. structverify/detection/synthetic_generator.py +270 -0
  74. structverify/explanation/__init__.py +0 -0
  75. structverify/explanation/_config.py +18 -0
  76. structverify/explanation/_llm.py +25 -0
  77. structverify/explanation/explainer.py +183 -0
  78. structverify/explanation/fallback.py +29 -0
  79. structverify/explanation/formatters.py +75 -0
  80. structverify/explanation/prompts/__init__.py +1 -0
  81. structverify/explanation/prompts/match.py +27 -0
  82. structverify/explanation/prompts/mismatch.py +20 -0
  83. structverify/explanation/prompts/multihop.py +16 -0
  84. structverify/explanation/prompts/unverifiable.py +17 -0
  85. structverify/graph/__init__.py +0 -0
  86. structverify/graph/claim_graph.py +226 -0
  87. structverify/graph/document_graph.py +487 -0
  88. structverify/graph/graph_builder.py +238 -0
  89. structverify/graph/graph_multihop.py +335 -0
  90. structverify/graph/graph_store.py +281 -0
  91. structverify/graph/provenance.py +52 -0
  92. structverify/memory/__init__.py +44 -0
  93. structverify/memory/agent_memory.py +142 -0
  94. structverify/memory/embedder.py +69 -0
  95. structverify/memory/exemplar_store.py +241 -0
  96. structverify/memory/normalizer.py +91 -0
  97. structverify/memory/schema.py +119 -0
  98. structverify/memory/storage/__init__.py +29 -0
  99. structverify/memory/storage/jsonl_store.py +117 -0
  100. structverify/memory/working_memory.py +370 -0
  101. structverify/preprocessing/Dockerfile.scraper +27 -0
  102. structverify/preprocessing/__init__.py +0 -0
  103. structverify/preprocessing/extractor.py +574 -0
  104. structverify/preprocessing/pdf/__init__.py +16 -0
  105. structverify/preprocessing/pdf/fields.py +95 -0
  106. structverify/preprocessing/pdf/markdown.py +107 -0
  107. structverify/preprocessing/pdf/models.py +34 -0
  108. structverify/preprocessing/pdf/ocr.py +172 -0
  109. structverify/preprocessing/pdf/pipeline.py +74 -0
  110. structverify/preprocessing/pdf/reader.py +119 -0
  111. structverify/preprocessing/pdf/scoring.py +61 -0
  112. structverify/preprocessing/scraper_sandbox.py +561 -0
  113. structverify/preprocessing/segmenter.py +48 -0
  114. structverify/preprocessing/sir_builder.py +240 -0
  115. structverify/progress.py +591 -0
  116. structverify/retrieval/__init__.py +0 -0
  117. structverify/retrieval/base.py +208 -0
  118. structverify/retrieval/base_connector.py +85 -0
  119. structverify/retrieval/catalog_ranker.py +300 -0
  120. structverify/retrieval/catalog_search.py +583 -0
  121. structverify/retrieval/chunking.py +92 -0
  122. structverify/retrieval/custom_csv_source.py +386 -0
  123. structverify/retrieval/custom_db_source.py +396 -0
  124. structverify/retrieval/custom_docs_source.py +152 -0
  125. structverify/retrieval/dimension_resolver.py +281 -0
  126. structverify/retrieval/evidence_subgraph.py +63 -0
  127. structverify/retrieval/kosis_connector.py +1192 -0
  128. structverify/retrieval/kosis_relevance.py +142 -0
  129. structverify/retrieval/kosis_source.py +1541 -0
  130. structverify/retrieval/query_builder.py +72 -0
  131. structverify/retrieval/registry.py +133 -0
  132. structverify/retrieval/relevance_judge.py +141 -0
  133. structverify/retrieval/row_matcher.py +267 -0
  134. structverify/storage/__init__.py +0 -0
  135. structverify/storage/db_manager.py +157 -0
  136. structverify/storage/dwh_manager.py +92 -0
  137. structverify/storage/init_db.py +99 -0
  138. structverify/storage/raw_storage.py +29 -0
  139. structverify/training/__init__.py +26 -0
  140. structverify/training/curator.py +124 -0
  141. structverify/training/dataset.py +134 -0
  142. structverify/training/doctor.py +99 -0
  143. structverify/training/evalgate.py +96 -0
  144. structverify/training/generate.py +101 -0
  145. structverify/training/loop.py +116 -0
  146. structverify/training/recipe/train_mlx.py +99 -0
  147. structverify/training/recipe/train_qlora.py +104 -0
  148. structverify/training/tasks.py +79 -0
  149. structverify/utils/__init__.py +0 -0
  150. structverify/utils/embedding_client.py +248 -0
  151. structverify/utils/llm_client.py +809 -0
  152. structverify/utils/logger.py +81 -0
  153. structverify/verification/__init__.py +0 -0
  154. structverify/verification/_config.py +45 -0
  155. structverify/verification/adapters.py +405 -0
  156. structverify/verification/conformance.py +117 -0
  157. structverify/verification/decide_verdict.py +216 -0
  158. structverify/verification/decide_verdict_agent.py +454 -0
  159. structverify/verification/growth_diff.py +267 -0
  160. structverify/verification/row_match.py +345 -0
  161. structverify/verification/units.py +64 -0
  162. structverify/verification/verdict_thresholds.py +232 -0
  163. structverify/verification/verifier.py +84 -0
  164. structverify-0.3.0.dist-info/METADATA +903 -0
  165. structverify-0.3.0.dist-info/RECORD +168 -0
  166. structverify-0.3.0.dist-info/WHEEL +5 -0
  167. structverify-0.3.0.dist-info/licenses/LICENSE +21 -0
  168. structverify-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,370 @@
1
+ # [이수민 - 2026-05-14]
2
+ # - DocumentWorkingMemory: 한 doc 처리 동안만 살아있는 working memory
3
+ # - 목적: step간 컨텍스트 공유 + verifier 도메인 가드 + 파생 주장 검증
4
+ # - 수명: 파이프라인 시작 시 생성, 종료 시 소멸 (영속화 없음)
5
+ # - 영속 메모리(AgentMemory, ExemplarStore)와는 다른 layer
6
+ """
7
+ memory/working_memory.py — Document-scoped Working Memory
8
+
9
+ [설계 의도]
10
+ 한 입력 문서 처리 동안 모든 step이 공유하는 in-memory 컨텍스트.
11
+ - step간 데이터 명시적 전달 (config 산재 방지)
12
+ - claim 간 관계성 (같은 indicator 묶음) 노출
13
+ - verifier가 도메인 일관성 가드로 활용
14
+
15
+ [라이프사이클]
16
+ memory = DocumentWorkingMemory(doc_id, run_id)
17
+ # 각 step이 read/write
18
+ memory.record_domain(...)
19
+ memory.record_claim(...)
20
+ related = memory.find_related_claims("출생아 수")
21
+ # 종료 시 소멸 (영속화 안 함)
22
+
23
+ [참고]
24
+ - 영속 KG (cross-doc) → memory/agent_memory.py (Phase 2 보류)
25
+ - 영속 사례 retrieval → memory/exemplar_store.py (Phase 2 보류)
26
+ """
27
+ from __future__ import annotations
28
+
29
+ from collections import defaultdict
30
+ from datetime import datetime
31
+ from typing import Any
32
+
33
+ from pydantic import BaseModel, Field
34
+
35
+ from structverify.core.schemas import Claim
36
+ from structverify.utils.logger import get_logger
37
+
38
+ logger = get_logger(__name__)
39
+
40
+
41
+ # ── 보조 모델 ────────────────────────────────────────────────────────────────
42
+
43
+ class StatIdUsage(BaseModel):
44
+ """Step 7에서 성공한 stat_id 사용 기록."""
45
+ stat_id: str
46
+ indicator: str
47
+ category_path: str | None = None
48
+ time_period: str | None = None
49
+ used_at: datetime = Field(default_factory=datetime.utcnow)
50
+
51
+
52
+ # ── 메인 클래스 ──────────────────────────────────────────────────────────────
53
+
54
+ class DocumentWorkingMemory(BaseModel):
55
+ """
56
+ 한 doc 처리 동안의 working memory.
57
+
58
+ 모든 step이 이 객체를 공유하며 read/write 한다.
59
+ """
60
+ # ── 식별 ─────────────────────────────────────────────────────────────────
61
+ doc_id: str
62
+ run_id: str
63
+ source_uri: str | None = None
64
+ created_at: datetime = Field(default_factory=datetime.utcnow)
65
+
66
+ # ── Step 3: Domain ──────────────────────────────────────────────────────
67
+ domain: str | None = None
68
+ domain_description: str | None = None
69
+
70
+ # ── Step 4.5: Temporal ──────────────────────────────────────────────────
71
+ anchor_year: int | None = None
72
+ temporal_resolved: dict[str, str] = Field(default_factory=dict)
73
+
74
+ # ── Step 4~6: Claims ────────────────────────────────────────────────────
75
+ claims: list[Claim] = Field(default_factory=list)
76
+ metric_to_claims: dict[str, list[str]] = Field(default_factory=dict)
77
+
78
+ # ── Step 7: KOSIS ───────────────────────────────────────────────────────
79
+ used_stat_ids: dict[str, StatIdUsage] = Field(default_factory=dict)
80
+ rejected_stat_ids: dict[str, str] = Field(default_factory=dict)
81
+
82
+ # ── 자유 메모 ────────────────────────────────────────────────────────────
83
+ notes: dict[str, Any] = Field(default_factory=dict)
84
+
85
+ model_config = {"arbitrary_types_allowed": True}
86
+
87
+ # ════════════════════════════════════════════════════════════════════════
88
+ # 기록 (Write)
89
+ # ════════════════════════════════════════════════════════════════════════
90
+
91
+ def record_domain(self, domain: str, description: str | None = None) -> None:
92
+ """Step 3 결과 기록."""
93
+ self.domain = domain
94
+ self.domain_description = description
95
+ logger.debug(f"[memory:{self.doc_id[:8]}] domain={domain}")
96
+
97
+ def record_anchor_year(self, year: int) -> None:
98
+ """Step 4.5에서 결정된 anchor_year."""
99
+ self.anchor_year = year
100
+ logger.debug(f"[memory:{self.doc_id[:8]}] anchor_year={year}")
101
+
102
+ def record_temporal(self, expression: str, resolved: str) -> None:
103
+ """시간 표현 → 해소된 절대 시점."""
104
+ self.temporal_resolved[expression] = resolved
105
+
106
+ def record_claim(self, claim: Claim) -> None:
107
+ """
108
+ Claim 추가 + metric_to_claims 자동 업데이트.
109
+
110
+ 같은 indicator를 가진 claim들이 자동으로 묶여서 find_related_claims로 조회 가능.
111
+ """
112
+ # 중복 방지 (같은 claim_id면 스킵)
113
+ cid = str(claim.claim_id)
114
+ if any(str(c.claim_id) == cid for c in self.claims):
115
+ return
116
+ self.claims.append(claim)
117
+
118
+ # metric_to_claims 인덱스 업데이트
119
+ if claim.schema and claim.schema.indicator:
120
+ indicator = claim.schema.indicator
121
+ self.metric_to_claims.setdefault(indicator, []).append(cid)
122
+
123
+ def record_claims(self, claims: list[Claim]) -> None:
124
+ """여러 Claim 한 번에 기록."""
125
+ for c in claims:
126
+ self.record_claim(c)
127
+
128
+ def record_stat_id_used(
129
+ self,
130
+ indicator: str,
131
+ stat_id: str,
132
+ category_path: str | None = None,
133
+ time_period: str | None = None,
134
+ ) -> None:
135
+ """Step 7에서 성공한 stat_id 캐시."""
136
+ self.used_stat_ids[indicator] = StatIdUsage(
137
+ stat_id=stat_id,
138
+ indicator=indicator,
139
+ category_path=category_path,
140
+ time_period=time_period,
141
+ )
142
+
143
+ def record_stat_id_rejected(self, stat_id: str, reason: str) -> None:
144
+ """도메인 어긋남 등으로 거절된 stat_id."""
145
+ self.rejected_stat_ids[stat_id] = reason
146
+
147
+ def add_note(self, step: str, key: str, value: Any) -> None:
148
+ """자유 메모. notes[step][key] = value."""
149
+ self.notes.setdefault(step, {})[key] = value
150
+
151
+ # ════════════════════════════════════════════════════════════════════════
152
+ # 조회 (Read)
153
+ # ════════════════════════════════════════════════════════════════════════
154
+
155
+ def find_related_claims(self, indicator: str) -> list[Claim]:
156
+ """
157
+ 같은 indicator를 가진 모든 Claim 반환.
158
+
159
+ 파생 주장 검증의 핵심 — "8.7% 증가" 검증 시 같은 metric의 baseline/comparison claim 가져옴.
160
+ """
161
+ claim_ids = self.metric_to_claims.get(indicator, [])
162
+ if not claim_ids:
163
+ return []
164
+ # claim_id 매칭으로 실제 Claim 객체 반환
165
+ return [c for c in self.claims if str(c.claim_id) in claim_ids]
166
+
167
+ def find_claims_by_indicator_prefix(self, prefix: str) -> list[Claim]:
168
+ """
169
+ indicator가 특정 prefix로 시작하는 claim 반환.
170
+
171
+ 예: "출생아 수"로 시작 → "출생아 수", "출생아 수 증가율" 둘 다 매칭.
172
+ """
173
+ out = []
174
+ for indicator, cids in self.metric_to_claims.items():
175
+ if indicator.startswith(prefix):
176
+ out.extend(c for c in self.claims if str(c.claim_id) in cids)
177
+ return out
178
+
179
+ def get_stat_id_for_indicator(self, indicator: str) -> StatIdUsage | None:
180
+ """같은 indicator로 이전에 성공한 stat_id 있으면 반환 (캐시 hit)."""
181
+ return self.used_stat_ids.get(indicator)
182
+
183
+ def is_stat_id_rejected(self, stat_id: str) -> bool:
184
+ """이 stat_id가 이번 doc에서 이미 거절됐는지."""
185
+ return stat_id in self.rejected_stat_ids
186
+
187
+ def domain_matches_category(self, category_path: str | None) -> bool:
188
+ """
189
+ evidence의 category_path가 이 doc의 domain과 일치하는지.
190
+
191
+ Verifier의 일관성 가드용. domain이 미설정이거나 category가 None이면 True (관대).
192
+
193
+ 매칭 규칙 (단순 키워드 기반, Phase 2에서 정교화 가능):
194
+ domain="population" ↔ category_path 포함 "인구" → True
195
+ domain="employment" ↔ category_path 포함 "고용"|"노동"|"취업" → True
196
+ domain="environment" ↔ category_path 포함 "환경"|"기상"|"기후" → True
197
+ domain="economy" ↔ category_path 포함 "경제"|"산업"|"물가" → True
198
+ """
199
+ if not self.domain or not category_path:
200
+ return True # 정보 부족 → 통과
201
+
202
+ # 도메인 → 카테고리 키워드 매핑
203
+ _DOMAIN_KEYWORDS = {
204
+ "population": ["인구", "출생", "사망", "혼인", "이혼", "가구"],
205
+ "employment": ["고용", "노동", "취업", "임금", "근로", "실업"],
206
+ "environment": ["환경", "기상", "기후", "오염", "에너지", "탄소"],
207
+ "economy": ["경제", "산업", "물가", "성장", "무역", "gdp"],
208
+ "education": ["교육", "학교", "학생", "진학"],
209
+ "health": ["보건", "의료", "건강", "질환"],
210
+ }
211
+
212
+ keywords = _DOMAIN_KEYWORDS.get(self.domain.lower(), [])
213
+ if not keywords:
214
+ return True # 알 수 없는 도메인 → 통과 (보수적)
215
+
216
+ return any(kw in category_path for kw in keywords)
217
+
218
+ # ════════════════════════════════════════════════════════════════════════
219
+ # 덤프 / 디버깅
220
+ # ════════════════════════════════════════════════════════════════════════
221
+
222
+ def dump(self) -> dict:
223
+ """현재 메모리 상태 전체를 dict로 반환 (디버깅/로깅용)."""
224
+ return {
225
+ "doc_id": self.doc_id,
226
+ "run_id": self.run_id,
227
+ "source_uri": self.source_uri,
228
+ "created_at": self.created_at.isoformat(),
229
+ "domain": self.domain,
230
+ "domain_description": self.domain_description,
231
+ "anchor_year": self.anchor_year,
232
+ "temporal_resolved": dict(self.temporal_resolved),
233
+ "claims_count": len(self.claims),
234
+ "metric_to_claims": {k: list(v) for k, v in self.metric_to_claims.items()},
235
+ "used_stat_ids": {k: v.model_dump() for k, v in self.used_stat_ids.items()},
236
+ "rejected_stat_ids": dict(self.rejected_stat_ids),
237
+ "notes": dict(self.notes),
238
+ }
239
+
240
+ def stats(self) -> dict[str, int]:
241
+ """간단 통계."""
242
+ return {
243
+ "claims": len(self.claims),
244
+ "metrics": len(self.metric_to_claims),
245
+ "temporal": len(self.temporal_resolved),
246
+ "stat_ids_used": len(self.used_stat_ids),
247
+ "stat_ids_rej": len(self.rejected_stat_ids),
248
+ }
249
+
250
+
251
+ # ════════════════════════════════════════════════════════════════════════════
252
+ # 단위 테스트 (직접 실행 시)
253
+ # ════════════════════════════════════════════════════════════════════════════
254
+
255
+ if __name__ == "__main__":
256
+ from uuid import uuid4
257
+ from structverify.core.schemas import ClaimSchema, ClaimType
258
+
259
+ print("=" * 60)
260
+ print("DocumentWorkingMemory 단위 테스트")
261
+ print("=" * 60)
262
+
263
+ # 1. 인스턴스 생성
264
+ mem = DocumentWorkingMemory(
265
+ doc_id="doc_test_001",
266
+ run_id="run_test_001",
267
+ source_uri="https://example.com/news/birth",
268
+ )
269
+ print(f"\n[1] 인스턴스 생성: doc_id={mem.doc_id}")
270
+
271
+ # 2. Step 3: 도메인 기록
272
+ mem.record_domain("population", "인구/가구 통계")
273
+ print(f"[2] 도메인 기록: {mem.domain} / {mem.domain_description}")
274
+
275
+ # 3. Step 4.5: 시간 정보 기록
276
+ mem.record_anchor_year(2025)
277
+ mem.record_temporal("올해", "2025")
278
+ mem.record_temporal("지난해 같은 달", "2024-04")
279
+ mem.record_temporal("전년", "2024")
280
+ print(f"[3] anchor_year={mem.anchor_year}, temporal={mem.temporal_resolved}")
281
+
282
+ # 4. Step 4~6: claim 기록 (birth 기사 흉내)
283
+ test_doc_uuid = uuid4()
284
+ def _mk_claim(sent_id, text, indicator, value, unit, time_period, parent_path=None):
285
+ return Claim(
286
+ claim_id=uuid4(),
287
+ doc_id=test_doc_uuid,
288
+ block_id="b0002",
289
+ sent_id=sent_id,
290
+ claim_text=text,
291
+ claim_type="numerical",
292
+ schema=ClaimSchema(
293
+ indicator=indicator, value=value, unit=unit,
294
+ time_period=time_period,
295
+ parent_path=parent_path,
296
+ modifier=None,
297
+ ),
298
+ )
299
+
300
+ claims = [
301
+ _mk_claim("b0002_s0000", "올해 4월 출생아 수는 총 2만 717명",
302
+ "출생아 수", 20717.0, "명", "2025-04", "인구 > 출생"),
303
+ _mk_claim("b0002_s0000", "지난해 같은 달(1만 9059명)보다 8.7% 늘었다",
304
+ "출생아 수 증가율", 8.7, "%", "2025-04", "인구 > 출생"),
305
+ _mk_claim("b0002_s0001", "1991년 4월(8.7%) 이후 34년 만에",
306
+ "출생아 수 증가율", 8.7, "%", "1991-04", "인구 > 출생"),
307
+ _mk_claim("b0002_s0002", "합계출산율 0.79명",
308
+ "합계출산율", 0.79, "명", "2025-04"),
309
+ ]
310
+ mem.record_claims(claims)
311
+ print(f"[4] claim 4개 기록 → metric_to_claims: {list(mem.metric_to_claims.keys())}")
312
+
313
+ # 5. find_related_claims 테스트
314
+ related = mem.find_related_claims("출생아 수 증가율")
315
+ print(f"[5] find_related('출생아 수 증가율') → {len(related)}건")
316
+ for c in related:
317
+ print(f" · [{c.sent_id}] {c.schema.value}{c.schema.unit} @ {c.schema.time_period}")
318
+
319
+ # 6. prefix 검색
320
+ prefix_hit = mem.find_claims_by_indicator_prefix("출생아 수")
321
+ print(f"[6] find_by_prefix('출생아 수') → {len(prefix_hit)}건 "
322
+ f"(증가율 포함)")
323
+
324
+ # 7. Step 7: stat_id 캐시
325
+ mem.record_stat_id_used(
326
+ indicator="출생아 수",
327
+ stat_id="DT_1B8000G",
328
+ category_path="인구 > 출생",
329
+ time_period="2025-04",
330
+ )
331
+ cached = mem.get_stat_id_for_indicator("출생아 수")
332
+ print(f"[7] stat_id 캐시 hit: {cached.stat_id} ({cached.category_path})")
333
+
334
+ # 8. 도메인 가드 테스트
335
+ print(f"\n[8] 도메인 가드 (domain={mem.domain}):")
336
+ tests = [
337
+ ("인구 > 출생", True),
338
+ ("인구 > 가구", True),
339
+ ("고용 > 임금", False),
340
+ ("환경 > 기상", False),
341
+ (None, True), # category 없으면 통과
342
+ ]
343
+ for cat, expected in tests:
344
+ actual = mem.domain_matches_category(cat)
345
+ status = "✓" if actual == expected else "✗"
346
+ print(f" {status} '{cat}' → {actual} (기대={expected})")
347
+
348
+ # 9. rejected stat_id
349
+ mem.record_stat_id_rejected("DT_WRONG_ID", "domain mismatch")
350
+ print(f"\n[9] is_stat_id_rejected('DT_WRONG_ID') = "
351
+ f"{mem.is_stat_id_rejected('DT_WRONG_ID')}")
352
+
353
+ # 10. add_note
354
+ mem.add_note("verify", "domain_guard_triggered", 2)
355
+ print(f"[10] notes={mem.notes}")
356
+
357
+ # 11. stats + dump
358
+ print(f"\n[11] stats: {mem.stats()}")
359
+
360
+ print(f"\n[12] dump (요약):")
361
+ d = mem.dump()
362
+ for k, v in d.items():
363
+ v_str = str(v)
364
+ if len(v_str) > 80:
365
+ v_str = v_str[:77] + "..."
366
+ print(f" {k:20s} = {v_str}")
367
+
368
+ print(f"\n{'='*60}")
369
+ print("모든 테스트 통과")
370
+ print(f"{'='*60}")
@@ -0,0 +1,27 @@
1
+ # Dockerfile.scraper — LLM 스크래핑 코드 실행용 격리 컨테이너
2
+ #
3
+ # [김예슬 - 2026-04-28]
4
+ # - python:3.13-slim 기반
5
+ # - httpx, beautifulsoup4만 설치 (최소 의존성)
6
+ # - 비root 사용자(scraper)로 실행 (보안)
7
+ # - 네트워크: docker run 시 --network=bridge 로 외부 인터넷만 허용
8
+ # - 파일시스템: 스크립트 파일만 read-only 마운트
9
+ #
10
+ # 빌드:
11
+ # docker build -f Dockerfile.scraper -t structverify-scraper .
12
+ #
13
+ # 수동 테스트:
14
+ # docker run --rm --memory=256m --cpus=0.5 \
15
+ # -v /tmp/test.py:/app/scraper.py:ro \
16
+ # structverify-scraper python scraper.py
17
+
18
+ FROM python:3.13-slim
19
+
20
+ WORKDIR /app
21
+
22
+ RUN pip install --no-cache-dir httpx beautifulsoup4==4.12.3
23
+
24
+ RUN useradd -m -u 1000 scraper
25
+ USER scraper
26
+
27
+ CMD ["python", "scraper.py"]
File without changes