Skip to content

refactor(engine): MCP 우선 전환 Phase 1 — 스토리 코어를 LLM 코드에서 분리 - #48

Merged
devlikebear merged 10 commits into
mainfrom
feat/mcp-pivot-phase1
Aug 23, 2026
Merged

refactor(engine): MCP 우선 전환 Phase 1 — 스토리 코어를 LLM 코드에서 분리#48
devlikebear merged 10 commits into
mainfrom
feat/mcp-pivot-phase1

Conversation

@devlikebear

Copy link
Copy Markdown
Owner

MCP 우선 전환(#47)의 **Phase 0(결정 확정)과 Phase 1(스토리 코어 추출)**입니다.

사용자에게 보이는 동작 변화는 0입니다. 컴패니언과 AI 모드는 전과 똑같이 동작합니다. 이 PR은 MCP 서버(Phase 2)가 딛고 설 수 있도록 스토리 코어를 LLM 코드에서 떼어내는 준비 작업입니다.

왜 추출이 먼저인가

MCP의 핵심 툴 두 개가 제거 대상 패키지 위에 서 있었습니다.

  • linetta_get_story_contextai.ContextBuilder (internal/ai)
  • linetta_apply_story_ops / linetta_undo_last_changecompanion.Service.ApplyOps (internal/companion)

즉 "internal/aiinternal/companion을 지운다"는 계획은 핵심 툴을 같이 죽입니다. 경계는 패키지가 아니라 LLM 루프 vs 스토리 오퍼레이션/컨텍스트이므로, 살릴 것을 먼저 옮기고 나중에 LLM 코드를 지웁니다.

변경 내용

커밋 내용
42e0a6a 설계 문서 + 구현 계획
3dcd45c Task 1.1 internal/storycontext 추출
bbf249e Task 1.2 internal/storyops 추출
89fa1e0 Task 1.3 팩트·메모리·레퍼런스를 스토리 브리프에 병합
4fb67a1 Task 1.4 요약기의 LLM 표면을 한 파일로 격리
b4ee65f 스토리 코어 LLM 무의존을 make test-go에서 강제
505e5f2, 7e32a97 Phase 1 완료 표시, Phase 0 결정 확정

짚어둘 지점

렌더러 평문화 (Task 1.1). ai.BuildMessages는 반환 타입으로 tars/pkg/llm.ChatMessage를 썼지만 내부는 문자열 두 개를 만드는 것뿐이었습니다. storycontext.Render(c) (system, user string)로 바꾸고, internal/ai에는 얇은 BuildMessages 어댑터만 남겼습니다. 이래야 "스토리 코어는 LLM 코드를 import하지 않는다"는 검증이 성립하고, MCP 툴이 마크다운을 직접 렌더링할 수 있습니다.

컨텍스트 조립 경로가 둘이었습니다 (Task 1.3). ai.Context(씬 중심)에는 팩트·메모리·레퍼런스 필드가 아예 없었고, 그 수집은 컴패니언 gatherContext에만 있었습니다. ContextSelection에 토글은 이미 있는데 이를 지키는 빌더가 없는 상태였습니다. 이번에 선택적 소스 인터페이스로 병합해 토글을 완성했습니다. 다만 engineapp에 배선하지 않았으므로 ai.run / ai.preview_context 프롬프트는 바이트 단위로 동일합니다. 첫 소비자는 Phase 2의 MCP 툴입니다.

메모리는 인터페이스 주입. storyopstars/pkg/memory를 직접 물지 않도록 MemoryRecorder로 받습니다. 덕분에 컴패니언 메모리 없이 만들어질 MCP 적용기에서 remember 옵이 패닉 대신 "memory is not available"로 실패합니다.

테스트 전략 (계획서에서 의도적으로 이탈). 계획은 기존 적용/되돌리기 테스트를 storyops로 "옮긴다"였지만, 컴패니언에 남겨 위임 경로를 종단으로 검증하게 하고 storyops에는 적용·되돌리기·롤백·스냅샷·메모리 부재 가드를 직접 검증하는 테스트를 새로 추가했습니다. 이동보다 강한 검증이라 판단했고, 계획서에도 그렇게 기록했습니다.

잠복 버그 하나. 컴패니언 테스트 픽스처가 NewService를 우회해 &Service{}를 직접 만들고 있어 위임 필드가 nil이 되는 문제가 있었습니다. 픽스처에 배선을 추가해 해결했습니다.

검증

로컬에서 전부 통과했습니다.

make test          # 엔진 41/41 + 프론트엔드(lint/vitest/build) + Tauri(cargo check + 14/14)
go test -tags mobile ./...   # 41/41
go build -tags mas ./...     # OK
bash scripts/validate-story-core-deps.sh   # storycontext/storyops의 tars llm·agentloop·session 의존 0

RPC 메서드명과 알림 리터럴("ai.run", "ai.delta", "companion.*" 등)은 전부 보존됐음을 별도로 확인했습니다 — 프론트엔드 계약은 그대로입니다.

다음

Phase 2(MCP 호스트 + 인증 + 읽기 툴 9개)는 이 브랜치 위에 쌓아 별도 PR로 올립니다.

Closes #47 아님 — #47은 전체 전환을 추적하는 상위 이슈로 Phase 7까지 열어 둡니다.

🤖 Generated with Claude Code

devlikebear and others added 8 commits August 22, 2026 23:16
Linetta becomes a pure writing tool; AI collaboration moves to external
MCP clients (Claude Code / Claude Desktop). Tracking issue: #47.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move the story-brief assembly - Context types, ContextSelection,
ContextBuilder, prompt rendering, token estimates - into a new
internal/storycontext package that performs no LLM calls and cannot
import LLM client code (verified: zero tars deps).

The renderer now returns plain strings (storycontext.Render); the only
prompt logic left in internal/ai is the BuildMessages adapter wrapping
Render into llm.ChatMessage for the runner, covered by new adapter
tests. RPC method names and notification literals (ai.run, ai.delta,
...) are unchanged.

Part of the MCP-first pivot (#47), Phase 1 Task 1.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move the story-mutation vocabulary and its applier - Op/Proposal types,
validation with lenient kind normalization, ApplyOps with all-or-nothing
structural rollback, the one-step undo batches, and outline change
counting - into a new internal/storyops package with zero tars
dependencies (memory is injected via a MemoryRecorder interface, so a
future MCP applier without companion memory fails the remember op with
a clear message instead of a panic).

The companion now delegates ApplyOps/UndoApply to storyops and keeps
only what is chat-specific: proposal-fence parsing, intent gating, the
tool registry, and the outline-approval preview. Type aliases keep the
RPC handlers and existing tests unchanged, and those tests now exercise
the delegation end-to-end; new storyops tests cover apply/undo/rollback,
the companion-before snapshot, and the missing-memory guard directly.

Part of the MCP-first pivot (#47), Phase 1 Task 1.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The engine has had two context assembly paths: the scene-centric
ai.ContextBuilder (hierarchical summaries, prev-scene summary, related
scenes) and the companion's gatherContext (the only place facts,
memories, and references were collected) - so the ContextSelection
toggles for those three sections existed without a builder that honored
them. storycontext now completes the merge: optional FactSource /
MemorySource / ReferenceSource interfaces populate Context fields,
ApplyContextSelection clears them when toggled off, and the renderer
carries the sections using the companion's established prompt format.

Nothing wires the sources yet - engineapp is untouched, so ai.run and
ai.preview_context behave exactly as before. The MCP story-context tool
(Phase 2) is the first consumer. Tests cover section rendering, empty
omission, toggle clearing, and a provider-less BuildFull returning a
complete brief.

Part of the MCP-first pivot (#47), Phase 1 Task 1.3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Split provider-driven summarization (client factory, Chat calls, both
prompts) into llm_path.go behind a single summarizeViaLLM helper. The
short-scene plain-text path and the queue/recursion orchestration stay
in summarizer.go and no longer reference tars at all, so the pivot's
removal phase deletes the LLM file wholesale while nodes.update_content
keeps its postUpdate hook and short summaries keep working.

Part of the MCP-first pivot (#47), Phase 1 Task 1.4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
internal/storycontext and internal/storyops must stay free of tars
llm/agentloop/session linkage so the MCP pivot's removal phase can
delete the LLM loop without touching the code the MCP tools stand on.

Part of the MCP-first pivot (#47), Phase 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All five open questions resolved as recommended: remove the companion
only after MCP is validated in real use (Phase 5 -> 6), accept that
mobile loses AI entirely, drop the web_search setting while keeping
web_fetch for Fact Book URL capture, defer a hand-written summary UI to
Phase 7, and target 1.0.0. Downstream tasks that were written
conditionally now state the decision outright.

Part of the MCP-first pivot (#47), Phase 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
devlikebear and others added 2 commits August 23, 2026 19:04
…them

The storyops extraction copied plainTextFromDoc and trimRunes out of
companion/query.go with a comment saying the companion copy would die in
the removal phase. SonarCloud failed the quality gate on it (9.3%
duplication on new code, limit 3%), and it was right to: carrying two
byte-identical implementations through three phases is a real
maintenance hazard, not just a metric.

storyops now exports PlainTextFromDoc and TrimRunes as the canonical
pair; the companion keeps one-line local wrappers so none of its call
sites change. Behavior is identical - the implementations were already
the same bytes.

Part of the MCP-first pivot (#47), Phase 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sixteen builder tests each opened a store, wired the mention resyncer,
and constructed the same seven repos by hand - the block was identical
in twelve of them. Moving the file into storycontext made SonarCloud
count those lines as new code, which is how a long-standing duplication
surfaced as a failing quality gate on this PR.

A ctxFixture now owns the setup and hands back the repos each test
names. Net 52 lines lighter, and a new builder test no longer starts by
copying thirteen lines.

Part of the MCP-first pivot (#47), Phase 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
4.9% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@devlikebear
devlikebear merged commit 4f28dc1 into main Aug 23, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: MCP 우선 전환 — 내장 AI(BYOK)를 외부 MCP 클라이언트 협업으로 이행

1 participant