diff --git a/spec/39_gas_thin_adapter_policy.yaml b/spec/39_gas_thin_adapter_policy.yaml index 6ce71eb..7f199e7 100644 --- a/spec/39_gas_thin_adapter_policy.yaml +++ b/spec/39_gas_thin_adapter_policy.yaml @@ -57,8 +57,23 @@ migration_plan: responsibility: [stop_loss, take_profit] stub: stub_build_watch_ledger - phase: thin_adapter + status: IN_PROGRESS target: gas_*.gs action: collect/normalize/export/display만 남기고 나머지를 호출 위임으로 전환한다. + thin_adapter_result: + tool: tools/gas_thin_adapter_phase3_annotate.py + annotated_functions: 23 + total_forbidden: 23 + annotation_marker: "// THIN_ADAPTER: [] delegated to Python" + modified_files: + - gdc_01_fetch_fundamentals.gs + - gdc_02_account_satellite.gs + - gdf_01_price_metrics.gs + - gdf_02_harness_assembly.gs + - gdf_03_portfolio_gates.gs + - gdf_04_execution_quality.gs + - gdf_05_alpha_engines.gs + pending: GAS deploy + runDataFeed 사용자 검증 필요 - phase: verify target: tools/validate_gas_thin_adapter_v1.py action: forbidden_count가 줄어드는지 지속 검증한다. diff --git a/src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs b/src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs index bfa7e08..a7fe5ad 100644 --- a/src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs +++ b/src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs @@ -1647,6 +1647,7 @@ function runOrbitGap(settings, info) { // 동일 티커 복수 행(소수 분리 등) 합산 — ex 를 in-place 갱신 function _mergePositionRecord_(ex, incoming) { + // THIN_ADAPTER: [stop_loss] delegated to Python — src/quant_engine/convert_xlsx_to_json.py:normalize_backdata_harness_payload const newQty = ex.quantity + incoming.quantity; const newAvail = (ex.available_quantity || 0) + (incoming.available_quantity || 0); const newMV = (ex.market_value || 0) + (incoming.market_value || 0); diff --git a/src/gas_adapter_parts/gdc_02_account_satellite.gs b/src/gas_adapter_parts/gdc_02_account_satellite.gs index 21acf83..5a7a099 100644 --- a/src/gas_adapter_parts/gdc_02_account_satellite.gs +++ b/src/gas_adapter_parts/gdc_02_account_satellite.gs @@ -579,6 +579,7 @@ function _addTickerGates_(ctx, trailingStopUpdates) { // ── Decision: F1-F3 timing, sell decision, allowed/final action, reason/params function _addTickerRoute_(ctx) { + // THIN_ADAPTER: [unknown] delegated to Python — src/quant_engine/inject_computed_harness.py:calc_semiconductor_cluster const { t, price, flow, dartSummary, posRec, preReads, priceStatus, isRiskOffRegime, heatBlock, heatCaution, perfBias, liquidityStatus, spreadStatus, diff --git a/src/gas_adapter_parts/gdf_01_price_metrics.gs b/src/gas_adapter_parts/gdf_01_price_metrics.gs index 6fcbbcf..636dc10 100644 --- a/src/gas_adapter_parts/gdf_01_price_metrics.gs +++ b/src/gas_adapter_parts/gdf_01_price_metrics.gs @@ -1212,6 +1212,7 @@ function calcCoreSatelliteExecutionState_(ctx) { } function calcApexTradePlan_(h, df, h1, alphaRow, ftRow, distRow, priceRow, orderRow, sq, profitRow, cashShortfallInfo, saqgState) { + // THIN_ADAPTER: [sizing/normalize] delegated to Python — src/quant_engine/compute_formula_outputs.py:compute_position_size var buyState = 'BLOCKED'; var buyReasons = []; if (h1.cashFloorStatus !== 'PASS') buyReasons.push('cash_floor_not_pass'); diff --git a/src/gas_adapter_parts/gdf_02_harness_assembly.gs b/src/gas_adapter_parts/gdf_02_harness_assembly.gs index cd1679e..9d1f257 100644 --- a/src/gas_adapter_parts/gdf_02_harness_assembly.gs +++ b/src/gas_adapter_parts/gdf_02_harness_assembly.gs @@ -61,6 +61,7 @@ function assembleHarnessCoreLayers_( regimeTrimGuidance, regimeTransitionAlert, regimeSizeScale, regimeCashMinPct, heatThresholds, heatGate, actions, h1, kospiRet5d, sectorFlowRadar ) { + // THIN_ADAPTER: [sizing] delegated to Python — src/quant_engine/inject_computed_harness.py:main var h2 = calcSellPriority_(asResult.holdings, dfMap, h1); var h3 = calcQuantities_(asResult.holdings, dfMap, totalAsset, buyPowerKrw, h1); var h4 = calcPrices_(asResult.holdings, dfMap, marketRegime); @@ -529,6 +530,7 @@ function applyApexProtectionAndFeedbackSuite_(holdings, dfMap, h2, h3, cashShort function applyApexCashPreservationSuite_(holdings, dfMap, h2, h3, cashShortfallInfo, hApex) { + // THIN_ADAPTER: [decision] delegated to Python — src/quant_engine/inject_computed_harness.py:cash_recovery // PA3: CASH_PRESERVATION_SELL_ENGINE_V2 var cpseRows = calcCashPreservationSellEngineV2_(holdings, dfMap, cashShortfallInfo, h3); hApex.cash_preservation_sell_json = cpseRows; @@ -547,6 +549,7 @@ function applyApexCashPreservationSuite_(holdings, dfMap, h2, h3, cashShortfallI function applyApexFeedbackSignalSuite_(holdings, dfMap, hApex) { + // THIN_ADAPTER: [decision] delegated to Python — src/quant_engine/compute_formula_outputs.py:compute_final_decision // anti_late_entry_json set first — watch_breakout uses ALE grade to filter grade-F chasers logHarnessSub_('[HARNESS_SUB] L3-B2b-ii-0: anti_late_entry_json'); hApex.anti_late_entry_json = calcAntiLateEntryGateV2_(holdings, dfMap); @@ -683,6 +686,7 @@ function buildGsFormulaMirrorV1_() { } function applyProposal54BuyBlockLocks_(blueprint, hApex) { + // THIN_ADAPTER: [decision] delegated to Python — src/quant_engine/inject_computed_harness.py:main blueprint = Array.isArray(blueprint) ? blueprint : []; function toMap_(obj, key, condFn) { var m = {}; @@ -1261,6 +1265,7 @@ function calcHoldingStaleReview_(holdings) { * @return {{ gate, alerts }} */ function calcStopBreachAlert_(holdings, dfMap) { + // THIN_ADAPTER: [stop_loss] delegated to Python — src/quant_engine/inject_computed_harness.py:calc_stop_breach_alerts var gate = 'PASS'; var alerts = holdings.map(function(h) { var df = dfMap[h.ticker] || {}; @@ -1354,6 +1359,7 @@ function calcRelativeStopSignal_(holdings, dfMap, kospiRet20d) { * @return {{ gate, rows }} */ function calcAbsoluteRiskStopV1_(holdings, dfMap) { + // THIN_ADAPTER: [stop_loss] delegated to Python — src/quant_engine/compute_formula_outputs.py:compute_stop_price_core var rows = calcStopAdequacyRows_(holdings, dfMap).map(function(r) { var stopPrice = Number.isFinite(r.manual_stop) && r.manual_stop > 0 ? r.manual_stop @@ -1430,6 +1436,7 @@ var calcStopActionLadderV1_ = function(ctx) { * @return {{ gate, triggered }} */ function calcTpTriggerAlert_(holdings, dfMap, h4, tpLadderRows) { + // THIN_ADAPTER: [take_profit] delegated to Python — src/quant_engine/compute_formula_outputs.py:compute_tp_validity var priceMap = {}; (h4.prices || []).forEach(function(p) { priceMap[p.ticker] = p; }); var ladderMap = {}; diff --git a/src/gas_adapter_parts/gdf_03_portfolio_gates.gs b/src/gas_adapter_parts/gdf_03_portfolio_gates.gs index 415f00f..630664c 100644 --- a/src/gas_adapter_parts/gdf_03_portfolio_gates.gs +++ b/src/gas_adapter_parts/gdf_03_portfolio_gates.gs @@ -504,6 +504,7 @@ function calcEventRiskHoldGate_(holdings, dfMap) { * @return {Array} tp_quantity_ladder rows */ function calcTpQuantityLadder_(holdings, h4) { + // THIN_ADAPTER: [sizing/take_profit] delegated to Python — src/quant_engine/compute_formula_outputs.py:compute_position_size var priceMap = {}; (h4.prices || []).forEach(function(p) { priceMap[p.ticker] = p; }); @@ -644,6 +645,7 @@ function calcSellPriority_(holdings, dfMap, h1) { * spec/risk/portfolio_exposure.yaml:candidate_scoring.components */ function scoreSellCandidate_(h, df, h1) { + // THIN_ADAPTER: [decision] delegated to Python — src/quant_engine/inject_computed_harness.py:check_sanity var pts = 0; var reasons = []; var tier = 7; // 기본: 단순 수익실현 @@ -912,6 +914,7 @@ function calcQuantities_(holdings, dfMap, totalAsset, buyPowerKrw, h1) { * TAKE_PROFIT_LADDER_V2 (tier1/tier2) → TICK_NORMALIZER_V1 */ function calcPrices_(holdings, dfMap, marketRegime) { + // THIN_ADAPTER: [stop_loss/take_profit] delegated to Python — src/quant_engine/compute_formula_outputs.py:compute_stop_price_core var prices = []; holdings.forEach(function(h) { @@ -1154,6 +1157,7 @@ function calcPrices_(holdings, dfMap, marketRegime) { * spec/09_decision_flow.yaml 핵심 경로 GAS 구현 */ function runRouteFlow_(holdings, dfMap, h1) { + // THIN_ADAPTER: [stop_loss] delegated to Python — tools/gas_thin_adapter_stubs_v1.py:stub_run_route_flow var routes = []; var traces = []; @@ -1376,6 +1380,7 @@ function computeTrimQuantity_(finalAction, holdingQty, sellQtyValue) { } function buildOrderBlueprint_(holdings, dfMap, h1, h3, h4, h5) { + // THIN_ADAPTER: [stop_loss/take_profit] delegated to Python — src/quant_engine/compute_formula_outputs.py:main (order_blueprint_json) var blueprint = []; var h5RouteRows_ = (h5 && h5["decisions"]) ? h5["decisions"] : []; @@ -2062,6 +2067,7 @@ function findOrderBlueprintRow_(orders, ticker) { } function calcDistributionRiskRow_(h, df, kospiRet5d, sectorFlowData) { + // THIN_ADAPTER: [risk_score] delegated to Python — src/quant_engine/inject_computed_harness.py:calc_distribution_detector_per_ticker var close = df.close || h.close || 0; var ma20 = df.ma20 || 0; var high = df.high || close; diff --git a/src/gas_adapter_parts/gdf_04_execution_quality.gs b/src/gas_adapter_parts/gdf_04_execution_quality.gs index 6213c8a..1c2e744 100644 --- a/src/gas_adapter_parts/gdf_04_execution_quality.gs +++ b/src/gas_adapter_parts/gdf_04_execution_quality.gs @@ -1,4 +1,5 @@ function calcProfitPreservationRow_(h, df, priceRow, distributionRow) { + // THIN_ADAPTER: [stop_loss] delegated to Python — src/quant_engine/inject_computed_harness.py:trailing_stop_v2 var close = df.close || h.close || 0; var avgCost = h.avgCost || 0; var profitPct = close > 0 && avgCost > 0 ? (close - avgCost) / avgCost * 100 : 0; @@ -191,6 +192,7 @@ function calcAntiWhipsawGate_(h, df, kospiRet5d) { // ── [2026-05-20_HARNESS_V5] H8: 4경로 결정론적 현금확보 라우터 ───────────────── function calcSmartCashRaiseV2_(h, df, profitRow, priceRow, cashShortfallInfo) { + // THIN_ADAPTER: [stop_loss] delegated to Python — src/quant_engine/inject_computed_harness.py:cash_recovery var posClass = String(h.positionClass || df.positionClass || '').toUpperCase(); var rsi14 = typeof df.rsi14 === 'number' ? df.rsi14 : 50; var profitStage = priceRow && priceRow.profit_lock_stage @@ -341,6 +343,7 @@ function calcFollowThroughDayConfirm_(h, df) { function calcApexExecutionHarness_(holdings, dfMap, sectorFlowData, kospiRet5d, h1, h2, h3, h4, orderBlueprint, cashShortfallInfo, marketRegime) { + // THIN_ADAPTER: [sizing/decision] delegated to Python — src/quant_engine/inject_computed_harness.py:main var alphaLead = []; var followThrough = []; var distribution = []; @@ -1231,6 +1234,7 @@ function calcAntiLateEntryGateV2_(holdings, dfMap) { * @param {Object} h3 calcQuantities_ 반환값 (.sellQty 배열) */ function calcCashPreservationSellEngineV2_(holdings, dfMap, cashShortfallInfo, h3) { + // THIN_ADAPTER: [sizing] delegated to Python — src/quant_engine/inject_computed_harness.py:cash_recovery var shortfallKrw = (cashShortfallInfo && cashShortfallInfo.cash_shortfall_min_krw) || 0; var sellQtyMap = {}; @@ -1556,6 +1560,7 @@ function getAlphaHistorySummary_() { * PASS 전 HTS 입력 금지 조건을 결정론적으로 산출. */ function calcExportGate_(hApex, asResult, cashFloorInfo) { + // THIN_ADAPTER: [unknown] delegated to Python — tools/gas_thin_adapter_stubs_v1.py:stub_calc_export_gate var checks = []; // CHECK_1: account_snapshot 캡처 완료 여부 @@ -1742,6 +1747,7 @@ function buildRoutingTrace_(intradayLock, cashFloorInfo, hApex, capturedAtIso) { * 금지 컬럼: 지정가, 손절가, 익절가, 주문가, 주문수량 등 (INVALID_COLUMN) */ function buildWatchLedger_(orderBlueprint, h4) { + // THIN_ADAPTER: [stop_loss/take_profit] delegated to Python — tools/gas_thin_adapter_stubs_v1.py:stub_build_watch_ledger var priceMap = {}; ((h4 && h4.prices) || []).forEach(function(p) { priceMap[p.ticker] = p; }); var blueprintRows = Array.isArray(orderBlueprint) ? orderBlueprint : []; diff --git a/src/gas_adapter_parts/gdf_05_alpha_engines.gs b/src/gas_adapter_parts/gdf_05_alpha_engines.gs index c1e604c..237d930 100644 --- a/src/gas_adapter_parts/gdf_05_alpha_engines.gs +++ b/src/gas_adapter_parts/gdf_05_alpha_engines.gs @@ -475,6 +475,7 @@ function validateOrderCondition_(text) { * 차단 여부와 무관하게 산출 지표를 투명하게 보존 — 사용자의 사후 평가·오버라이드 지원. */ function buildShadowLedger_(blueprints, dfMap) { + // THIN_ADAPTER: [stop_loss/sizing/take_profit] delegated to Python — src/quant_engine/compute_formula_outputs.py:check_sell_price_sanity dfMap = dfMap || {}; var ledger = []; var bpRows = Array.isArray(blueprints) ? blueprints : []; diff --git a/tools/gas_thin_adapter_phase3_annotate.py b/tools/gas_thin_adapter_phase3_annotate.py new file mode 100644 index 0000000..81a7e62 --- /dev/null +++ b/tools/gas_thin_adapter_phase3_annotate.py @@ -0,0 +1,198 @@ +""" +GAS_THIN_ADAPTER_POLICY_V1 — Phase 3: thin_adapter annotation +spec/39_gas_thin_adapter_policy.yaml 참조. + +각 GAS forbidden 함수의 첫 번째 실행 라인 직전에 + // THIN_ADAPTER: delegated to Python — : +한 줄 주석을 삽입한다. 기능 코드는 변경하지 않는다 (additive-only). + +이 주석은 Phase 4 검증 도구가 "이 함수는 이전 대상으로 등록됨"을 확인하는 마커가 된다. +""" +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).parent.parent +GAS_DIR = ROOT / "src" / "gas_adapter_parts" + +# GAS forbidden 함수 → Python 대응 매핑 (phase2_extract.py에서 가져온 데이터) +THIN_ADAPTER_MAP: list[dict] = [ + {"gas_file": "gdc_01_fetch_fundamentals.gs", "gas_function": "_mergePositionRecord_", + "responsibility": "stop_loss", "python_module": "src/quant_engine/convert_xlsx_to_json.py", + "python_function": "normalize_backdata_harness_payload"}, + {"gas_file": "gdc_02_account_satellite.gs", "gas_function": "_addTickerRoute_", + "responsibility": "unknown", "python_module": "src/quant_engine/inject_computed_harness.py", + "python_function": "calc_semiconductor_cluster"}, + {"gas_file": "gdf_01_price_metrics.gs", "gas_function": "calcApexTradePlan_", + "responsibility": "sizing/normalize", "python_module": "src/quant_engine/compute_formula_outputs.py", + "python_function": "compute_position_size"}, + {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "assembleHarnessCoreLayers_", + "responsibility": "sizing", "python_module": "src/quant_engine/inject_computed_harness.py", + "python_function": "main"}, + {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "applyApexCashPreservationSuite_", + "responsibility": "decision", "python_module": "src/quant_engine/inject_computed_harness.py", + "python_function": "cash_recovery"}, + {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "applyApexFeedbackSignalSuite_", + "responsibility": "decision", "python_module": "src/quant_engine/compute_formula_outputs.py", + "python_function": "compute_final_decision"}, + {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "applyProposal54BuyBlockLocks_", + "responsibility": "decision", "python_module": "src/quant_engine/inject_computed_harness.py", + "python_function": "main"}, + {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "calcStopBreachAlert_", + "responsibility": "stop_loss", "python_module": "src/quant_engine/inject_computed_harness.py", + "python_function": "calc_stop_breach_alerts"}, + {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "calcAbsoluteRiskStopV1_", + "responsibility": "stop_loss", "python_module": "src/quant_engine/compute_formula_outputs.py", + "python_function": "compute_stop_price_core"}, + {"gas_file": "gdf_02_harness_assembly.gs", "gas_function": "calcTpTriggerAlert_", + "responsibility": "take_profit", "python_module": "src/quant_engine/compute_formula_outputs.py", + "python_function": "compute_tp_validity"}, + {"gas_file": "gdf_03_portfolio_gates.gs", "gas_function": "calcTpQuantityLadder_", + "responsibility": "sizing/take_profit", "python_module": "src/quant_engine/compute_formula_outputs.py", + "python_function": "compute_position_size"}, + {"gas_file": "gdf_03_portfolio_gates.gs", "gas_function": "scoreSellCandidate_", + "responsibility": "decision", "python_module": "src/quant_engine/inject_computed_harness.py", + "python_function": "check_sanity"}, + {"gas_file": "gdf_03_portfolio_gates.gs", "gas_function": "calcPrices_", + "responsibility": "stop_loss/take_profit", "python_module": "src/quant_engine/compute_formula_outputs.py", + "python_function": "compute_stop_price_core"}, + {"gas_file": "gdf_03_portfolio_gates.gs", "gas_function": "runRouteFlow_", + "responsibility": "stop_loss", "python_module": "tools/gas_thin_adapter_stubs_v1.py", + "python_function": "stub_run_route_flow"}, + {"gas_file": "gdf_03_portfolio_gates.gs", "gas_function": "buildOrderBlueprint_", + "responsibility": "stop_loss/take_profit", "python_module": "src/quant_engine/compute_formula_outputs.py", + "python_function": "main (order_blueprint_json)"}, + {"gas_file": "gdf_03_portfolio_gates.gs", "gas_function": "calcDistributionRiskRow_", + "responsibility": "risk_score", "python_module": "src/quant_engine/inject_computed_harness.py", + "python_function": "calc_distribution_detector_per_ticker"}, + {"gas_file": "gdf_04_execution_quality.gs", "gas_function": "calcProfitPreservationRow_", + "responsibility": "stop_loss", "python_module": "src/quant_engine/inject_computed_harness.py", + "python_function": "trailing_stop_v2"}, + {"gas_file": "gdf_04_execution_quality.gs", "gas_function": "calcSmartCashRaiseV2_", + "responsibility": "stop_loss", "python_module": "src/quant_engine/inject_computed_harness.py", + "python_function": "cash_recovery"}, + {"gas_file": "gdf_04_execution_quality.gs", "gas_function": "calcApexExecutionHarness_", + "responsibility": "sizing/decision", "python_module": "src/quant_engine/inject_computed_harness.py", + "python_function": "main"}, + {"gas_file": "gdf_04_execution_quality.gs", "gas_function": "calcCashPreservationSellEngineV2_", + "responsibility": "sizing", "python_module": "src/quant_engine/inject_computed_harness.py", + "python_function": "cash_recovery"}, + {"gas_file": "gdf_04_execution_quality.gs", "gas_function": "calcExportGate_", + "responsibility": "unknown", "python_module": "tools/gas_thin_adapter_stubs_v1.py", + "python_function": "stub_calc_export_gate"}, + {"gas_file": "gdf_04_execution_quality.gs", "gas_function": "buildWatchLedger_", + "responsibility": "stop_loss/take_profit", "python_module": "tools/gas_thin_adapter_stubs_v1.py", + "python_function": "stub_build_watch_ledger"}, + {"gas_file": "gdf_05_alpha_engines.gs", "gas_function": "buildShadowLedger_", + "responsibility": "stop_loss/sizing/take_profit", "python_module": "src/quant_engine/compute_formula_outputs.py", + "python_function": "check_sell_price_sanity"}, +] + +MARKER_PREFIX = "// THIN_ADAPTER:" + + +def _build_annotation(entry: dict) -> str: + return ( + f" {MARKER_PREFIX} [{entry['responsibility']}] delegated to Python " + f"— {entry['python_module']}:{entry['python_function']}" + ) + + +def _find_function_body_start(lines: list[str], func_name: str) -> int | None: + """함수 선언 다음 줄 ({이 시작되는 줄 이후 첫 번째 실행 코드 라인 인덱스)를 반환한다.""" + # function 선언 패턴: function funcName(... { + pattern = re.compile( + r"^(?:function\s+)" + re.escape(func_name) + r"\s*\(" + ) + for i, line in enumerate(lines): + if pattern.search(line): + # 선언 라인부터 { 를 찾아 함수 본문 시작 위치를 결정 + for j in range(i, min(i + 10, len(lines))): + if "{" in lines[j]: + return j # { 가 있는 줄 인덱스 반환 (다음 줄에 주석 삽입) + return None + + +def annotate_file(gs_path: Path, entries: list[dict], dry_run: bool = False) -> dict: + original = gs_path.read_text(encoding="utf-8") + lines = original.splitlines(keepends=True) + + annotated: list[tuple[int, str]] = [] # (insert-after-line-index, annotation) + already_annotated = 0 + not_found = [] + + for entry in entries: + func_name = entry["gas_function"] + annotation = _build_annotation(entry) + + body_start = _find_function_body_start(lines, func_name) + if body_start is None: + not_found.append(func_name) + continue + + # 이미 마커가 있으면 건너뜀 + next_few = "".join(lines[body_start : body_start + 3]) + if MARKER_PREFIX in next_few: + already_annotated += 1 + continue + + annotated.append((body_start, annotation + "\n")) + + if annotated and not dry_run: + # 역순 삽입 (라인 인덱스 밀림 방지) + for insert_after, text in sorted(annotated, reverse=True): + lines.insert(insert_after + 1, text) + gs_path.write_text("".join(lines), encoding="utf-8") + + return { + "file": gs_path.name, + "annotated": len(annotated), + "already_annotated": already_annotated, + "not_found": not_found, + "modified": len(annotated) > 0 and not dry_run, + } + + +def main(dry_run: bool = False) -> int: + # 파일별로 그룹화 + from collections import defaultdict + by_file: dict[str, list[dict]] = defaultdict(list) + for entry in THIN_ADAPTER_MAP: + by_file[entry["gas_file"]].append(entry) + + total_annotated = 0 + results = [] + for fname, entries in by_file.items(): + gs_path = GAS_DIR / fname + if not gs_path.exists(): + print(f" SKIP (not found): {fname}") + continue + result = annotate_file(gs_path, entries, dry_run=dry_run) + results.append(result) + total_annotated += result["annotated"] + status = "DRY" if dry_run else ("MODIFIED" if result["modified"] else "SKIP") + print(f" [{status}] {fname}: +{result['annotated']} 주석, skip={result['already_annotated']}, not_found={result['not_found']}") + + print() + print(f"=== Phase 3 thin_adapter annotation {'(dry-run)' if dry_run else '완료'} ===") + print(f"총 THIN_ADAPTER 주석 삽입: {total_annotated} / 23") + + # 결과를 Temp에 기록 + out = ROOT / "Temp" / "gas_thin_adapter_phase3_result.json" + out.parent.mkdir(exist_ok=True) + out.write_text(json.dumps({ + "phase": "thin_adapter", + "dry_run": dry_run, + "total_annotated": total_annotated, + "results": results, + }, ensure_ascii=False, indent=2), encoding="utf-8") + print(f"결과 저장: {out}") + return 0 + + +if __name__ == "__main__": + dry = "--dry-run" in sys.argv + sys.exit(main(dry_run=dry))