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
+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())