1
0
Fork 0
Everything2Everything/docs/ssot/_data/master.json
Yun Chan 232f453e15 docs: 변환 그래프 OS 마스터플랜 SSOT (멀티에이전트 분석+리서치 종합)
전체 소스 심층 분석(5) + 인터넷 리서치(7) → 3 독립 아키텍트 → 심사 랭킹 → 마스터플랜 종합을 Workflow로 오케스트레이션해 생성.

- docs/ssot/_data/*.json : SSOT 원천 데이터 (master/analyses/researches/designs/ranking)
- docs/ssot/build.py : JSON → index.html(웹 대시보드) + PLAN.md 제너레이터
- 9 ADR · 8단계 로드맵(P1 그래프엔진+PDF압축 → P8 굳히기)
- 양방향/다방향 그래프 라우팅, Codex OAuth+API, FFmpeg/Ghostscript 미디어 레이어
2026-06-01 11:38:36 +09:00

552 lines
No EOL
47 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"vision": "Everything2Everything의 북극성은 \"세상의 모든 변환을 원자(atomic) 엣지로 등록하면, 엔진이 그 조합으로 임의의 A→Z를 스스로 합성하고, 변환하면서 AI가 결과를 더 좋게 만드는 변환 그래프 OS\"다. 핵심 통찰은 현재 DocumentProvider.RouteAsync(92-205)가 사실상 '사람이 손으로 그린 Dijkstra'(md→html→docx, docx→html→md, hwp→html→md를 switch에 박아넣음)라는 점이며, 이 손그림을 삭제하고 엔진이 같은 경로를 '계산'하게 만드는 것이 모든 확장의 열쇠다. 그래프가 코어가 되면 FFmpeg(미디어), PDF 압축, HWP 양방향, AI 요약/번역/캡션이 전부 '엣지 추가'로 환원되고, OutputsForInput은 1-hop 직접 출력에서 도달 가능한 모든 포맷(transitive closure)으로 폭발한다. 동시에 사용자가 명시한 신규 가치(PDF 압축·HWP·영상·AI)를 인프라 완성을 기다리지 않고 빠르게 출시해 체감 차별화를 먼저 만든다. AI는 핵심 엔진이 아니라 '키 없으면 조용히 비활성되는 부가가치 엣지'로, 변환의 로컬 예측가능성이라는 신뢰를 절대 깨지 않는다.",
"elevatorPitch": "손으로 짠 변환 switch를 자동 경로 탐색 그래프로 교체하고, 그 위에 PDF 압축·영상·HWP·AI를 엣지로 얹어, 코드 한 줄당 N×M 매트릭스가 발현하는 '변환하면서 더 좋아지는' 만능 변환기.",
"designPrinciples": [
{
"name": "변환은 엣지, 엔진은 라우터",
"description": "모든 Provider는 단일 홉 원자 변환(md→html, png→pdf)만 선언한다. 멀티홉(md→docx)은 절대 Provider 내부에 손으로 짜지 않고 엔진의 그래프 탐색이 자동 합성한다. DocumentProvider.RouteAsync의 switch 지옥이 재발하지 않도록 이를 불변식으로 강제한다."
},
{
"name": "기능이 그래프를 견인하되, 그래프가 기능을 받친다",
"description": "사용자 체감 가치(PDF 압축·HWP·영상·AI)를 빠르게 출시하되, 신규 기능은 반드시 '그래프 엣지'로만 추가한다. Phase 0에 심은 그래프 코어가 하드코딩 유혹을 구조적으로 차단한다."
},
{
"name": "손실은 가중치다",
"description": "품질 손실을 ConversionPair.LossClass(Lossless/Container/Recode/Rasterize)로 SSOT화하고 -log(보존율)+홉페널티로 환산한다. 멀티홉 경로 선택과 UI '손실 변환' 경고 배지가 모두 이 단일 출처를 소비한다."
},
{
"name": "AI는 끄면 사라지는 부가 엣지",
"description": "AI는 절대 기본 경로를 점유하지 않는다. 키가 없으면 모든 기존 변환은 100% 동작하고 AI 페어만 자동 비활성(NotReady)되며 ✨AI 배지로만 opt-in 노출된다. 변환의 로컬 예측가능성 신뢰를 깨지 않는다."
},
{
"name": "무거운 외부 도구는 분리 호출로만",
"description": "FFmpeg(GPL 정적링크 금지·LGPL 분리호출만), Ghostscript/MuPDF(AGPL·사용자 설치본 감지만), H2Orestart/Calibre(GPL·외부 프로세스 분리)를 본체에 절대 정적 링크하지 않는다. 라이선스 경계를 코드 리뷰 게이트로 강제해 상업 배포 오염을 원천 차단한다."
},
{
"name": "순수 .NET 우선, 외부 바이너리 차선",
"description": "단일 포터블 EXE 부담을 줄이기 위해 SharpCompress·Parquet.Net·PDFsharp·Svg.Skia 같은 순수 관리 코드를 EXE에 직접 포함하고, FFmpeg/Pandoc/Calibre 같은 무거운 바이너리는 '외부 설치 감지 + 미설치 시 안내/자동조달' 모델로만 통합한다."
},
{
"name": "점진 마이그레이션, 무중단",
"description": "IConverterProvider/ConvertResult/ConvertOptions 일반화는 기존 8개 Provider를 어댑터로 감싸 한 번에 깨지지 않게 한다. 모든 코어 변경은 회귀 테스트(현재 0개에서 출발)로 '동일 동작'을 객관 증명한다."
}
],
"targetArchitecture": {
"overview": "4계층 변환 그래프 아키텍처. (1) Abstractions 계층이 Provider 계약을 담고, (2) 그래프 코어가 모든 원자 변환을 방향 그래프로 합성해 Dijkstra로 멀티홉 경로를 푼다. (3) Provider 계층은 in-box 코드 Provider(이미지/문서/미디어/AI)와 manifest 기반 외부 도구 어댑터로 나뉘며, (4) 실행 계층(ExternalProcessRunner·ISettingsStore)이 외부 프로세스·설정·키를 횡단 관리한다. 핵심은 ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격하는 것이다.",
"layers": [
{
"name": "Abstractions 계층 (Everything2Everything.Abstractions)",
"responsibility": "Provider 계약을 별도 어셈블리로 분리해 타입 동일성을 보장하고 향후 플러그인의 안정적 참조점을 제공",
"components": [
"IConverterProvider",
"ConvertRequest/ConvertContext",
"ConvertResult(비파일 산출물 포함)",
"ProviderCapability",
"ConversionPair+LossClass",
"ExternalDependency"
]
},
{
"name": "그래프 코어 계층 (Core.Graph)",
"responsibility": "모든 Provider Capability를 순회해 방향 그래프(노드=확장자, 엣지=Provider+LossClass 가중치)를 빌드하고, 자체 Dijkstra로 최저손실 멀티홉 경로를 탐색·실행",
"components": [
"ConversionGraph",
"PathFinder(자체 Dijkstra)",
"ChainExecutor(ExecuteChainAsync)",
"ConversionEngine(라우터로 축소)",
"ProviderRegistry(증분 등록 Register/Rebuild)"
]
},
{
"name": "Provider 계층",
"responsibility": "단일 홉 원자 변환 능력을 선언·실행. in-box 코드 Provider와 manifest 어댑터 Provider 공존",
"components": [
"MagickProvider/PdfProvider/HtmlProvider(기존)",
"LlmProvider(AI)",
"FfmpegProvider(미디어)",
"PdfToolProvider(압축)",
"ImageCombineProvider(N→1)",
"ExternalToolProvider(manifest 어댑터 베이스)"
]
},
{
"name": "실행/인프라 계층",
"responsibility": "외부 프로세스 실행·설정 영속화·키 보안·미리보기를 횡단 제공",
"components": [
"ExternalProcessRunner(CliWrap, 타임아웃+stderr+Kill)",
"ISettingsStore(DPAPI 암호화)",
"ExternalToolDetector(번들 경로 폴백)",
"IPreviewRenderer(프리뷰 캐시)",
"ManifestLoader"
]
}
],
"dataFlow": "파일 입력 → ConversionEngine.ConvertOneAsync가 입력/출력 확장자 정규화 → ConversionGraph.FindBestPath(in,out,options)로 경로 탐색(직접 엣지 있으면 1홉, 없으면 손실가중치 기반 멀티홉) → ChainExecutor가 경로의 각 홉을 순차 실행하며 중간 산출물을 공용 workDir(Temp/e2e_{Guid})에 체이닝 → 각 홉은 Provider.ConvertAsync(ConvertRequest) 호출, 진행률은 홉 수로 분할 매핑 → 마지막 홉 산출물을 OutputPathHelper로 충돌 해결 후 최종 출력 → ConvertResult(출력 경로 + 비파일 산출물) 반환, 중간 산출물 정리. AI/외부도구 엣지는 CheckAvailabilityAsync 게이트를 먼저 통과해야 그래프에 활성 노드로 참여."
},
"coreDecisions": [
{
"id": "ADR-1",
"title": "ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격",
"decision": "_byPair 단일 룩업(ProviderRegistry.cs:6,43)을 유지하되 그 위에 인접 리스트 그래프(Dictionary<string,List<Edge>>)를 빌드하고, 외부 의존성 없는 자체 Dijkstra(80~120줄, .NET 9 PriorityQueue 사용)로 멀티홉 경로를 탐색한다. DocumentProvider.RouteAsync의 손그림 멀티홉을 엔진 합성으로 대체.",
"rationale": "현재 멀티홉이 Provider 내부 switch에 하드코딩되어 형식 N개에 O(N²)로 수동 증식한다. NCSA Polyglot 모델(노드=포맷, 엣지=Provider, 가중치=손실)은 학계 검증된 best practice이며, 그래프가 수십 노드·수백 엣지 규모라 성능 이슈가 없다.",
"alternatives": "QuikGraph(MS-PL, 2022 이후 정체)·Pandoc식 단일 AST 허브(이질적 도메인에 부적합). 자체 구현이 단일 EXE/AOT/라이선스 검토 모두 무부담이라 1순위.",
"tradeoffs": "멀티홉은 중간 임시파일 I/O가 늘고 손실이 누적될 수 있다. 완화: 직접 엣지 우선, MaxHops=3 제한, 손실 블랙리스트, 손실 경로 UI 경고 배지."
},
{
"id": "ADR-2",
"title": "손실을 ConversionPair.LossClass 가중치로 SSOT화",
"decision": "ConversionPair에 LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8) 필드를 추가하고, 엣지 가중치를 -log(품질보존율)+홉페널티+실행비용 합산으로 계산한다. UI '손실 변환' 배지도 이 가중치를 소비.",
"rationale": "손실은 본래 곱셈적(0.9×0.8)이므로 -log 변환으로 덧셈 최단경로(Dijkstra)가 곧 최대 품질보존 경로가 된다. 래스터화(텍스트/벡터→PNG)는 단방향 손실 절벽이므로 큰 페널티로 자연 회피.",
"alternatives": "동적 손실 측정(Versus식 실측). 초기엔 정적 가중치 테이블로 시작하고 동적 측정은 처음부터 넣지 않는다(과도한 복잡도).",
"tradeoffs": "정적 가중치는 추정값이라 일부 쌍에서 비최적 경로 가능. 완화: 보수적으로 직접 엣지 우선, 멀티홉은 fallback으로만 운영."
},
{
"id": "ADR-3",
"title": "레지스트리 충돌을 조용한 first-wins에서 Priority 기반 명시 선택으로 교체",
"decision": "_byPair.TryAdd(ProviderRegistry.cs:22)의 '조용한 첫 등록자 우선'을 ProviderCapability.Priority 필드 + 다중 Provider 공존 모델 + 충돌 시 진단 경고로 교체한다. 같은 (input,output)에 빠른변환/고품질/AI 등 복수 전략 등록 허용.",
"rationale": "PDF압축 vs PDF렌더, AI변환 vs 일반변환처럼 한 쌍에 복수 전략이 필연적으로 생긴다. 현재는 부트스트랩 순서에 따라 비결정적으로 한쪽이 조용히 사라져 데이터 손실이다.",
"alternatives": "현 first-wins 유지(확장 불가). 비용 기반 자동 선택만(사용자 전략 선택 불가). Priority+공존이 그래프 가중치와도 자연 연결.",
"tradeoffs": "같은 쌍에 복수 Provider가 등록되면 UI에서 전략 선택지를 노출해야 하는 추가 복잡도. 완화: 기본은 최저비용 자동 선택, 고급 모드에서만 명시 선택."
},
{
"id": "ADR-4",
"title": "IConverterProvider 시그니처를 ConvertRequest/ConvertContext로 일반화",
"decision": "단일 sourcePath/단일 outputExtension/IProgress<double> 고정 시그니처(IConverterProvider.cs:9-15)를 ConvertRequest(다중 입력·옵션 백·미디어 메타) + ConvertContext로 일반화하고, ConvertResult(ConvertResult.cs)에 ExtractedText/AiResponse/Metadata/IntermediateArtifacts 필드를 추가한다. 기존 8개 Provider는 어댑터로 감싸 무중단 마이그레이션.",
"rationale": "현 시그니처는 N→1 결합, AI 비파일 응답, 영상 메타데이터 프로빙, 멀티홉 중간 컨텍스트를 표현할 수 없다. 미디어/AI 엣지가 들어올 '그릇'을 코어에 먼저 판다.",
"alternatives": "시그니처 유지하고 옵션에 모든 것 욱여넣기(갓 오브젝트 가속). 점진 어댑터 전략이 8개 Provider 동시 파괴를 방지.",
"tradeoffs": "어댑터 계층이 일시적 중복을 만든다. 완화: 회귀 테스트로 동일 동작 보장 후 어댑터를 점진 제거."
},
{
"id": "ADR-5",
"title": "AI는 IAiProvider 특수 인터페이스가 아니라 그래프의 부가 엣지로 편입",
"decision": "LlmProvider를 일반 IConverterProvider로 구현하고 Microsoft.Extensions.AI(IChatClient) 추상화 위에 OpenAI/Anthropic 공식 SDK를 연결한다. AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출되고, 키 부재 시 CheckAvailabilityAsync가 NotReady를 반환해 그래프에서 자동 비활성된다.",
"rationale": "AI를 특수 카테고리로 두면 그래프·레지스트리 밖에 별도 배관이 생긴다. 엣지로 환원하면 OcrProvider가 Windows OCR을 흡수한 선례처럼 매트릭스에 자연 편입되고, AI 후처리 파이프(OCR→LLM 교정)도 멀티홉으로 자동 합성된다.",
"alternatives": "별도 IAiConverterProvider 확장(추상화 분기 증가). 통합 IConverterProvider가 단순하고 그래프와 정합.",
"tradeoffs": "AI는 비결정적·유료·네트워크 의존이라 '재현 가능한 변환'과 충돌. 완화: ✨AI 배지·기본 경로 불점유·키 없으면 비활성 불변식."
},
{
"id": "ADR-6",
"title": "무거운 외부 도구는 분리 프로세스 호출 + 라이선스 게이트로만 통합",
"decision": "FFmpeg는 BtbN lgpl-shared 빌드를 별도 프로세스로 호출(LGPL 준수), Ghostscript/MuPDF는 AGPL이라 사용자 설치본 감지만, H2Orestart/Calibre/Pandoc은 GPL이라 외부 프로세스 분리. 공통 ExternalProcessRunner(CliWrap, 타임아웃+stderr+Kill)로 통일하고, 라이선스 경계를 코드 리뷰 게이트로 강제한다.",
"rationale": "단일 포터블 EXE 상업 배포에서 GPL/AGPL 바이너리 정적 링크는 즉시 라이선스 오염이다. 이미 LibreOffice를 외부 도구로 다루는 검증된 패턴을 그대로 확장.",
"alternatives": "GPL 빌드 번들(라이선스 위반)·상업 라이선스 구매(비용). 분리 호출 + 사용자 설치 감지/LGPL 자동조달이 안전.",
"tradeoffs": "진정한 자족 EXE가 아니라 외부 의존 체인이 길어진다. 완화: 순수 .NET 라이브러리 우선, 외부 도구는 NotReady로 친절히 안내."
},
{
"id": "ADR-7",
"title": "CombineAsync를 ImageCombineProvider(N→1 엣지)로 분리",
"decision": "ConversionEngine.CombineAsync의 ImageMagick 직접 의존(ConversionEngine.cs:2,164-257)과 정적 HashSet(CombinableInputs/Outputs:14-23)을 IMultiInputProvider 추상화로 분리한다. 엔진은 라이브러리 중립이 되고 결합 가능 형식은 Provider 능력 선언으로 통합.",
"rationale": "현재 '결합'이 Provider 추상화 밖에 있어 엔진이 ImageMagick에 결합되고, PDF 병합·동영상 concat·오디오 믹스 같은 비이미지 결합으로 확장 불가하다. 정적 HashSet과 능력 선언의 이중 관리도 해소.",
"alternatives": "현 구조 유지(이미지 결합만 영구 고착). N→1 추상화가 모든 결합을 동일 패턴으로 흡수.",
"tradeoffs": "결합 진행률 보고가 단일 출력 가정과 달라 재설계 필요. 완화: ConvertProgress를 N→1 케이스로 확장."
},
{
"id": "ADR-8",
"title": "ConvertOptions 갓 오브젝트를 그래프 옵션 + 형식별 옵션 백으로 분해",
"decision": "11개 sub-record 갓 오브젝트(ConvertOptions.cs:35-55)를 그래프 옵션(AllowMultiHop/MaxHops/AvoidLossy) + 형식별 옵션 백(IReadOnlyDictionary 또는 Provider 선언형 스키마)으로 분해한다. Video/Audio/Ai/PdfCompress를 sub-record 증식 없이 수용하고 ISettingsStore(DPAPI 암호화)로 영속화.",
"rationale": "형식 추가마다 sub-record가 비대해지고 모든 Provider가 무관한 옵션을 끌고 다닌다. 영상 코덱·AI 프롬프트·PDF 압축 레벨을 담을 자리가 코어 record 증식 없이 필요하다.",
"alternatives": "sub-record 계속 추가(god object 가속). 옵션 백이 형식별 옵션만 주입해 확장성 확보.",
"tradeoffs": "강타입 안전성이 약화된다. 완화: Provider가 옵션 스키마(이름/타입/범위/기본값)를 선언하고 UI가 동적 생성·검증."
},
{
"id": "ADR-9",
"title": "manifest는 풀 DSL이 아니라 단순 CLI용 선언적 인자 템플릿으로 제한 채택",
"decision": "manifest를 ExternalProcessRunner 위의 '선언적 인자 템플릿({input}/{output}/{outdir}/{format})'으로만 좁게 채택해 qpdf/Ghostscript 같은 단순 CLI 압축 도구를 코드 없이 추가한다. 복잡 로직(FFmpeg HW가속 폴백·AI)은 in-box 코드 Provider 원칙을 P1부터 못박는다.",
"rationale": "Provider 8개·테스트 0개 단일 개발자 프로젝트에 풀 manifest DSL·동적 ALC 로더는 ROI가 낮다. FFmpeg의 nvenc→AV1 조건부 폴백은 manifest로 표현 불가하므로 하이브리드 경계가 필수.",
"alternatives": "풀 플러그인 생태계(과잉 엔지니어링)·전부 코드(확장 비용). 좁은 manifest가 단순 도구 추가 비용만 제거.",
"tradeoffs": "manifest가 또 다른 갓 오브젝트가 될 위험. 완화: '90% 단순 CLI만 manifest, 복잡 로직은 in-box' 경계를 P1 불변식으로 명문화."
}
],
"roadmap": [
{
"phase": "P1",
"title": "그래프 엔진 도입 + 즉시 체감 가치(PDF 압축)",
"goal": "ProviderRegistry를 ConversionGraph로 승격하고 멀티홉 경로 탐색을 엔진에 내장한다. 동시에 PDF 압축이라는 즉시 체감 신기능을 출시해 '보이지 않는 리팩터링의 함정'을 회피한다.",
"deliverables": [
"ConversionGraph + 자체 Dijkstra PathFinder(외부 의존성 0, .NET 9 PriorityQueue)",
"ConversionPair.LossClass 필드 + 정적 가중치 테이블",
"ConversionEngine.ConvertOneAsync 그래프 위임 + ChainExecutor(공용 workDir 헬퍼)",
"PdfToolProvider 신설: PDF 압축(Light=PDFsharp 구조최적화, Strong=PDFium 렌더+Magick 재인코딩, Max=Ghostscript 외부폴백) + 병합/분할",
"xUnit 테스트 프로젝트 신설(현재 0개) + 그래프 경로탐색 회귀 테스트"
],
"keyChanges": [
{
"area": "ProviderRegistry.cs",
"change": "_byPair 위에 인접 리스트 그래프 빌드, 증분 등록 Register/Rebuild 추가"
},
{
"area": "ConversionEngine.cs:91",
"change": "TryGet 직접 매핑에서 그래프 FindBestPath→ExecuteChainAsync 위임으로 전환"
},
{
"area": "ConversionPair",
"change": "LossClass 필드 추가, 엣지 가중치 SSOT"
},
{
"area": "신규 PdfToolProvider",
"change": "동일포맷 pdf→pdf Skip(ConversionEngine.cs:88) 우회, 3단계 압축"
}
],
"effort": "L",
"risk": "medium",
"exitCriteria": "기존 모든 변환이 그래프 경로로 동일 동작(회귀 테스트 통과)하고, PDF 파일을 3단계 레벨로 압축해 출력 용량 감소를 GUI에서 확인 가능."
},
{
"phase": "P2",
"title": "손그림 멀티홉 제거 + HWP 한글 양방향",
"goal": "DocumentProvider.RouteAsync의 손코딩 switch를 삭제하고 원자 엣지만 선언하게 해 그래프를 도그푸딩한다. HWP→DOCX/HTML/TXT 출력 매트릭스를 확장해 한글 사용자 핵심 요구를 충족.",
"deliverables": [
"DocumentProvider.RouteAsync(92-205) 삭제 → md→html, html→docx 등 원자 엣지만 선언, md→docx는 엔진 자동 합성",
"HWP/HWPX 출력 확장: HwpxProvider Outputs에 .docx/.html/.txt/.odt 추가(soffice --convert-to 파라미터화)",
".hwp 입력 시 --infilter='Hwp2002_File' 조건부 지정 + 함초롬/맑은고딕 폰트 누락 감지 경고",
"DocumentProvider .pdf pdfdocx/html/txt (soffice) + pdftxt (PdfPig)",
"RouteAsync "
],
"keyChanges": [
{
"area": "DocumentProvider.cs:92-205",
"change": " switch , "
},
{
"area": "HwpxProvider",
"change": "Outputs .docx/.html/.txt/.odt , soffice "
},
{
"area": "DocumentProvider Inputs",
"change": ".pdf PDF "
}
],
"dependsOn": "P1",
"effort": "M",
"risk": "medium",
"exitCriteria": "HWP/HWPX DOCX/HTML/TXT/PDF , mddocx RouteAsync ."
},
{
"phase": "P3",
"title": " + ",
"goal": "3 LibreOffice ExternalProcessRunner (·stderr·Kill), IConverterProvider/ConvertResult ·AI .",
"deliverables": [
"ExternalProcessRunner(CliWrap): +stderr+Kill , LibreOffice 3 ",
"Abstractions (IConverterProvider/ConvertResult , )",
"IConverterProviderConvertRequest/ConvertContext , 8 Provider ",
"ConvertResult ExtractedText/AiResponse/Metadata/IntermediateArtifacts ",
"ISettingsStore(DPAPI ) API · ",
"Priority _byPair.TryAdd first-wins "
],
"keyChanges": [
{
"area": "3 Provider",
"change": "ConvertWithLibreOfficeAsync ExternalProcessRunner "
},
{
"area": "IConverterProvider.cs:9-15",
"change": "ConvertRequest/ConvertContext "
},
{
"area": "ConvertResult.cs",
"change": " "
},
{
"area": " ISettingsStore",
"change": "DPAPI ProtectedData JSON "
}
],
"dependsOn": "P2",
"effort": "L",
"risk": "medium",
"exitCriteria": "LibreOffice stderr , ( )."
},
{
"phase": "P4",
"title": " / ·",
"goal": "FFmpeg ' ' . / N×M · .",
"newProviders": [
"FfmpegProvider"
],
"deliverables": [
"FfmpegProvider: FFMpegCore(MIT) + (mp4/mkv/webm/mov/avi/gif)·(mp3/aac/m4a/opus/flac/wav) N×M",
" : ExternalToolDetector.TryFindFfmpeg + BtbN lgpl-shared (SHA256 ), GlobalFFOptions ",
"HW (nvenc/qsv/amf) + SW , NotifyOnProgressIProgress , CancellableThrough(ct)",
" : ConvertManyAsync for-loop(57-70) Parallel.ForEachAsync (MaxDegreeOfParallelism)",
"PreviewServiceIPreviewRenderer + FFmpeg + ",
"ImageMagick ResourceLimits (decompression bomb ) + NU190x "
],
"keyChanges": [
{
"area": " FfmpegProvider",
"change": "FFMpegCore , HW , RequiresExternal"
},
{
"area": "ConversionEngine.cs:57-70",
"change": " for-loop Parallel.ForEachAsync "
},
{
"area": "PreviewService.cs:23",
"change": " switch IPreviewRenderer , "
}
],
"dependsOn": "P3",
"effort": "XL",
"risk": "high",
"exitCriteria": "mp4webm, wavmp3 / HW · , 100 ."
},
{
"phase": "P5",
"title": "AI Codex OAuth + API",
"goal": " 'AI ' . API + SDK, Codex CLI opt-in. AI , .",
"newProviders": [
"LlmProvider"
],
"deliverables": [
"LlmProvider: Microsoft.Extensions.AI(IChatClient) OpenAI/Anthropic SDK + Codex CLI opt-in(codex exec --json --output-schema)",
"AI : (pdf/docx/txttxt/md), (), OCR(OcrProvider 2 ), (png/jpgtxt ), (json Structured Output)",
" : ISettingsStore DPAPI + OPENAI_API_KEY/ANTHROPIC_API_KEY , CheckAvailabilityAsync ",
"UI: AI AI (· ) + // ",
"Codex SemaphoreSlim(1) (auth.json refresh race ) --ephemeral"
],
"keyChanges": [
{
"area": " LlmProvider",
"change": "IConverterProvider , AI "
},
{
"area": "CheckAvailabilityAsync",
"change": "/codex --version NotReady "
},
{
"area": "UI",
"change": "AI , "
}
],
"dependsOn": "P3",
"effort": "L",
"risk": "high",
"exitCriteria": "API PDF · · , AI 100% ."
},
{
"phase": "P6",
"title": " + CLI",
"goal": " N×M· (videomp3txt AI ), CLI · .",
"deliverables": [
"OutputsForInput transitive closure ' ' UI + ",
" : hwppdfpng, videomp3txt(AI) ",
" CLI : --json/--output-dir/--quality/--prompt/--codec/--recursive + stdout JSON + exit code",
" (FileSystemWatcher + + , )",
"QuickProgressWindow + "
],
"keyChanges": [
{
"area": "ProviderRegistry.cs:52",
"change": "OutputsForInput reachability "
},
{
"area": "CliRouter.cs:21",
"change": " + stdout JSON + exit code"
},
{
"area": "App.xaml.cs:96",
"change": "Quick "
}
],
"dependsOn": "P5",
"effort": "L",
"risk": "medium",
"exitCriteria": "HWP PNG() , CLI WPF JSON stdout ."
},
{
"phase": "P7",
"title": " .NET + manifest ",
"goal": "EXE , CLI manifest .",
"newProviders": [
"ArchiveProvider",
"DataProvider",
"VectorProvider",
"PandocProvider",
"EbookProvider"
],
"deliverables": [
"ArchiveProvider(SharpCompress, ) zip/7z/tar/gz/bz2",
"DataProvider(Parquet.Net/ClosedXML/CsvHelper) csvjsonxlsxparquet",
"VectorProvider(Svg.Skia) svgpng/jpg/webp/pdf, EPS Magick+Ghostscript",
"PandocProvider( CLI) md/rst/latex/ipynb/epub , LibreOffice Priority ",
"EbookProvider(Calibre ebook-convert, ) epubmobiazw3pdf",
"manifest (ExternalProcessRunner 릿): qpdf/gs CLI "
],
"keyChanges": [
{
"area": " 4-5 Provider",
"change": " .NET EXE , CLI "
},
{
"area": "ManifestLoader",
"change": "tools/*.manifest.json CLI "
},
{
"area": "Bootstrap",
"change": " Provider + manifest "
}
],
"dependsOn": "P6",
"effort": "L",
"risk": "low",
"exitCriteria": "zip /, csvxlsx, svgpng , manifest CLI ."
},
{
"phase": "P8",
"title": "·· ",
"goal": " ·UI · . .",
"deliverables": [
"UI MVVM (MainWindow.xaml.cs 1171) + Provider UI ",
" Core + + / ",
"CI : NuGet + self-contained portable EXE + (FFmpeg LGPL ) + dotnet test ",
" 3 (AllFormats/PopularOutputs/) FormatCatalog ",
" : OutputPathHelper · ·JSONL round-trip "
],
"keyChanges": [
{
"area": "MainWindow.xaml.cs",
"change": "MVVM , UI"
},
{
"area": "BuildMsix.ps1",
"change": " + "
},
{
"area": "build.yml/release.yml",
"change": "+ +self-contained "
}
],
"dependsOn": "P7",
"effort": "L",
"risk": "low",
"exitCriteria": "PR , self-contained portable EXE , FormatCatalog ."
}
],
"conversionMatrix": {
"currentState": "8 Provider PairsFromMatrix N×M ProviderRegistry (input,output) (_byPair) . (mddocx) DocumentProvider.RouteAsync(92-205) N O(N²) . ' /PDF/' , (/PDF, HWP , /) . (pdfpdf ) ConversionEngine.cs:88 Skip.",
"targetState": "ConversionGraph Provider Capability , Dijkstra AZ . OutputsForInput transitive closure ' ' . PDF/HWP , /, //, AI , (pdfpdf) .",
"gaps": [
"PDF (pdfpdf): Provider PdfToolProvider ",
"PDFDOCX/HTML : LibreOffice ",
"HWP/HWPX : H2Orestart import HWP , DOCX/HTML/TXT ",
"/ : mp4/mp3/flac Provider 0",
"////: (ComingSoon enum )",
"AI (//): ··Provider ",
" ( , PDF ): ConversionEngine.cs:88 Skip "
],
"graphRoutingPlan": "1) ( 1): ProviderRegistry (16-30) Provider Capability.SupportedConversions Dictionary<string,List<Edge>> . = (.png/.pdf/.docx), =Edge{Provider, ConversionPair, Weight}. · . 2) : ConversionPair.LossClass(Lossless=0/Container=0.05/Recode=0.4/Rasterize=0.8) -log() ( ) ( >in-process) . -log . 3) : .NET 9 System.Collections.Generic.PriorityQueue Dijkstra(O(E log V), 80~120) . ConversionEngine.ConvertOneAsync(91) 1( ), FindBestPath(inExt,outExt,options) . AllowMultiHop( true)/MaxHops( 3)/AvoidLossy . 4) : ChainExecutor workDir(Temp/e2e_{Guid}) , IProgress . provider.ConvertAsync ( ). 5) : , ( ), . 6) UI: OutputsForInput reachability(transitive closure) , ' ' ."
},
"aiIntegration": {
"codexOAuth": "Codex CLI PATH opt-in . : ChatGPT OAuth (auth.json access/refresh) Codex api.openai.com Bearer codex CLI . ExternalProcessRunner `codex exec --skip-git-repo-check --json --output-schema schema.json -o out.json --cd <tempdir> \"<프롬프트 + 파일경로>\"` 형태. --skip-git-repo-check는 변환 앱에 필수(git 저장소 아닌 폴더 허용), --output-schema로 응답을 JSON Schema로 강제해 메타데이터 추출, --json으로 JSONL 이벤트 스트림 파싱. CheckAvailabilityAsync에서 `codex --version` 프로브 + auth.json 존재 확인. auth.json refresh 토큰 race를 막기 위해 SemaphoreSlim(1) 직렬화 또는 --ephemeral 사용.",
"apiMode": "기본 경로는 API 키 + 공식 SDK다. Microsoft.Extensions.AI(IChatClient, MIT) 단일 추상화로 OpenAI(공식 OpenAI 패키지, MIT)와 Anthropic(공식 Anthropic 패키지, MIT)을 동일 인터페이스로 다룬다. 사용자는 설정에서 'OpenAI / Claude / Codex CLI / auto'를 고르고 API 키만 입력한다. 키는 ISettingsStore에서 System.Security.Cryptography.ProtectedData(DPAPI, CurrentUser)로 암호화해 %LOCALAPPDATA%에 저장하고, OPENAI_API_KEY/ANTHROPIC_API_KEY 환경변수도 폴백으로 읽어 CI/파워유저 친화. CheckAvailabilityAsync가 키 부재 시 NotReady(키 발급 URL을 ExternalDependency로 안내)를 반환해 그래프에서 자동 비활성.",
"useCases": [
"요약: pdf/docx/txt/md → txt/md (긴 문서를 LLM이 요약)",
"번역: txt/docx/md → txt/docx (대상 언어는 옵션, 비파일 입력 LLM 왕복)",
"OCR 교정: OcrProvider 출력(.txt)을 받아 LLM이 오탈자/줄바꿈 정리 (그래프가 OCR→LLM 2단계 멀티홉으로 자동 합성)",
"이미지 캡션/대체텍스트: png/jpg → txt (비전 모델)",
"문서 언어 번역 + 포맷 정규화: csv→md(표), txt→md",
"메타데이터 생성: 임의 입력 → json (제목/태그/요약, Structured Outputs로 구조화)"
],
"architecture": "LlmProvider를 별도 IAiProvider가 아닌 일반 IConverterProvider로 구현해 그래프의 부가 엣지로 편입한다(ADR-5). AI는 로컬 변환이 없는 신규 페어(요약/번역/캡션/메타데이터)에만 노출되며, 등록 순서로 '로컬 변환이 이미 있는 페어는 로컬 Provider가 우선, AI는 신규 페어만'을 보장한다(Priority 충돌 모델). 불변식: 키가 없어도 모든 기존 변환은 100% 동작하고 AI 페어만 비활성, AI는 절대 기본 경로를 점유하지 않으며 UI에 ✨AI 배지(종량과금·네트워크 명시)로만 opt-in 노출된다. 텍스트 추출이 필요하면 DocumentProvider/PdfProvider/OcrProvider를 주입받아 '추출→LLM' 2단계로 구성(OcrProvider가 PdfProvider를 주입받는 선례). 프라이버시: 로컬 문서가 외부 서버로 전송되므로 명시적 동의 토글 필수(기본 OFF), 미래에 Ollama 로컬 모델 경로를 IChatClient로 열어둔다."
},
"mediaLayer": {
"video": "FfmpegProvider(FFMpegCore 5.4.0, MIT)로 mp4/mkv/webm/mov/avi/gif N×M 트랜스코딩. H.264/H.265는 HW 인코더(h264_nvenc/qsv/amf) 우선, LGPL 빌드엔 libx264/x265(GPL)가 없으므로 HW 미지원 시 AV1(libaom)/VP9(libvpx, 둘 다 BSD-like royalty-free)로 폴백. FFprobe로 duration 확보 후 NotifyOnProgress(Action<double>,TimeSpan)을 IProgress에 직결, CancellableThrough(ct)로 취소.",
"audio": "오디오는 mp3/aac/m4a/opus/ogg/flac/wav N×M. AAC는 FFmpeg 네이티브 aac 인코더(LGPL, libfdk-aac=nonfree 회피), Opus/FLAC/MP3는 LGPL 빌드로 직접 처리. 오디오 전용 출력(flac/mp3)은 영상 입력에서 오디오 트랙만 추출.",
"pdfCompression": "PdfToolProvider 3단계: Light=PDFsharp(MIT, in-process) 또는 qpdf(Apache 2.0) 구조 최적화(object stream 압축·linearize), Strong=PDFium 렌더+ImageMagick 재인코딩(텍스트 선택성 잃지만 라이선스 안전), Max=Ghostscript(-dPDFSETTINGS /screen)는 AGPL이라 번들 금지·사용자 설치본 감지만. 병합/분할/암호화는 PDFsharp 또는 qpdf.",
"imageOptim": "기존 MagickProvider의 ApplyEncoding(jpg/png/webp/avif/tiff 품질·알파평탄화·MaxLongEdge)을 공용 ImageEncoder 헬퍼로 추출해 PdfProvider/HtmlProvider/CombineAsync의 4중 복제를 제거. 동일포맷 이미지 리인코딩(품질 조절)도 엣지로 허용.",
"approach": "단일 포터블 EXE 부담을 줄이기 위해 무거운 바이너리(FFmpeg ~100MB)는 절대 번들하지 않고 'RequiresExternal + 최초 사용 시 자동 다운로드' 모델. 라이선스 게이트(코드 리뷰 강제): FFmpeg는 BtbN lgpl-shared 빌드(--enable-gpl/nonfree 없음)를 별도 프로세스로 호출(동적 분리)해 LGPL 준수 — gyan.dev/BtbN gpl 빌드(GPLv3) 번들 절대 금지. ExternalToolDetector.TryFindFfmpeg가 (a)%LOCALAPPDATA%\\Everything2Everything\\ffmpeg, (b)시스템 PATH 순 탐지, 없으면 lgpl-shared zip을 SHA256 검증 후 다운로드. GlobalFFOptions.Configure로 경로 고정. NVENC는 LGPL 빌드에서 --enable-nonfree 없이 합법 사용 가능(NVIDIA 공식 확인). About 화면에 'uses FFmpeg under LGPLv2.1' 고지 + 소스 다운로드 링크(LGPL 의무). MSIX 변형에서는 샌드박스 정책상 lgpl-shared DLL을 패키지 동봉(여전히 LGPL 준수). Ghostscript/MuPDF(AGPL)는 사용자 설치본 감지만, codec 특허(H.264/AAC) 위험을 줄이려 AV1/VP9/Opus/FLAC(royalty-free)를 기본 권장 출력으로."
},
"riskRegister": [
{
"risk": "'보이지 않는 리팩터링의 함정' — 그래프 코어 재설계가 사용자 체감 변화 0인 상태로 길어짐",
"likelihood": "medium",
"impact": "high",
"mitigation": "P1에서 그래프 도입과 PDF 압축(즉시 체감 신기능)을 묶고, transitive closure로 늘어나는 '만들 수 있는 포맷 목록'을 가시 성과로 노출. DocumentProvider.RouteAsync 삭제를 회귀 테스트로 동일 동작 증명."
},
{
"risk": "'최단 경로' 압박으로 LlmProvider/FfmpegProvider를 또 하드코딩 switch로 끼워넣어 RouteAsync 지옥 재생산",
"likelihood": "medium",
"impact": "high",
"mitigation": "P1에 그래프 코어를 먼저 심어 하드코딩을 구조적으로 차단. '신규 기능은 그래프 엣지로만 추가'를 불변식으로 명문화하고 코드 리뷰 게이트로 강제."
},
{
"risk": "GPL/AGPL 바이너리(FFmpeg gpl빌드·Ghostscript·H2Orestart) 정적 링크로 상업 배포 라이선스 오염",
"likelihood": "medium",
"impact": "high",
"mitigation": "모든 무거운 외부 도구를 별도 프로세스 분리 호출 + 사용자 설치 감지/LGPL 빌드 자동조달로만 통합. 라이선스 경계를 코드 리뷰 게이트로 강제(ADR-6)."
},
{
"risk": "멀티홉 손실 누적·은폐 — HWP→PDF(래스터화)→DOCX가 '편집가능'을 약속하나 이미지 덩어리 반환",
"likelihood": "medium",
"impact": "medium",
"mitigation": "LossClass 가중치로 래스터화에 큰 페널티, 멀티홉은 직접 엣지 없을 때만, MaxHops=3, 손실 블랙리스트, 손실 경로 UI 경고 배지 3겹 가드레일."
},
{
"risk": "인터페이스 일반화(ConvertRequest)가 8개 기존 Provider를 한 번에 깸",
"likelihood": "medium",
"impact": "high",
"mitigation": "기존 시그니처를 어댑터로 감싸 점진 마이그레이션, 무중단을 회귀 테스트로 보장. P3에 배치해 미디어/AI 동기가 코드에 들어온 뒤 일반화."
},
{
"risk": "AI 비결정성·종량과금·네트워크 의존이 '로컬 예측가능 변환' 신뢰를 깸",
"likelihood": "high",
"impact": "medium",
"mitigation": "AI는 기본 경로 불점유, ✨AI 배지 opt-in, 키 없으면 조용히 비활성을 설계 불변식으로 박음. 토큰/비용 표시, 사용자 확인 게이트, 재시도·백오프."
},
{
"risk": "테스트 0개 상태에서 대규모 코어 변경이 회귀를 탐지 못 함",
"likelihood": "high",
"impact": "high",
"mitigation": "P1에서 xUnit 테스트 프로젝트를 최우선 신설하고 그래프 경로탐색·DocumentProvider 회귀를 첫 안전망으로. CI에 dotnet test 게이트 추가."
},
{
"risk": "manifest가 또 다른 갓 오브젝트화 — FFmpeg HW가속 폴백 같은 복잡 로직을 manifest로 표현 시도",
"likelihood": "low",
"impact": "medium",
"mitigation": "manifest는 '90% 단순 CLI(qpdf/gs)만, 복잡 로직은 in-box 코드 Provider' 경계를 P1부터 불변식으로 명문화."
}
],
"successMetrics": [
{
"metric": "멀티홉 경로 자동 합성",
"current": "DocumentProvider.RouteAsync에 손코딩된 3-4개 체인만 동작",
"target": "엔진이 임의 A→Z를 그래프 탐색으로 자동 합성, RouteAsync 0줄"
},
{
"metric": "입력당 도달 가능 출력 포맷 수",
"current": "1-hop 직접 출력만(OutputsForInput 직접 매핑)",
"target": "transitive closure로 확장된 도달 가능 전체 포맷 + 손실 배지"
},
{
"metric": "지원 카테고리 수",
"current": "이미지/PDF/문서/HEIC/OCR (약 5)",
"target": "+영상/오디오/아카이브/데이터/벡터/전자책/AI (약 12)"
},
{
"metric": "PDF 압축 기능",
"current": "어떤 Provider도 수행 불가",
"target": "3단계 레벨(Light/Strong/Max) 압축 + 병합/분할"
},
{
"metric": "HWP 출력 매트릭스",
"current": "→PDF/이미지만, →DOCX/HTML/TXT 미노출",
"target": "HWP→DOCX/HTML/TXT/PDF 완성"
},
{
"metric": "코어 테스트 커버리지",
"current": "테스트 프로젝트 0개",
"target": "그래프 탐색·OutputPathHelper·결합·JSONL round-trip 커버 + CI 게이트"
},
{
"metric": "배치 처리 동시성",
"current": "순차 for-loop(코어 1개만 사용)",
"target": "Parallel.ForEachAsync(MaxDegreeOfParallelism)로 멀티코어 활용"
},
{
"metric": "CLI 자동화 가능성",
"current": "WPF 창만 띄우고 stdout 무반환",
"target": "--json/--codec/--prompt 플래그 + stdout JSON + exit code"
}
],
"ssotNotes": "다음 세션은 P1(그래프 엔진 도입 + PDF 압축)부터 시작한다. 시작 순서와 검증 포인트:\\n\\n1) 가장 먼저 xUnit 테스트 프로젝트를 신설하라(현재 0개). 이게 모든 코어 변경의 안전망이며, 특히 DocumentProvider.RouteAsync 삭제가 '손그림과 동일 동작'임을 증명할 회귀 테스트의 전제다. 먼저 현재 RouteAsync의 모든 경로(md→docx, docx→md, hwp→html 등)에 대한 골든 테스트를 작성해 baseline을 고정하라.\\n\\n2) ConversionGraph + 자체 Dijkstra를 ProviderRegistry 옆에 얇게 얹어라. ProviderRegistry.cs:16-30 생성자 루프에 그래프 빌드 한 단계만 추가. _byPair는 유지(직접 엣지 1홉 호환). ConversionPair에 LossClass 필드 추가가 선결.\\n\\n3) 첫 검증: ConversionEngine.ConvertOneAsync(91)를 그래프 위임으로 바꾼 뒤, 기존 모든 변환이 동일 동작하는지 회귀 테스트로 확인. 그 다음에야 RouteAsync를 삭제하고 원자 엣지만 선언하게 바꿔 md→docx가 그래프 합성으로 동일하게 나오는지 검증.\\n\\n4) PDF 압축(PdfToolProvider)은 ConversionEngine.cs:88의 동일포맷 Skip을 우회해야 한다 — pdf→pdf를 엣지로 허용하는 메커니즘이 그래프 도입과 함께 필요. PDFsharp(MIT) in-process 압축부터 시작하면 외부 의존성 0으로 즉시 체감 가치.\\n\\n먼저 검증할 불변식: (a) 기존 8개 Provider 변환이 그래프 경로로 100% 동일 동작, (b) 멀티홉은 직접 엣지 없을 때만 발동, (c) 손실 경로에 가중치가 정확히 반영되는지. 라이선스 게이트(GPL/AGPL 분리 호출)는 P4(미디어)부터 본격 적용되지만, P1의 Ghostscript 폴백에서도 '사용자 설치본 감지만, 번들 금지' 원칙을 처음부터 지켜라.\\n\\n참고: 빌드 후에는 메모리의 project_build_pipeline(publish + 카스케이드 재등록 PowerShell 시퀀스)를 따르고, 사용자가 직접 push & GUI 검증하는 워크플로이므로 큰 결정은 빠른 승인 후 단일 commit으로 진행."
}