# -*- coding: utf-8 -*- """ Everything2Everything SSOT 제너레이터. _data/*.json (마스터플랜 + 분석 + 리서치 + 설계안 + 심사 + 메타) 을 읽어 - index.html : 단일 파일 다크테마 대시보드 (외부 의존성 0, 폰트만 CDN fallback) - PLAN.md : 사람이 읽고 다음 세션이 참조하는 마크다운 SSOT 를 생성한다. 다음 세션 사용법: 1) _data/*.json 을 갱신 (로드맵 단계 status 등) 2) python build.py 3) index.html / PLAN.md 가 재생성됨 """ import json import os import html HERE = os.path.dirname(os.path.abspath(__file__)) DATA = os.path.join(HERE, "_data") def load(name): with open(os.path.join(DATA, name), encoding="utf-8") as f: return json.load(f) master = load("master.json") analyses = load("analyses.json") researches = load("researches.json") designs = load("designs.json") ranking = load("ranking.json") meta = load("meta.json") # 로드맵 진행 상태(다음 세션이 갱신). 없으면 모두 'planned'. status_path = os.path.join(DATA, "status.json") if os.path.exists(status_path): with open(status_path, encoding="utf-8") as f: STATUS = json.load(f) else: STATUS = {} def e(s): return html.escape(str(s if s is not None else "")) def nl2br(s): return e(s).replace("\\n", "
").replace("\n", "
") # ------------------------------------------------------------------ CSS CSS = r""" :root{ --bg:#080b11; --bg1:#0d121b; --card:#121a26; --card2:#16202e; --line:#22304333; --line2:#2a3a50; --tx:#e7eef6; --dim:#9fb2c8; --faint:#65788f; --cy:#22d3ee; --em:#34d399; --vi:#a78bfa; --am:#fbbf24; --rs:#fb7185; --bl:#60a5fa; --r:16px; --rs2:10px; --mono:"JetBrains Mono",ui-monospace,"SFMono-Regular",Consolas,"Cascadia Code",monospace; --sans:"Pretendard Variable",Pretendard,-apple-system,"Segoe UI","Malgun Gothic",sans-serif; } *{box-sizing:border-box} html{scroll-behavior:smooth} body{margin:0;background:var(--bg);color:var(--tx);font-family:var(--sans); font-size:15.5px;line-height:1.72;-webkit-font-smoothing:antialiased} body::before{content:"";position:fixed;inset:0;z-index:-2; background: radial-gradient(60% 50% at 78% -8%, #1b3a4a55, transparent 70%), radial-gradient(55% 45% at 12% 4%, #2c224a55, transparent 70%), var(--bg);} body::after{content:"";position:fixed;inset:0;z-index:-1;opacity:.4; background-image:linear-gradient(#ffffff05 1px,transparent 1px),linear-gradient(90deg,#ffffff05 1px,transparent 1px); background-size:54px 54px;mask-image:radial-gradient(circle at 50% 0,#000,transparent 80%)} a{color:var(--cy);text-decoration:none} a:hover{text-decoration:underline} .wrap{display:grid;grid-template-columns:248px minmax(0,1fr);gap:0;max-width:1320px;margin:0 auto} /* nav */ nav{position:sticky;top:0;align-self:start;height:100vh;overflow-y:auto;padding:30px 18px 40px; border-right:1px solid var(--line2)} nav .brand{font-family:var(--mono);font-weight:700;font-size:15px;letter-spacing:-.3px;line-height:1.35; background:linear-gradient(92deg,var(--cy),var(--vi));-webkit-background-clip:text;background-clip:text;color:transparent} nav .brsub{color:var(--faint);font-size:11.5px;margin:6px 0 22px;font-family:var(--mono)} nav a{display:block;color:var(--dim);font-size:13.5px;padding:6px 11px;border-radius:8px;margin:1px 0; border-left:2px solid transparent;transition:.15s} nav a:hover{background:#ffffff08;color:var(--tx);text-decoration:none} nav a.on{color:var(--tx);background:#22d3ee14;border-left-color:var(--cy)} nav .nsec{color:var(--faint);font-size:10.5px;text-transform:uppercase;letter-spacing:.12em;margin:18px 0 6px 11px} main{padding:0 clamp(20px,4vw,60px) 120px;min-width:0} /* hero */ .hero{padding:64px 0 40px} .kick{display:inline-flex;gap:9px;align-items:center;font-family:var(--mono);font-size:12px;color:var(--cy); border:1px solid var(--cy);border-color:#22d3ee44;background:#22d3ee0e;padding:5px 13px;border-radius:999px} .kick b{color:var(--em)} h1{font-size:clamp(30px,5vw,50px);line-height:1.08;letter-spacing:-1.4px;margin:22px 0 0;font-weight:800} h1 .g{background:linear-gradient(96deg,var(--cy) 10%,var(--em) 50%,var(--vi));-webkit-background-clip:text;background-clip:text;color:transparent} .pitch{font-size:18.5px;color:var(--dim);max-width:62ch;margin:20px 0 0;line-height:1.6} .metarow{display:flex;flex-wrap:wrap;gap:10px;margin-top:30px} .chip{font-family:var(--mono);font-size:12px;color:var(--dim);background:var(--card);border:1px solid var(--line2); padding:7px 13px;border-radius:9px} .chip b{color:var(--tx)} .chip .k{color:var(--faint)} /* sections */ section{padding:50px 0;border-top:1px solid var(--line);scroll-margin-top:18px} .sh{display:flex;align-items:baseline;gap:14px;margin:0 0 8px} .sh .no{font-family:var(--mono);font-size:13px;color:var(--cy);font-weight:700} h2{font-size:27px;letter-spacing:-.7px;margin:0;font-weight:750} .sub{color:var(--dim);font-size:15px;margin:0 0 26px;max-width:78ch} .lead{font-size:17px;color:var(--tx);max-width:80ch;line-height:1.7} .dim{color:var(--dim)} /* grid + cards */ .grid{display:grid;gap:14px} .g2{grid-template-columns:repeat(2,1fr)} .g3{grid-template-columns:repeat(3,1fr)} .g4{grid-template-columns:repeat(4,1fr)} @media(max-width:900px){.g2,.g3,.g4{grid-template-columns:1fr}} .card{background:linear-gradient(180deg,var(--card),var(--bg1));border:1px solid var(--line2);border-radius:var(--r); padding:20px 22px;position:relative;transition:.18s} .card:hover{border-color:#3a5170;transform:translateY(-2px);box-shadow:0 14px 40px -22px #000} .card h3{margin:0 0 8px;font-size:16.5px;letter-spacing:-.2px} .card p{margin:0;color:var(--dim);font-size:14px} .num{font-family:var(--mono);color:var(--vi);font-size:13px;font-weight:700} code,.mono{font-family:var(--mono);font-size:.88em} :not(pre)>code{background:#ffffff0d;border:1px solid var(--line);padding:1.5px 6px;border-radius:6px;color:#bfe6ef} /* badges */ .b{display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:11px;font-weight:700; padding:3px 9px;border-radius:7px;letter-spacing:.02em;white-space:nowrap} .b-xl{background:#fb71851f;color:var(--rs);border:1px solid #fb718544} .b-l{background:#fbbf241f;color:var(--am);border:1px solid #fbbf2444} .b-m{background:#22d3ee1f;color:var(--cy);border:1px solid #22d3ee44} .b-s{background:#34d3991f;color:var(--em);border:1px solid #34d39944} .b-high{background:#fb71851f;color:var(--rs);border:1px solid #fb718544} .b-medium{background:#fbbf241f;color:var(--am);border:1px solid #fbbf2444} .b-low{background:#34d3991f;color:var(--em);border:1px solid #34d39944} .b-gh{background:#a78bfa1f;color:var(--vi);border:1px solid #a78bfa44} .b-done{background:#34d39926;color:var(--em);border:1px solid #34d39955} .b-prog{background:#60a5fa22;color:var(--bl);border:1px solid #60a5fa55} .b-plan{background:#ffffff0a;color:var(--faint);border:1px solid var(--line2)} /* roadmap timeline */ .tl{position:relative;margin-top:8px;padding-left:0} .ph{position:relative;border:1px solid var(--line2);border-radius:var(--r);margin:0 0 14px;overflow:hidden; background:linear-gradient(180deg,var(--card),var(--bg1))} .ph>summary{list-style:none;cursor:pointer;padding:18px 22px;display:flex;align-items:center;gap:16px} .ph>summary::-webkit-details-marker{display:none} .ph .pno{font-family:var(--mono);font-weight:800;font-size:15px;min-width:42px;height:42px;display:grid;place-items:center; border-radius:11px;background:#22d3ee14;color:var(--cy);border:1px solid #22d3ee3a} .ph[open] .pno{background:linear-gradient(135deg,var(--cy),var(--vi));color:#04121a;border-color:transparent} .ph .pti{flex:1;min-width:0} .ph .pti b{font-size:16.5px;letter-spacing:-.2px} .ph .pti .pg{color:var(--dim);font-size:13.5px;margin-top:3px} .ph .pbadges{display:flex;gap:7px;flex-wrap:wrap;align-items:center} .ph .body{padding:4px 22px 22px 80px;border-top:1px solid var(--line)} .ph .body h4{margin:18px 0 9px;font-size:12px;text-transform:uppercase;letter-spacing:.11em;color:var(--faint);font-family:var(--mono)} .lst{list-style:none;padding:0;margin:0;display:grid;gap:7px} .lst li{position:relative;padding-left:20px;color:var(--dim);font-size:14px} .lst li::before{content:"";position:absolute;left:2px;top:9px;width:7px;height:7px;border-radius:2px; background:linear-gradient(135deg,var(--cy),var(--em))} .kc{display:grid;gap:6px} .kc .row{display:grid;grid-template-columns:minmax(120px,210px) 1fr;gap:12px;font-size:13.5px; padding:8px 0;border-bottom:1px dashed var(--line)} .kc .row .a{font-family:var(--mono);font-size:12.5px;color:var(--am)} .kc .row .c{color:var(--dim)} .exit{margin-top:16px;background:#34d3990d;border:1px solid #34d39933;border-radius:var(--rs2);padding:12px 15px; font-size:13.5px;color:var(--tx)} .exit b{color:var(--em);font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.08em} .provtag{font-family:var(--mono);font-size:11.5px;color:var(--vi);background:#a78bfa12;border:1px solid #a78bfa33; padding:3px 9px;border-radius:7px;display:inline-block;margin:3px 5px 0 0} /* ADR */ .adr{border:1px solid var(--line2);border-radius:var(--r);overflow:hidden;background:linear-gradient(180deg,var(--card),var(--bg1))} .adr>summary{cursor:pointer;list-style:none;padding:16px 20px;display:flex;gap:14px;align-items:center} .adr>summary::-webkit-details-marker{display:none} .adr .aid{font-family:var(--mono);font-weight:800;color:var(--vi);font-size:13px;min-width:52px} .adr .atit{flex:1;font-weight:650;font-size:15.5px} .adr .arrow{color:var(--faint);transition:.2s} .adr[open] .arrow{transform:rotate(90deg);color:var(--cy)} .adr .abody{padding:2px 20px 20px 20px;border-top:1px solid var(--line);display:grid;gap:12px} .adr .field>span{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--faint);display:block;margin-bottom:3px} .adr .field.dec p{color:var(--tx)} .adr .field p{margin:0;color:var(--dim);font-size:14px} .adr .two{display:grid;grid-template-columns:1fr 1fr;gap:14px} @media(max-width:760px){.adr .two{grid-template-columns:1fr}} /* arch layers */ .layer{display:grid;grid-template-columns:max-content 1fr;gap:18px;align-items:start; border:1px solid var(--line2);border-left:3px solid var(--cy);border-radius:12px;padding:16px 20px;margin:0 0 12px; background:linear-gradient(180deg,var(--card),var(--bg1))} .layer:nth-child(2){border-left-color:var(--em)} .layer:nth-child(3){border-left-color:var(--vi)} .layer:nth-child(4){border-left-color:var(--am)} .layer .ln{font-weight:700;font-size:15px;max-width:210px} .layer .lr{color:var(--dim);font-size:13.5px;margin-top:4px} .layer .comp{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px} .layer .comp span{font-family:var(--mono);font-size:11.5px;color:var(--dim);background:#ffffff08;border:1px solid var(--line2);padding:3px 9px;border-radius:7px} .flow{margin-top:14px;background:var(--card2);border:1px solid var(--line2);border-radius:var(--r);padding:18px 20px} .flow b{color:var(--cy);font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase} .flow p{margin:8px 0 0;color:var(--dim);font-size:14px} /* matrix */ .mtx{display:grid;grid-template-columns:1fr 1fr;gap:14px} @media(max-width:760px){.mtx{grid-template-columns:1fr}} .mbox{border:1px solid var(--line2);border-radius:var(--r);padding:18px 20px;background:linear-gradient(180deg,var(--card),var(--bg1))} .mbox.cur{border-color:#fb718533}.mbox.tgt{border-color:#34d39933} .mbox .t{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.1em;margin-bottom:8px} .mbox.cur .t{color:var(--rs)}.mbox.tgt .t{color:var(--em)} .mbox p{margin:0;color:var(--dim);font-size:14px} .gaps{display:grid;gap:8px;margin-top:14px} .gaps li{list-style:none;display:flex;gap:11px;align-items:flex-start;font-size:14px;color:var(--dim); border:1px solid var(--line2);border-radius:10px;padding:11px 14px;background:var(--card)} .gaps li::before{content:"GAP";font-family:var(--mono);font-size:10px;font-weight:700;color:var(--rs); background:#fb71851a;border:1px solid #fb718540;padding:2px 7px;border-radius:6px;margin-top:1px;flex-shrink:0} .plan-box{margin-top:14px;border:1px solid #22d3ee33;background:#22d3ee08;border-radius:var(--r);padding:18px 20px} .plan-box .t{font-family:var(--mono);font-size:11px;color:var(--cy);text-transform:uppercase;letter-spacing:.09em;margin-bottom:8px} .plan-box p{margin:0;color:var(--dim);font-size:14px;line-height:1.7} /* use case pills */ .uc{display:grid;gap:8px} .uc li{list-style:none;font-size:14px;color:var(--dim);padding-left:26px;position:relative} .uc li::before{content:"AI";position:absolute;left:0;top:1px;font-family:var(--mono);font-size:9.5px;font-weight:800; color:var(--vi);background:#a78bfa18;border:1px solid #a78bfa44;border-radius:6px;padding:2px 5px} /* table */ .tbl{width:100%;border-collapse:collapse;font-size:13.8px;margin-top:6px} .tbl th{text-align:left;font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.07em; color:var(--faint);padding:9px 12px;border-bottom:1px solid var(--line2)} .tbl td{padding:11px 12px;border-bottom:1px solid var(--line);color:var(--dim);vertical-align:top} .tbl tr:hover td{background:#ffffff04} .tbl td b{color:var(--tx)} .arrow-c{color:var(--em);font-family:var(--mono)} /* metrics */ .met{display:grid;grid-template-columns:1.3fr auto 1.3fr;gap:0;align-items:center; border:1px solid var(--line2);border-radius:12px;padding:14px 18px;margin-bottom:10px;background:var(--card)} @media(max-width:760px){.met{grid-template-columns:1fr}} .met .mn{font-weight:700;font-size:14.5px;grid-column:1/-1;margin-bottom:6px} .met .from{font-size:13px;color:var(--rs)} .met .to{font-size:13px;color:var(--em);text-align:right} .met .ar{color:var(--faint);font-family:var(--mono);padding:0 16px} @media(max-width:760px){.met .to{text-align:left}.met .ar{padding:4px 0}} /* research / analysis */ .rcard{border:1px solid var(--line2);border-radius:var(--r);overflow:hidden;margin-bottom:12px;background:linear-gradient(180deg,var(--card),var(--bg1))} .rcard>summary{cursor:pointer;list-style:none;padding:16px 20px;display:flex;gap:13px;align-items:center} .rcard>summary::-webkit-details-marker{display:none} .rcard .rt{flex:1;font-weight:650;font-size:15px} .rcard .rdot{width:9px;height:9px;border-radius:50%;background:linear-gradient(135deg,var(--cy),var(--vi));flex-shrink:0} .rcard .rbody{padding:4px 20px 20px;border-top:1px solid var(--line);display:grid;gap:14px} .rcard h4{margin:14px 0 8px;font-size:11.5px;text-transform:uppercase;letter-spacing:.1em;color:var(--faint);font-family:var(--mono)} .srcs{display:flex;flex-wrap:wrap;gap:7px} .srcs a{font-family:var(--mono);font-size:11.5px;background:#ffffff08;border:1px solid var(--line2);padding:4px 9px;border-radius:7px;color:var(--cy)} .weak li::before{background:var(--rs)!important} .blk li::before{background:var(--am)!important} .opp li::before{background:var(--em)!important} /* score bars */ .score{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center;margin:4px 0 14px} .score .bar{height:9px;border-radius:6px;background:#ffffff0d;overflow:hidden} .score .fill{height:100%;border-radius:6px;background:linear-gradient(90deg,var(--cy),var(--em))} .score .v{font-family:var(--mono);font-weight:800;font-size:18px} .win{border-color:#34d39955!important;box-shadow:0 0 0 1px #34d39933, 0 18px 50px -30px #34d39966} .winbadge{font-family:var(--mono);font-size:10px;font-weight:800;color:#04121a;background:var(--em);padding:3px 8px;border-radius:6px} /* handoff */ .handoff{border:1px solid #fbbf2440;background:linear-gradient(180deg,#fbbf240a,var(--bg1));border-radius:var(--r);padding:24px 26px} .handoff .t{font-family:var(--mono);color:var(--am);font-size:12px;letter-spacing:.08em;text-transform:uppercase} .handoff p{color:var(--tx);font-size:15px;line-height:1.78;margin:12px 0 0} .next{margin-top:18px;display:grid;gap:9px} .next li{list-style:none;display:flex;gap:12px;align-items:flex-start;font-size:14.5px;color:var(--dim)} .next li .n{font-family:var(--mono);font-weight:800;color:var(--am);background:#fbbf2418;border:1px solid #fbbf2440; min-width:26px;height:26px;border-radius:8px;display:grid;place-items:center;font-size:12px;flex-shrink:0} footer{border-top:1px solid var(--line2);margin-top:60px;padding:30px 0;color:var(--faint);font-size:12.5px;font-family:var(--mono)} .toggle-all{font-family:var(--mono);font-size:12px;color:var(--cy);background:transparent;border:1px solid #22d3ee44; padding:6px 13px;border-radius:8px;cursor:pointer;margin-bottom:14px} .toggle-all:hover{background:#22d3ee12} @media(max-width:980px){.wrap{grid-template-columns:1fr}nav{display:none}} """ # ------------------------------------------------------------------ JS JS = r""" const secs=[...document.querySelectorAll('section[id]')]; const links=[...document.querySelectorAll('nav a')]; const byId={};links.forEach(a=>byId[a.getAttribute('href').slice(1)]=a); const io=new IntersectionObserver((es)=>{es.forEach(en=>{if(en.isIntersecting){ links.forEach(l=>l.classList.remove('on'));const a=byId[en.target.id];if(a)a.classList.add('on');}});}, {rootMargin:'-12% 0px -78% 0px'}); secs.forEach(s=>io.observe(s)); document.querySelectorAll('[data-toggle]').forEach(btn=>{ btn.addEventListener('click',()=>{ const sel=btn.getAttribute('data-toggle'); const ds=document.querySelectorAll(sel); const anyClosed=[...ds].some(d=>!d.open); ds.forEach(d=>d.open=anyClosed); btn.textContent=anyClosed?btn.dataset.close:btn.dataset.open; }); }); """ # ------------------------------------------------------------------ builders def badge_effort(x): return f'{e(x)}' def badge_risk(x): return f'RISK {e(x)}' def status_badge(phase): st = STATUS.get(phase, "planned") cls = {"done": "b-done", "in_progress": "b-prog", "planned": "b-plan"}.get(st, "b-plan") lab = {"done": "완료", "in_progress": "진행중", "planned": "예정"}.get(st, "예정") return f'{lab}' def principles(): cards = "" for i, p in enumerate(master["designPrinciples"], 1): cards += f"""
P{i:02d}

{e(p['name'])}

{e(p['description'])}

""" return f'
{cards}
' def architecture(): layers = "" for L in master["targetArchitecture"]["layers"]: comps = "".join(f"{e(c)}" for c in L["components"]) layers += f"""
{e(L['name'])}
{e(L['responsibility'])}
{comps}
""" flow = master["targetArchitecture"]["dataFlow"] return f"""

{e(master['targetArchitecture']['overview'])}

{layers}
데이터 흐름

{e(flow)}

""" def adrs(): out = "" for a in master["coreDecisions"]: out += f"""
{e(a['id'])}{e(a['title'])}
결정

{e(a['decision'])}

근거

{e(a['rationale'])}

대안

{e(a.get('alternatives',''))}

트레이드오프

{e(a['tradeoffs'])}

""" return out def roadmap(): out = "" for i, r in enumerate(master["roadmap"]): deliv = "".join(f"
  • {e(d)}
  • " for d in r.get("deliverables", [])) kc = "".join( f'
    {e(c["area"])}
    {e(c["change"])}
    ' for c in r.get("keyChanges", []) ) provs = "".join(f'+ {e(p)}' for p in r.get("newProviders", [])) provs_html = f'

    신규 Provider

    {provs}
    ' if provs else "" dep = r.get("dependsOn") dep_html = f'depends · {e(dep)}' if dep else "" open_attr = " open" if i == 0 else "" out += f"""
    {e(r['phase'])}
    {e(r['title'])}
    {e(r['goal'])}
    {status_badge(r['phase'])}{badge_effort(r['effort'])}{badge_risk(r['risk'])}{dep_html}

    산출물

    {('

    핵심 코드 변경

    '+kc+'
    ') if kc else ''} {provs_html}
    Exit Criteria
    {e(r['exitCriteria'])}
    """ return f'
    {out}
    ' def matrix(): m = master["conversionMatrix"] gaps = "".join(f"
  • {e(g)}
  • " for g in m["gaps"]) return f"""
    현재 상태

    {e(m['currentState'])}

    목표 상태

    {e(m['targetState'])}

    구조적 공백

    그래프 라우팅 설계 — ProviderRegistry → ConversionGraph

    {e(m['graphRoutingPlan'])}

    """ def ai_section(): ai = master["aiIntegration"] uc = "".join(f"
  • {e(u)}
  • " for u in ai["useCases"]) return f"""

    Codex non-interactive OAuth

    {e(ai['codexOAuth'])}

    API 키 모드 (기본 경로)

    {e(ai['apiMode'])}

    활용 사례 (AI 전용 신규 엣지)

    아키텍처 — AI는 끄면 사라지는 부가 엣지

    {e(ai['architecture'])}

    """ def media_section(): m = master["mediaLayer"] rows = [ ("영상 (Video)", m["video"], "--cy"), ("오디오 (Audio)", m["audio"], "--em"), ("PDF 압축", m["pdfCompression"], "--vi"), ("이미지 최적화", m["imageOptim"], "--am"), ] cards = "".join( f'

    {e(t)}

    {e(d)}

    ' for t, d, c in rows ) return f"""
    {cards}
    외부 바이너리 · 라이선스 게이트 전략

    {e(m['approach'])}

    """ def risks(): rows = "" for r in master["riskRegister"]: rows += f"""{e(r['risk'])} {badge_risk(r['likelihood'])}{badge_risk(r['impact'])} {e(r['mitigation'])}""" return f"""{rows}
    리스크발생가능영향완화책
    """ def metrics(): out = "" for m in master["successMetrics"]: out += f"""
    {e(m['metric'])}
    {e(m['current'])}
    {e(m['target'])}
    """ return out def research_section(): out = "" for r in researches: libs = "" for L in r.get("libraries", []): libs += f"""{e(L.get('name'))}{e(L.get('purpose'))} {e(L.get('license'))}{e(L.get('maturity'))}""" libtbl = (f'

    라이브러리 · 도구

    {libs}
    이름용도라이선스성숙도
    ') if libs else "" finds = "".join(f"
  • {e(k)}
  • " for k in r.get("keyFindings", [])) srcs = "".join( f'{e(s.split("//")[-1][:46])}' for s in r.get("sources", []) if str(s).startswith("http") ) out += f"""
    {e(r['topic'])}

    핵심 발견

      {finds}

    권장 접근 (.NET 9 / 단일 EXE)

    {e(r.get('recommendedApproach'))}

    {libtbl}

    통합 노트

    {e(r.get('integrationNotes'))}

    {('

    출처

    '+srcs+'
    ') if srcs else ''}
    """ return out def analysis_section(): out = "" for a in analyses: weak = "".join(f"
  • {e(w)}
  • " for w in a.get("weaknesses", [])) blk = "".join(f"
  • {e(b)}
  • " for b in a.get("extensibilityBlockers", [])) opp = "".join( f'
  • {e(o["title"])} impact {e(o["impact"])} effort {e(o["effort"])}
  • ' for o in a.get("improvementOpportunities", []) ) out += f"""
    {e(a['subsystem'])}

    {e(a.get('summary'))}

    설계 약점

      {weak}

    확장성 차단 요소 (file:line)

      {blk}

    개선 기회

      {opp}
    """ return out def designs_section(): rk = {r["angle"].split("(")[0].strip(): r for r in ranking["rankings"]} # map by index hint in angle text cards = "" for idx, d in enumerate(designs): # find matching ranking by 'idx N' rinfo = next((r for r in ranking["rankings"] if f"idx {idx}" in r["angle"]), None) score = rinfo["score"] if rinfo else "-" is_win = (rinfo and rinfo["score"] == max(rr["score"] for rr in ranking["rankings"])) pct = (score if isinstance(score, (int, float)) else 0) moves = "".join(f"
  • {e(mv)}
  • " for mv in d.get("keyMoves", [])[:6]) win_html = 'SELECTED BASE' if is_win else "" cards += f"""

    {e(d['angle'])}

    {win_html}
    {e(score)}

    {e(d['vision'])[:340]}…

    핵심 변경

    최대 리스크 · {e(d.get('biggestRisk',''))[:170]}

    """ graft = "".join(f"
  • {e(g)}
  • " for g in ranking["bestIdeasToGraft"]) return f"""
    {cards}
    심사 종합 권고

    {nl2br(ranking['recommendation'])}

    마스터플랜에 흡수한 최고의 아이디어

    """ # nav NAV_ITEMS = [ ("개요", [("vision", "비전"), ("principles", "설계 원칙"), ("architecture", "타깃 아키텍처")]), ("설계", [("adr", "핵심 결정 (ADR)"), ("matrix", "변환 매트릭스"), ("ai", "AI 통합"), ("media", "미디어 레이어")]), ("실행", [("roadmap", "로드맵 P1–P8"), ("metrics", "성공 지표"), ("risks", "리스크 레지스터"), ("handoff", "다음 세션 인계")]), ("근거", [("designs", "설계안 · 심사"), ("research", "인터넷 리서치"), ("analysis", "코드 분석")]), ] def nav(): out = "" for grp, items in NAV_ITEMS: out += f'
    {grp}
    ' for hid, lab in items: out += f'{lab}' return out u = meta["usage"] metarow = f"""
    생성 {e(meta['generatedDate'])}
    방법 멀티에이전트 Workflow
    에이전트 {u['agents']}
    서브에이전트 토큰 {u['subagentTokens']:,}
    도구 호출 {u['toolUses']}
    종합 {e(meta['winningSynthesis'])}
    """ HTML = f""" {e(meta['title'])}
    MASTER PLAN · Single Source of Truth

    모든 변환을 엣지로, 엔진이 경로를 합성하는 만능 변환기

    {e(master['elevatorPitch'])}

    {metarow}
    00

    북극성 비전

    {e(master['vision'])}

    01

    설계 원칙

    아키텍처를 관통하는 불변식. 모든 코드 변경은 이 원칙을 위배하지 않아야 한다.

    {principles()}
    02

    타깃 아키텍처

    4계층 변환 그래프 아키텍처. 핵심은 ProviderRegistry를 단일 홉 딕셔너리에서 ConversionGraph로 승격하는 것.

    {architecture()}
    03

    핵심 아키텍처 결정 (ADR)

    현재 코드의 구체적 한계를 직접 겨냥한 결정. 클릭하면 근거·대안·트레이드오프가 펼쳐진다.

    {adrs()}
    04

    변환 매트릭스 · 그래프 라우팅

    단일 홉 → 멀티홉 자동 합성. transitive closure로 "이 파일로 만들 수 있는 모든 포맷"이 폭발한다.

    {matrix()}
    05

    AI 통합 — Codex OAuth + API

    키가 없어도 모든 기존 변환은 100% 동작. AI는 ✨ 배지로만 opt-in 노출되는 부가가치 엣지.

    {ai_section()}
    06

    미디어 레이어 — 영상·오디오·PDF 압축

    FFmpeg(LGPL 분리 호출)·Ghostscript(AGPL 감지만)로 카테고리를 미디어 변환기로 점프.

    {media_section()}
    07

    실행 로드맵 · P1 → P8

    각 단계가 독립적으로 가치를 전달하고 이전 단계에 의존한다. P1은 그래프 코어 + 즉시 체감(PDF 압축)을 함께 심는다.

    {roadmap()}
    08

    성공 지표

    현재 → 목표. 각 지표가 로드맵 완료를 객관적으로 측정한다.

    {metrics()}
    09

    리스크 레지스터

    가장 큰 위협은 "보이지 않는 리팩터링의 함정"과 GPL/AGPL 라이선스 오염.

    {risks()}
    10

    다음 세션 인계 노트

    이 문서가 SSOT다. 다음 세션은 아래 순서대로 시작한다.

    ▶ Handoff — 어디서부터 시작하고 무엇을 먼저 검증할지

    {nl2br(master['ssotNotes'])}

    11

    설계안 비교 · 심사

    3개 독립 아키텍트가 서로 다른 각도에서 제안했고, 심사가 점수화·종합했다.

    {designs_section()}
    12

    인터넷 리서치 (7개 토픽)

    2026년 6월 기준 WebSearch로 조사한 라이브러리·방법론·라이선스. 클릭하면 출처까지 펼쳐진다.

    {research_section()}
    13

    코드 심층 분석 (5개 서브시스템)

    현재 코드를 직접 읽고 file:line으로 인용한 약점·확장 차단 요소·개선 기회.

    {analysis_section()}
    """ with open(os.path.join(HERE, "index.html"), "w", encoding="utf-8") as f: f.write(HTML) # ------------------------------------------------------------------ Markdown SSOT def md(): L = [] L.append(f"# {meta['title']}\n") L.append(f"> {meta['subtitle']}\n") L.append(f"**생성** {meta['generatedDate']} · **방법** {meta['method']} ") L.append(f"**규모** {u['agents']} agents · {u['subagentTokens']:,} subagent tokens · {u['toolUses']} tool calls ") L.append(f"**종합** {meta['winningSynthesis']}\n") L.append("> 이 문서는 SSOT다. `docs/ssot/_data/*.json` 을 갱신하고 `python docs/ssot/build.py` 로 재생성한다. 웹 버전은 `docs/ssot/index.html`.\n") L.append("---\n## 엘리베이터 피치\n") L.append(master["elevatorPitch"] + "\n") L.append("## 북극성 비전\n") L.append(master["vision"] + "\n") L.append("## 설계 원칙\n") for i, p in enumerate(master["designPrinciples"], 1): L.append(f"{i}. **{p['name']}** — {p['description']}") L.append("\n## 타깃 아키텍처\n") L.append(master["targetArchitecture"]["overview"] + "\n") for Lr in master["targetArchitecture"]["layers"]: L.append(f"- **{Lr['name']}** — {Lr['responsibility']} \n `{'`, `'.join(Lr['components'])}`") L.append(f"\n**데이터 흐름:** {master['targetArchitecture']['dataFlow']}\n") L.append("## 핵심 아키텍처 결정 (ADR)\n") for a in master["coreDecisions"]: L.append(f"### {a['id']} · {a['title']}") L.append(f"- **결정:** {a['decision']}") L.append(f"- **근거:** {a['rationale']}") if a.get("alternatives"): L.append(f"- **대안:** {a['alternatives']}") L.append(f"- **트레이드오프:** {a['tradeoffs']}\n") L.append("## 실행 로드맵\n") for r in master["roadmap"]: dep = f" · depends: {r['dependsOn']}" if r.get("dependsOn") else "" st = STATUS.get(r["phase"], "planned") L.append(f"### {r['phase']} · {r['title']} `effort:{r['effort']}` `risk:{r['risk']}` `status:{st}`{dep}") L.append(f"**목표:** {r['goal']}\n") L.append("**산출물:**") for d in r.get("deliverables", []): L.append(f"- {d}") if r.get("keyChanges"): L.append("\n**핵심 코드 변경:**") for c in r["keyChanges"]: L.append(f"- `{c['area']}` — {c['change']}") if r.get("newProviders"): L.append(f"\n**신규 Provider:** {', '.join(r['newProviders'])}") L.append(f"\n**Exit Criteria:** {r['exitCriteria']}\n") L.append("## 변환 매트릭스 · 그래프 라우팅\n") m = master["conversionMatrix"] L.append(f"- **현재:** {m['currentState']}") L.append(f"- **목표:** {m['targetState']}\n") L.append("**구조적 공백:**") for g in m["gaps"]: L.append(f"- {g}") L.append(f"\n**그래프 라우팅 설계:** {m['graphRoutingPlan']}\n") L.append("## AI 통합\n") ai = master["aiIntegration"] L.append(f"- **Codex OAuth:** {ai['codexOAuth']}") L.append(f"- **API 모드:** {ai['apiMode']}") L.append(f"- **아키텍처:** {ai['architecture']}") L.append("- **활용 사례:**") for x in ai["useCases"]: L.append(f" - {x}") L.append("\n## 미디어 레이어\n") ml = master["mediaLayer"] for k, lab in [("video", "영상"), ("audio", "오디오"), ("pdfCompression", "PDF 압축"), ("imageOptim", "이미지 최적화"), ("approach", "외부 바이너리·라이선스 전략")]: L.append(f"- **{lab}:** {ml[k]}") L.append("\n## 리스크 레지스터\n") L.append("| 리스크 | 발생 | 영향 | 완화책 |\n|---|---|---|---|") for r in master["riskRegister"]: L.append(f"| {r['risk']} | {r['likelihood']} | {r['impact']} | {r['mitigation']} |") L.append("\n## 성공 지표\n") L.append("| 지표 | 현재 | 목표 |\n|---|---|---|") for x in master["successMetrics"]: L.append(f"| {x['metric']} | {x['current']} | {x['target']} |") L.append("\n## 다음 세션 인계 노트 (Handoff)\n") L.append(master["ssotNotes"].replace("\\n", "\n") + "\n") L.append("---\n## 심사 종합 권고\n") L.append(ranking["recommendation"].replace("\\n", "\n") + "\n") L.append("### 마스터플랜에 흡수한 최고의 아이디어\n") for g in ranking["bestIdeasToGraft"]: L.append(f"- {g}") return "\n".join(L) with open(os.path.join(HERE, "PLAN.md"), "w", encoding="utf-8") as f: f.write(md()) print("OK index.html", os.path.getsize(os.path.join(HERE, "index.html")), "bytes") print("OK PLAN.md", os.path.getsize(os.path.join(HERE, "PLAN.md")), "bytes")