init: company-haness 설계
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check an implementation against its exact organization design release binding."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.environ.get("CLAUDE_PROJECT_DIR") or os.path.dirname(os.path.dirname(HERE))
|
||||
RAW_COLOR = re.compile(r"(?<![-\w])#[0-9a-fA-F]{3,8}\b|\brgba?\s*\(")
|
||||
CODE_EXTENSIONS = (".js", ".jsx", ".ts", ".tsx", ".vue", ".svelte")
|
||||
IGNORED_DIRS = {"node_modules", ".git", "dist", "build", "coverage", "generated"}
|
||||
IGNORED_SUFFIXES = (".test", ".spec", ".stories", ".story")
|
||||
|
||||
|
||||
def _sha(path):
|
||||
digest = hashlib.sha256()
|
||||
with open(path, "rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _component_id(value):
|
||||
if isinstance(value, dict):
|
||||
value = value.get("id") or value.get("component-id")
|
||||
value = str(value or "").strip()
|
||||
value = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", value)
|
||||
value = re.sub(r"[^a-zA-Z0-9]+", "-", value).strip("-").lower()
|
||||
return value
|
||||
|
||||
|
||||
def _local_component_files(target):
|
||||
found = {}
|
||||
for base, dirs, files in os.walk(target):
|
||||
dirs[:] = [name for name in dirs if name not in IGNORED_DIRS]
|
||||
parts = {part.lower() for part in os.path.relpath(base, target).split(os.sep)}
|
||||
if not parts.intersection({"component", "components", "ui"}):
|
||||
continue
|
||||
for name in files:
|
||||
stem, ext = os.path.splitext(name)
|
||||
if ext.lower() not in CODE_EXTENSIONS or stem.lower() == "index":
|
||||
continue
|
||||
lower = stem.lower()
|
||||
if any(lower.endswith(suffix) for suffix in IGNORED_SUFFIXES):
|
||||
continue
|
||||
cid = _component_id(stem.removesuffix(".component"))
|
||||
if cid:
|
||||
found.setdefault(cid, []).append(os.path.join(base, name))
|
||||
return found
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--ui-report", required=True)
|
||||
parser.add_argument("--target", required=True)
|
||||
args = parser.parse_args(argv)
|
||||
errors = []
|
||||
try:
|
||||
with open(args.ui_report, encoding="utf-8") as handle:
|
||||
report = yaml.safe_load(handle) or {}
|
||||
body = report.get("payload") if isinstance(report.get("payload"), dict) else report
|
||||
binding = next((item for item in body.get("design-system-bindings", [])
|
||||
if isinstance(item, dict) and item.get("release-id")), None)
|
||||
if not binding:
|
||||
errors.append("exact design-system release binding 없음")
|
||||
else:
|
||||
ref = binding.get("release-ref")
|
||||
path = ref if os.path.isabs(str(ref or "")) else os.path.join(ROOT, str(ref or ""))
|
||||
if not os.path.isfile(path):
|
||||
errors.append("release-ref 파일 없음")
|
||||
elif _sha(path) != binding.get("release-sha256"):
|
||||
errors.append("release-ref SHA 불일치")
|
||||
else:
|
||||
with open(path, encoding="utf-8") as release_handle:
|
||||
release = (yaml.safe_load(release_handle) or {}).get("design-system-release", {})
|
||||
release_components = {_component_id(value) for value in release.get("components") or []}
|
||||
bound_components = {_component_id(value) for value in binding.get("component-ids") or []}
|
||||
if not bound_components.issubset(release_components):
|
||||
errors.append("component-ids에 release 미등록 component 포함")
|
||||
except Exception as exc:
|
||||
errors.append(f"ui report 로드 실패: {exc}")
|
||||
binding = None
|
||||
target = os.path.abspath(args.target)
|
||||
if not os.path.isdir(target):
|
||||
errors.append("target directory 없음")
|
||||
else:
|
||||
for base, _dirs, files in os.walk(target):
|
||||
for name in files:
|
||||
if not name.endswith((".css", ".scss", ".sass", ".less")):
|
||||
continue
|
||||
if "token" in name.lower() or "theme" in name.lower():
|
||||
continue
|
||||
path = os.path.join(base, name)
|
||||
try:
|
||||
content = open(path, encoding="utf-8").read()
|
||||
except Exception:
|
||||
continue
|
||||
if RAW_COLOR.search(content):
|
||||
errors.append(f"raw color token 사용: {os.path.relpath(path, target)}")
|
||||
if binding:
|
||||
local = _local_component_files(target)
|
||||
for cid, paths in sorted(local.items()):
|
||||
if len(paths) > 1:
|
||||
rels = [os.path.relpath(path, target) for path in paths]
|
||||
errors.append(f"local duplicate component id={cid}: {rels}")
|
||||
delta = binding.get("delta") or {}
|
||||
declared_delta = {_component_id(value) for value in delta.get("components") or []}
|
||||
delta_reasons = {
|
||||
_component_id(value): str(value.get("reason") or "").strip()
|
||||
for value in delta.get("components") or [] if isinstance(value, dict)
|
||||
}
|
||||
bound = {_component_id(value) for value in binding.get("component-ids") or []}
|
||||
for cid in sorted((set(local) & bound) - declared_delta):
|
||||
rels = [os.path.relpath(path, target) for path in local[cid]]
|
||||
errors.append(
|
||||
f"organization component local duplicate id={cid}: {rels}; "
|
||||
"재사용하거나 delta.components에 예외를 명시해야 함")
|
||||
for cid in sorted(set(local) & bound & declared_delta):
|
||||
if not delta_reasons.get(cid):
|
||||
errors.append(f"organization component delta id={cid}: reason 필수")
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"[design-adherence] ERROR: {error}", file=sys.stderr)
|
||||
return 2
|
||||
print(f"[design-adherence] OK: release={binding.get('release-id')}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user