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:
+4831
File diff suppressed because it is too large
Load Diff
+11135
File diff suppressed because it is too large
Load Diff
+3376
File diff suppressed because it is too large
Load Diff
@@ -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())
|
||||
@@ -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
@@ -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"],
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user