6a85087b83
전 로컬 LLM 관통 '이드' persona substrate 의 Document Server 측 빌드(W2~W4). 설계 = PKM eid-persona-substrate(r1~r3 수렴) / impl = eid-persona-impl. W2 — compose + 표면 배선: - app/eid/compose.py: persona→rules→overlay→task 단일 system 문자열 + 정적 ROUTE_MAP (런타임 sniffing 아님) + rules 부재 fail-loud · persona 부재 quiet · overflow fail-loud. - 자유-prose 3 표면(react_ask·study_subject_note·study_question_explanation) 중복 정체성· generic 정책 trim + compose 배선(AIClient 에 additive system 파라미터). 도메인 calibration 보존. - STRICT JSON 기계류(briefing_comparative·digest_topic)는 persona-ZERO 동결(불변식 #3). - app/prompts/substrate/: persona(외부 컴파일 산출물 vendor) + rules(생성 가드 서브셋) + overlay 5. W3 — migration + 워커 + study_diagnosis: - migration 301~305: eid_* append-only 원장(약점/복습초안/회고) + approval_requests(가변 큐) + 일정 파생뷰 2. - app/workers/study_weakness.py: study_question_progress.pattern_state 집계로 약점 derived 산출 (LLM 0) + bounded tier(watch/review/focus). nightly cron. - study_diagnosis 표면: 최신 스냅샷을 코치 언어로 번역(약점 판정은 코드, LLM 은 블록 값만 인용). W4-1 — egress 코드층 박탈: - app/eid/ai.py EidAIClient: 이드 표면 = call_primary(내부 MLX) only. 외부 LLM fallback 경로 구조적 봉쇄(call_fallback raise · 자동 fallback 제거 · 외부 endpoint 차단). egress 워커는 분리 유지. load-bearing 정정 3(환경 grounding 강제, 설계 회귀 아님): - rules = 운영 ruleset 전체 → 생성 가드 서브셋(HTML 산출물 룰이 study task 와 충돌). - append-only = REVOKE → CREATE RULE DO INSTEAD NOTHING(단일 owner role 은 REVOKE 무효 + migration 검증기가 plpgsql BEGIN 거부) + actor/source_* NOT NULL 스탬프. - 이드 LLM 봉쇄 = path discipline → EidAIClient 구조화. 검증: eid 순수 단위테스트 30 통과 + py_compile + migration 검증기 모사 + egress 적대감사 COMPLETE. DB/LLM/httpx 의존 테스트(append-only RULE·EidAIClient·E2E)는 staging(Docker) 가동. W4-2 네트워크 belt 은 조건부 보류(코드층 1차 충분, P0-3② 원격 실측 후 hard-gate 시 승격). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
111 lines
3.9 KiB
Python
111 lines
3.9 KiB
Python
"""eid.compose 단위 테스트 — persona→rules→overlay→task 합성 (stdlib only, venv 불필요).
|
|
|
|
실행: python3 tests/eid/test_compose.py (또는 pytest tests/eid/test_compose.py)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# app/ 를 import 루트로 (repo_root/tests/eid/ → repo_root/app)
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "app"))
|
|
|
|
from eid.compose import ( # noqa: E402
|
|
SEP,
|
|
SubstrateOverflow,
|
|
_persona,
|
|
compose,
|
|
is_composed_surface,
|
|
)
|
|
|
|
_TASK = "<<<TASK_SENTINEL>>>"
|
|
|
|
|
|
def test_order_persona_rules_task():
|
|
out = compose("react_ask", _TASK)
|
|
# persona(이드 정체성) · rules(생성 가드, '보수적'=conservative 룰) · task 모두 존재
|
|
assert "이드" in out, "persona 미주입"
|
|
assert "보수적" in out, "rules(생성 서브셋) 미주입"
|
|
assert _TASK in out, "task 미포함"
|
|
# 순서: persona < rules < task
|
|
assert out.index("이드") < out.index("보수적") < out.index(_TASK), "persona→rules→task 순서 위반"
|
|
|
|
|
|
def test_base_surface_has_no_overlay():
|
|
out = compose("study_subject_note", _TASK)
|
|
assert "학습 진단 코치" not in out, "base 표면에 기능 overlay 누출"
|
|
assert "뉴스 큐레이터" not in out
|
|
|
|
|
|
def test_overlay_surface_includes_overlay_between_rules_and_task():
|
|
out = compose("study_diagnosis", _TASK)
|
|
assert "학습 진단 코치" in out, "study overlay 미주입"
|
|
# overlay 는 rules 뒤, task 앞
|
|
assert out.index("보수적") < out.index("학습 진단 코치") < out.index(_TASK)
|
|
|
|
|
|
def test_unknown_surface_falls_back_to_base():
|
|
out = compose("totally_unknown_surface", _TASK)
|
|
assert "이드" in out and _TASK in out # persona+rules+task 유지
|
|
assert "학습 진단 코치" not in out # overlay 없음
|
|
|
|
|
|
def test_is_composed_surface():
|
|
assert is_composed_surface("react_ask")
|
|
assert is_composed_surface("study_diagnosis")
|
|
assert not is_composed_surface("classify") # 기계류 9종 = 미등록
|
|
assert not is_composed_surface("briefing_comparative") # JSON 기계류 = persona ZERO
|
|
|
|
|
|
def test_persona_quiet_on_unknown_variant():
|
|
assert _persona("bogus_variant") == "" # quiet fail-open
|
|
|
|
|
|
def test_sep_join_present():
|
|
out = compose("react_ask", _TASK)
|
|
assert SEP in out, "합본 구분자 SEP 누락"
|
|
|
|
|
|
def test_overflow_failloud_never_silent_drop():
|
|
# 아주 작은 budget → non-droppable floor 초과 → SubstrateOverflow(절대 silent drop 안 함)
|
|
raised = False
|
|
try:
|
|
compose("study_diagnosis", _TASK, budget_chars=50)
|
|
except SubstrateOverflow:
|
|
raised = True
|
|
assert raised, "budget 초과인데 silent 통과 — fail-loud 위반"
|
|
|
|
|
|
def test_generous_budget_passes():
|
|
out = compose("react_ask", _TASK, budget_chars=100_000)
|
|
assert _TASK in out # 넉넉한 예산 = 통과
|
|
|
|
|
|
def test_study_diagnosis_overlay_placeholders_survive_compose():
|
|
# study_diagnosis = study overlay 경로. {weakness_snapshot_block}/{habit_signal_block} 가
|
|
# compose 출력(system)에 리터럴로 남아야 surface 가 .replace 로 실데이터 치환 가능.
|
|
out = compose("study_diagnosis", task="")
|
|
assert "{weakness_snapshot_block}" in out, "약점 placeholder 누락(overlay degrade)"
|
|
assert "{habit_signal_block}" in out, "태도 placeholder 누락"
|
|
filled = out.replace("{weakness_snapshot_block}", "WB").replace("{habit_signal_block}", "HB")
|
|
assert "{weakness_snapshot_block}" not in filled and "WB" in filled and "HB" in filled
|
|
|
|
|
|
def _run():
|
|
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
|
fails = 0
|
|
for fn in fns:
|
|
try:
|
|
fn()
|
|
print(f" PASS {fn.__name__}")
|
|
except Exception as exc: # noqa: BLE001
|
|
fails += 1
|
|
print(f" FAIL {fn.__name__}: {type(exc).__name__}: {exc}")
|
|
print(f"\n{len(fns) - fails}/{len(fns)} passed")
|
|
return 1 if fails else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(_run())
|