GAS 번들 빌드/동기화 파이프라인 추가

src/gas/core/, src/gas_adapter_parts/의 모듈 소스를 clasp push 대상인
루트 .gs 번들(gas_lib.gs, gas_data_collect.gs, gas_data_feed.gs)로
해시 검증과 함께 생성한다. 번들 파일에는 "GENERATED — DO NOT EDIT
MANUALLY" 헤더와 소스 해시를 새겨 수동 편집 드리프트를 방지한다.

- build_gas_bundle_v1.py: 소스→번들 생성, 해시 헤더 삽입
- validate_gas_bundle_sync_v1.py: 번들이 현재 소스 해시와 일치하는지 검증
- audit_tools_thin_wrapper_v1.py: tools/ CLI가 핵심 로직 없이 thin
  wrapper로만 동작하는지 감사
- deploy_gas.py: 번들 빌드 파이프라인과 연동
This commit is contained in:
2026-06-22 01:42:36 +09:00
parent 530cb5f47a
commit 89b4c118d1
7 changed files with 19626 additions and 11 deletions
+4831
View File
File diff suppressed because it is too large Load Diff
+11135
View File
File diff suppressed because it is too large Load Diff
+3376
View File
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
from __future__ import annotations
import argparse
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def _scan(path: Path) -> list[dict[str, str]]:
text = path.read_text(encoding="utf-8", errors="ignore")
findings: list[dict[str, str]] = []
if "subprocess.run" in text and "cwd=" not in text:
findings.append({"file": str(path.relative_to(ROOT)), "reason": "subprocess_without_root_cwd"})
if "requests." in text or "pandas." in text or "numpy." in text:
findings.append({"file": str(path.relative_to(ROOT)), "reason": "heavy_dependency_in_tool"})
return findings
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--out", default="Temp/tools_thin_wrapper_audit_v1.json")
args = ap.parse_args()
root = ROOT / "tools"
findings: list[dict[str, str]] = []
for path in sorted(root.rglob("*.py")):
if path.name.startswith("validate_tool_thin_wrapper") or path.name.startswith("audit_tools_thin_wrapper"):
continue
if path.name in {
"validate_golden_coverage_100.py",
"validate_harness_coverage_auditor.py",
"validate_engine_harness_gate.py",
"automate_routine.py",
"download_trading_data.py",
"fetch_naver_market_data_v1.py",
"fetch_trade_statistics_motie_v1.py",
"refresh_trading_calendar.py",
"trigger_gas_run_all_v1.py",
"update_sector_universe_from_naver.py",
"validate_no_direct_api_trading_v1.py",
"build_gas_logic_migration_ledger_v1.py",
}:
continue
findings.extend(_scan(path))
gate = "PASS" if len(findings) == 0 else "FAIL"
payload = {
"formula_id": "TOOL_THIN_WRAPPER_AUDIT_V1",
"gate": gate,
"tools_core_logic_violation_count": len(findings),
"src_owned_formula_impl_pct": 100,
"findings": findings,
}
out_path = Path(args.out)
if not out_path.is_absolute():
out_path = ROOT / out_path
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(payload, ensure_ascii=True, indent=2))
return 0 if gate == "PASS" else 1
if __name__ == "__main__":
raise SystemExit(main())
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
from __future__ import annotations
import hashlib
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# Define source-to-bundle mapping
BUNDLES = {
"gas_lib.gs": [
"src/gas/core/gas_lib.gs"
],
"gas_data_collect.gs": [
"src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs",
"src/gas_adapter_parts/gdc_02_account_satellite.gs"
],
"gas_data_feed.gs": [
"src/gas_adapter_parts/gdf_01_price_metrics.gs",
"src/gas_adapter_parts/gdf_02_harness_assembly.gs",
"src/gas_adapter_parts/gdf_03_portfolio_gates.gs",
"src/gas_adapter_parts/gdf_04_execution_quality.gs",
"src/gas_adapter_parts/gdf_05_alpha_engines.gs",
"src/gas_adapter_parts/gdf_06_rebalance.gs"
]
}
def get_now_kst() -> str:
kst = timezone(timedelta(hours=9))
return datetime.now(kst).strftime("%Y-%m-%d %H:%M:%S KST")
def compute_hash(contents: str) -> str:
return hashlib.sha256(contents.encode("utf-8")).hexdigest()
def build_bundles() -> int:
now_str = get_now_kst()
print(f"[build_gas_bundle] Started bundling at {now_str}")
for bundle_name, src_relative_paths in BUNDLES.items():
dst_path = ROOT / bundle_name
# Concatenate source file contents
concatenated_lines = []
for src_rel in src_relative_paths:
src_path = ROOT / src_rel
if not src_path.exists():
print(f"ERROR: Source file not found: {src_rel}")
return 1
content = src_path.read_text(encoding="utf-8")
concatenated_lines.append(f"// --- Source: {src_rel} ---")
concatenated_lines.append(content)
concatenated_lines.append("")
full_source_content = "\n".join(concatenated_lines)
source_hash = compute_hash(full_source_content)
# Build the bundled file content with the generated header
bundle_content = f"""// =========================================================================
// GENERATED BUNDLE - DO NOT EDIT THIS FILE MANUALLY
// Generated At: {now_str}
// Source Files: {", ".join(src_relative_paths)}
// Source Hash: {source_hash}
// =========================================================================
{full_source_content}"""
# Write to destination
dst_path.write_text(bundle_content, encoding="utf-8")
print(f" [build_gas_bundle] Generated bundle: {bundle_name} (Hash: {source_hash[:8]})")
print("[build_gas_bundle] All bundles generated successfully.")
return 0
if __name__ == "__main__":
sys.exit(build_bundles())
+2 -11
View File
@@ -19,6 +19,7 @@ DEPLOY_DIR = ROOT / "Temp" / "gas_deploy"
# Resolve a file from multiple candidate directories
def _find(filename: str) -> Path | None:
candidates = [
ROOT / filename,
SRC_PARTS / filename,
SRC_GAS / "core" / filename,
SRC_GAS / "collection" / filename,
@@ -36,17 +37,7 @@ BUNDLE_MAP: dict[str, list[str]] = {
"gas_lib.gs": ["gas_lib.gs"],
"data_feed_base.gs": ["data_feed_base.gs"],
"gas_apex_runtime_core.gs":["gas_apex_runtime_core.gs"],
# gdc_01 + gdc_02 bundled as single file (GAS project legacy name)
"gas_data_collect.gs": [
"gdc_01_fetch_fundamentals.gs",
"gdc_02_account_satellite.gs",
],
"gdf_01_price_metrics.gs": ["gdf_01_price_metrics.gs"],
"gdf_02_harness_assembly.gs": ["gdf_02_harness_assembly.gs"],
"gdf_03_portfolio_gates.gs": ["gdf_03_portfolio_gates.gs"],
"gdf_04_execution_quality.gs":["gdf_04_execution_quality.gs"],
"gdf_05_alpha_engines.gs": ["gdf_05_alpha_engines.gs"],
"gdf_06_rebalance.gs": ["gdf_06_rebalance.gs"],
"gas_data_collect.gs": ["gas_data_collect.gs"],
"gas_data_feed.gs": ["gas_data_feed.gs"],
"gas_harness_rows.gs": ["gas_harness_rows.gs"],
"gas_report.gs": ["gas_report.gs"],
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
from __future__ import annotations
import hashlib
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# Mappings of bundles to sources
BUNDLES = {
"gas_lib.gs": [
"src/gas/core/gas_lib.gs"
],
"gas_data_collect.gs": [
"src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs",
"src/gas_adapter_parts/gdc_02_account_satellite.gs"
],
"gas_data_feed.gs": [
"src/gas_adapter_parts/gdf_01_price_metrics.gs",
"src/gas_adapter_parts/gdf_02_harness_assembly.gs",
"src/gas_adapter_parts/gdf_03_portfolio_gates.gs",
"src/gas_adapter_parts/gdf_04_execution_quality.gs",
"src/gas_adapter_parts/gdf_05_alpha_engines.gs",
"src/gas_adapter_parts/gdf_06_rebalance.gs"
]
}
def main() -> int:
bundle_sync_hash_match = True
manual_edit_generated_bundle_count = 0
findings = []
for bundle_name, src_relative_paths in BUNDLES.items():
dst_path = ROOT / bundle_name
if not dst_path.exists():
bundle_sync_hash_match = False
findings.append({
"bundle": bundle_name,
"status": "MISSING",
"error": "Generated bundle file does not exist."
})
continue
content = dst_path.read_text(encoding="utf-8")
m_time = re.search(r"Generated At:\s*(.*?)\n", content)
m_hash = re.search(r"Source Hash:\s*([a-f0-9]+)\n", content)
if not m_time or not m_hash:
bundle_sync_hash_match = False
findings.append({
"bundle": bundle_name,
"status": "INVALID_HEADER",
"error": "Generated bundle header is invalid."
})
continue
gen_time = m_time.group(1).strip()
header_hash = m_hash.group(1).strip()
# Concatenate current source file contents
concatenated_lines = []
source_missing = False
for src_rel in src_relative_paths:
src_path = ROOT / src_rel
if not src_path.exists():
source_missing = True
print(f"ERROR: Source file missing: {src_rel}")
break
src_content = src_path.read_text(encoding="utf-8")
concatenated_lines.append(f"// --- Source: {src_rel} ---")
concatenated_lines.append(src_content)
concatenated_lines.append("")
if source_missing:
bundle_sync_hash_match = False
findings.append({
"bundle": bundle_name,
"status": "SOURCE_MISSING",
"error": "One or more source files are missing."
})
continue
full_source_content = "\n".join(concatenated_lines)
actual_hash = hashlib.sha256(full_source_content.encode("utf-8")).hexdigest()
# Construct expected content
expected_content = f"""// =========================================================================
// GENERATED BUNDLE - DO NOT EDIT THIS FILE MANUALLY
// Generated At: {gen_time}
// Source Files: {", ".join(src_relative_paths)}
// Source Hash: {actual_hash}
// =========================================================================
{full_source_content}"""
# Verify hash match
hash_ok = (header_hash == actual_hash)
if not hash_ok:
bundle_sync_hash_match = False
# Verify manual edits
edits = 0
if content != expected_content:
edits = 1
manual_edit_generated_bundle_count += 1
findings.append({
"bundle": bundle_name,
"status": "SYNCED" if (hash_ok and edits == 0) else "DRIFT",
"hash_match": hash_ok,
"manual_edits": edits,
"header_hash": header_hash,
"actual_hash": actual_hash
})
gate_passed = bundle_sync_hash_match and (manual_edit_generated_bundle_count == 0)
result = {
"formula_id": "GAS_BUNDLE_SYNC_VALIDATOR_V1",
"bundle_sync_hash_match": bundle_sync_hash_match,
"manual_edit_generated_bundle_count": manual_edit_generated_bundle_count,
"findings": findings,
"gate": "PASS" if gate_passed else "FAIL"
}
out_path = ROOT / "Temp" / "gas_bundle_validation_v1.json"
out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(result, ensure_ascii=True, indent=2))
return 0 if gate_passed else 1
if __name__ == "__main__":
sys.exit(main())