Skill · Portfolio
디자인 포트폴리오 구성·첨삭
리서치·스케치·렌더·시제품 자료로 근거 있는 가로형 포트폴리오를 구성하고 첨삭하는 원작 스킬(산업디자인). 원작자 truman-t3, MIT.
0installs
industrial-design-portfolio/scripts/validate_manifest.py
#!/usr/bin/env python3"""Validate the evidence and authorship contract in a portfolio manifest."""from __future__ import annotationsimport argparseimport jsonimport reimport sysfrom pathlib import PathLAYOUTS = {f"ID{i:02d}" for i in range(1, 19)}LEVELS = {"E0", "E1", "E2", "E3"}STATUSES = {"ready", "caveat", "to_validate", "omit"}STYLE_PRESETS = {"workshop-orange", "instrument-blue", "material-green", "gallery-red"}def nonempty(value: object) -> bool: return isinstance(value, str) and bool(value.strip())def finding(level: str, message: str) -> tuple[str, str]: return level, messagedef validate(data: object) -> list[tuple[str, str]]: issues: list[tuple[str, str]] = [] if not isinstance(data, dict): return [finding("P0", "Manifest root must be a JSON object.")] for key in ("schema_version", "portfolio", "projects", "slides", "evidence", "assumptions", "generated_assets", "unresolved_gaps"): if key not in data: issues.append(finding("P0", f"Missing required field: {key}")) if data.get("schema_version") != "1.0": issues.append(finding("P1", "schema_version must be 1.0.")) portfolio = data.get("portfolio") if not isinstance(portfolio, dict): issues.append(finding("P0", "portfolio must be an object.")) else: for key in ("title", "audience", "presentation_context"): if not nonempty(portfolio.get(key)): issues.append(finding("P1", f"portfolio.{key} must be a non-empty string.")) preset = portfolio.get("style_preset") if preset is None: issues.append(finding("P2", "portfolio.style_preset is not recorded; default to workshop-orange or choose a registered preset.")) elif preset not in STYLE_PRESETS: issues.append(finding("P1", "portfolio.style_preset must be workshop-orange, instrument-blue, material-green, or gallery-red.")) if preset is not None and not nonempty(portfolio.get("style_rationale")): issues.append(finding("P2", "portfolio.style_rationale should explain why the preset fits the evidence and audience.")) projects = data.get("projects") project_ids: set[str] = set() if not isinstance(projects, list) or not projects: issues.append(finding("P0", "projects must contain at least one project.")) else: for index, project in enumerate(projects, start=1): if not isinstance(project, dict): issues.append(finding("P0", f"projects[{index}] must be an object.")) continue for key in ("id", "name", "thesis", "role"): if not nonempty(project.get(key)): issues.append(finding("P1", f"projects[{index}].{key} must be a non-empty string.")) project_id = project.get("id") if nonempty(project_id): if not re.fullmatch(r"[a-z0-9-]+", project_id): issues.append(finding("P1", f"projects[{index}].id must use lowercase letters, numbers, or hyphens.")) elif project_id in project_ids: issues.append(finding("P0", f"Duplicate project id: {project_id}.")) else: project_ids.add(project_id) if not isinstance(project.get("contributions"), list) or not project["contributions"] or not all(nonempty(x) for x in project["contributions"]): issues.append(finding("P0", f"projects[{index}].contributions must list personal contributions.")) evidence = data.get("evidence") evidence_ids: set[str] = set() if not isinstance(evidence, list): issues.append(finding("P0", "evidence must be an array.")) else: for index, item in enumerate(evidence, start=1): if not isinstance(item, dict): issues.append(finding("P0", f"evidence[{index}] must be an object.")) continue for key in ("id", "claim", "artifact", "source", "authorship"): if not nonempty(item.get(key)): issues.append(finding("P0", f"evidence[{index}].{key} must be a non-empty string.")) item_id = item.get("id") if nonempty(item_id): if item_id in evidence_ids: issues.append(finding("P0", f"Duplicate evidence id: {item_id}.")) evidence_ids.add(item_id) if item.get("level") not in LEVELS: issues.append(finding("P0", f"evidence[{index}].level must be E0, E1, E2, or E3.")) if item.get("status") not in STATUSES: issues.append(finding("P1", f"evidence[{index}].status is invalid.")) if item.get("level") == "E0" and item.get("status") == "ready": issues.append(finding("P0", f"evidence[{index}] is E0 and cannot be ready; mark it as an assumption, caveat, or omission.")) slides = data.get("slides") if not isinstance(slides, list) or not slides: issues.append(finding("P0", "slides must contain at least one slide.")) else: pages: set[int] = set() for index, slide in enumerate(slides, start=1): if not isinstance(slide, dict): issues.append(finding("P0", f"slides[{index}] must be an object.")) continue page = slide.get("page") if not isinstance(page, int) or page < 1: issues.append(finding("P1", f"slides[{index}].page must be a positive integer.")) elif page in pages: issues.append(finding("P1", f"Duplicate slide page: {page}.")) else: pages.add(page) if slide.get("project_id") not in project_ids: issues.append(finding("P0", f"slides[{index}] references an unknown project_id.")) if slide.get("layout_id") not in LAYOUTS: issues.append(finding("P1", f"slides[{index}].layout_id must be a registered ID01-ID18 layout.")) claim_ids = slide.get("claim_ids") if not isinstance(claim_ids, list): issues.append(finding("P1", f"slides[{index}].claim_ids must be an array.")) else: for claim_id in claim_ids: if claim_id not in evidence_ids: issues.append(finding("P0", f"slides[{index}] references unknown evidence id: {claim_id}.")) generated = data.get("generated_assets") if not isinstance(generated, list): issues.append(finding("P0", "generated_assets must be an array.")) else: for index, asset in enumerate(generated, start=1): if not isinstance(asset, dict) or not nonempty(asset.get("file")) or not nonempty(asset.get("disclosure")): issues.append(finding("P0", f"generated_assets[{index}] must include file and visible disclosure.")) for key in ("assumptions", "unresolved_gaps"): if not isinstance(data.get(key), list): issues.append(finding("P0", f"{key} must be an array.")) return issuesdef main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("manifest", type=Path) args = parser.parse_args() try: data = json.loads(args.manifest.read_text(encoding="utf-8")) except FileNotFoundError: print(f"P0: File not found: {args.manifest}") return 2 except json.JSONDecodeError as exc: print(f"P0: Invalid JSON: {exc}") return 2 issues = validate(data) for level in ("P0", "P1", "P2", "P3"): for item_level, message in issues: if item_level == level: print(f"{level}: {message}") blockers = sum(level in {"P0", "P1"} for level, _ in issues) print(f"Checked {args.manifest}: {len(issues)} finding(s), {blockers} blocker(s).") return 1 if blockers else 0if __name__ == "__main__": sys.exit(main())