refactor(engine): MCP 우선 전환 Phase 1 — 스토리 코어를 LLM 코드에서 분리 - #48
Merged
Conversation
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>
…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>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


MCP 우선 전환(#47)의 **Phase 0(결정 확정)과 Phase 1(스토리 코어 추출)**입니다.
사용자에게 보이는 동작 변화는 0입니다. 컴패니언과 AI 모드는 전과 똑같이 동작합니다. 이 PR은 MCP 서버(Phase 2)가 딛고 설 수 있도록 스토리 코어를 LLM 코드에서 떼어내는 준비 작업입니다.
왜 추출이 먼저인가
MCP의 핵심 툴 두 개가 제거 대상 패키지 위에 서 있었습니다.
linetta_get_story_context→ai.ContextBuilder(internal/ai)linetta_apply_story_ops/linetta_undo_last_change→companion.Service.ApplyOps(internal/companion)즉 "
internal/ai와internal/companion을 지운다"는 계획은 핵심 툴을 같이 죽입니다. 경계는 패키지가 아니라 LLM 루프 vs 스토리 오퍼레이션/컨텍스트이므로, 살릴 것을 먼저 옮기고 나중에 LLM 코드를 지웁니다.변경 내용
42e0a6a3dcd45cinternal/storycontext추출bbf249einternal/storyops추출89fa1e04fb67a1b4ee65fmake test-go에서 강제505e5f2,7e32a97짚어둘 지점
렌더러 평문화 (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 툴입니다.메모리는 인터페이스 주입.
storyops가tars/pkg/memory를 직접 물지 않도록MemoryRecorder로 받습니다. 덕분에 컴패니언 메모리 없이 만들어질 MCP 적용기에서remember옵이 패닉 대신 "memory is not available"로 실패합니다.테스트 전략 (계획서에서 의도적으로 이탈). 계획은 기존 적용/되돌리기 테스트를 storyops로 "옮긴다"였지만, 컴패니언에 남겨 위임 경로를 종단으로 검증하게 하고 storyops에는 적용·되돌리기·롤백·스냅샷·메모리 부재 가드를 직접 검증하는 테스트를 새로 추가했습니다. 이동보다 강한 검증이라 판단했고, 계획서에도 그렇게 기록했습니다.
잠복 버그 하나. 컴패니언 테스트 픽스처가
NewService를 우회해&Service{}를 직접 만들고 있어 위임 필드가 nil이 되는 문제가 있었습니다. 픽스처에 배선을 추가해 해결했습니다.검증
로컬에서 전부 통과했습니다.
RPC 메서드명과 알림 리터럴(
"ai.run","ai.delta","companion.*"등)은 전부 보존됐음을 별도로 확인했습니다 — 프론트엔드 계약은 그대로입니다.다음
Phase 2(MCP 호스트 + 인증 + 읽기 툴 9개)는 이 브랜치 위에 쌓아 별도 PR로 올립니다.
Closes #47 아님 — #47은 전체 전환을 추적하는 상위 이슈로 Phase 7까지 열어 둡니다.
🤖 Generated with Claude Code