#!/usr/bin/env python3 """Create private evidence scores and a privacy-minimized public profile.""" from __future__ import annotations import argparse import json import os import re import sys from collections import defaultdict from datetime import date, datetime, timezone from pathlib import Path from urllib.parse import urlparse SCHEMA_VERSION = "1.0" STAGES = ( "zero_to_one", "one_to_ten", "ten_to_hundred", ) FUNCTIONS = ( "problem_discovery", "product", "engineering", "design", "go_to_market", "operations", "people_leadership", "capital_partnerships", ) EVIDENCE_WEIGHTS = { "customer_outcome": 5, "operational_outcome": 5, "shipped_artifact": 4, "repeated_responsibility": 4, "peer_feedback": 2, "preference": 1, } HIGH_QUALITY_KINDS = { "customer_outcome", "operational_outcome", "shipped_artifact", "repeated_responsibility", } SENSITIVE_PATTERNS = { "email address": re.compile( r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", re.IGNORECASE ), "phone-like number": re.compile(r"(? dict: try: data = json.loads(path.read_text(encoding="utf-8")) except FileNotFoundError as exc: raise ProfileError(f"Input file not found: {path}") from exc except json.JSONDecodeError as exc: raise ProfileError(f"Invalid JSON in {path}: {exc}") from exc if not isinstance(data, dict): raise ProfileError("Profile input must be a JSON object") return data def require_string(value: object, field: str, *, maximum: int = 500) -> str: if not isinstance(value, str) or not value.strip(): raise ProfileError(f"{field} must be a non-empty string") clean = value.strip() if len(clean) > maximum: raise ProfileError(f"{field} exceeds {maximum} characters") return clean def validate_public_text(value: str, field: str) -> str: for label, pattern in SENSITIVE_PATTERNS.items(): if pattern.search(value): raise ProfileError(f"{field} appears to contain a {label}") return value def validate_alias(value: object) -> str: alias = require_string(value, "alias", maximum=50) if not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_-]{1,49}", alias): raise ProfileError( "alias must be a 2-50 character pseudonym using letters, digits, _ or -" ) return alias def validate_url(value: object, field: str, *, contact: bool = False) -> str: url = require_string(value, field, maximum=500) parsed = urlparse(url) if parsed.scheme != "https" or not parsed.hostname or parsed.username: raise ProfileError(f"{field} must be a credential-free HTTPS URL") if parsed.query or parsed.fragment: raise ProfileError(f"{field} must not contain a query string or fragment") if contact: if parsed.hostname.lower() != "github.com": raise ProfileError(f"{field} must use github.com") parts = [part for part in parsed.path.split("/") if part] if len(parts) < 3 or parts[2] not in {"issues", "discussions"}: raise ProfileError( f"{field} must point to a GitHub issues or discussions page" ) return url def validate_dimension_list( values: object, field: str, allowed: tuple[str, ...] ) -> list[str]: if values is None: return [] if not isinstance(values, list): raise ProfileError(f"{field} must be a list") result: list[str] = [] for value in values: if value not in allowed: raise ProfileError( f"{field} contains unsupported value {value!r}; " f"allowed: {', '.join(allowed)}" ) if value not in result: result.append(value) return result def validate_string_list(values: object, field: str, *, limit: int = 10) -> list[str]: if values is None: return [] if not isinstance(values, list) or len(values) > limit: raise ProfileError(f"{field} must be a list with at most {limit} items") result: list[str] = [] for index, value in enumerate(values): item = require_string(value, f"{field}[{index}]", maximum=80) validate_public_text(item, f"{field}[{index}]") if item not in result: result.append(item) return result def parse_iso_date(value: object, field: str) -> date: text = require_string(value, field, maximum=10) try: return date.fromisoformat(text) except ValueError as exc: raise ProfileError(f"{field} must use YYYY-MM-DD") from exc def validate_consent(value: object) -> dict: if not isinstance(value, dict): raise ProfileError("consent must be an object") if value.get("public_profile") is not True: raise ProfileError( "consent.public_profile must be true before generating a public profile" ) approved = parse_iso_date(value.get("approved_at"), "consent.approved_at") expires = parse_iso_date(value.get("expires_on"), "consent.expires_on") today = datetime.now(timezone.utc).date() if approved > today: raise ProfileError("consent.approved_at must not be in the future") if expires < today: raise ProfileError("consent.expires_on is already past") if expires < approved: raise ProfileError("consent.expires_on must not precede approved_at") if (expires - approved).days > 180: raise ProfileError("public profile consent may cover at most 180 days") scope = validate_public_text( require_string(value.get("scope"), "consent.scope", maximum=180), "consent.scope", ) return { "state": "public_profile_approved", "approved_at": approved.isoformat(), "expires_on": expires.isoformat(), "scope": scope, } def score_label(score: int) -> str: if score == 0: return "unknown" if score < 25: return "observed" if score < 50: return "practiced" if score < 75: return "strong" return "standout" def confidence_label(evidence_count: int, strong_count: int) -> str: if evidence_count >= 3 and strong_count >= 2: return "high" if evidence_count >= 2 and strong_count >= 1: return "medium" if evidence_count >= 1: return "low" return "none" def validate_evidence(values: object) -> tuple[list[dict], list[dict]]: if not isinstance(values, list) or not values: raise ProfileError("evidence must be a non-empty list") if len(values) > 50: raise ProfileError("evidence may contain at most 50 items") private_items: list[dict] = [] public_items: list[dict] = [] seen_ids: set[str] = set() for index, raw in enumerate(values): if not isinstance(raw, dict): raise ProfileError(f"evidence[{index}] must be an object") evidence_id = require_string(raw.get("id"), f"evidence[{index}].id", maximum=60) if not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_-]{0,59}", evidence_id): raise ProfileError(f"evidence[{index}].id has invalid characters") if evidence_id in seen_ids: raise ProfileError(f"duplicate evidence id: {evidence_id}") seen_ids.add(evidence_id) kind = raw.get("kind") if kind not in EVIDENCE_WEIGHTS: raise ProfileError( f"evidence[{index}].kind must be one of {', '.join(EVIDENCE_WEIGHTS)}" ) stages = validate_dimension_list( raw.get("stages"), f"evidence[{index}].stages", STAGES ) functions = validate_dimension_list( raw.get("functions"), f"evidence[{index}].functions", FUNCTIONS ) if not stages and not functions: raise ProfileError( f"evidence[{index}] must tag at least one stage or function" ) note = require_string( raw.get("private_note"), f"evidence[{index}].private_note", maximum=1000 ) share = raw.get("share") is True private_items.append( { "id": evidence_id, "kind": kind, "weight": EVIDENCE_WEIGHTS[kind], "stages": stages, "functions": functions, "private_note": note, "share": share, } ) if share: claim = validate_public_text( require_string( raw.get("public_claim"), f"evidence[{index}].public_claim", maximum=180, ), f"evidence[{index}].public_claim", ) proof = validate_url( raw.get("public_proof"), f"evidence[{index}].public_proof" ) public_items.append( { "id": evidence_id, "claim": claim, "proof": proof, "supports": stages + functions, } ) return private_items, public_items def compute_vectors(evidence: list[dict], dimensions: tuple[str, ...]) -> dict: raw_scores: defaultdict[str, int] = defaultdict(int) counts: defaultdict[str, int] = defaultdict(int) strong_counts: defaultdict[str, int] = defaultdict(int) ids: defaultdict[str, list[str]] = defaultdict(list) dimension_key = "stages" if dimensions == STAGES else "functions" for item in evidence: if item["kind"] == "preference": continue for dimension in item[dimension_key]: raw_scores[dimension] += item["weight"] counts[dimension] += 1 ids[dimension].append(item["id"]) if item["kind"] in HIGH_QUALITY_KINDS: strong_counts[dimension] += 1 output: dict[str, dict] = {} for dimension in dimensions: score = min(100, round(raw_scores[dimension] / 15 * 100)) output[dimension] = { "score": score, "level": score_label(score), "confidence": confidence_label(counts[dimension], strong_counts[dimension]), "evidence_count": counts[dimension], "evidence_ids": ids[dimension], } return output def validate_preferences(value: object) -> dict: if value is None: value = {} if not isinstance(value, dict): raise ProfileError("preferences must be an object") return { "stages": validate_dimension_list( value.get("stages"), "preferences.stages", STAGES ), "functions": validate_dimension_list( value.get("functions"), "preferences.functions", FUNCTIONS ), } def validate_seeking(value: object) -> dict: if not isinstance(value, dict): raise ProfileError("seeking must be an object") stages = validate_dimension_list(value.get("stages"), "seeking.stages", STAGES) functions = validate_dimension_list( value.get("functions"), "seeking.functions", FUNCTIONS ) if not stages and not functions: raise ProfileError("seeking must name at least one stage or function") return { "stages": stages, "functions": functions, "project_themes": validate_string_list( value.get("project_themes"), "seeking.project_themes" ), "collaboration_modes": validate_string_list( value.get("collaboration_modes"), "seeking.collaboration_modes", limit=5 ), "shared_principles": validate_string_list( value.get("shared_principles"), "seeking.shared_principles" ), } def validate_contact(value: object) -> dict: if not isinstance(value, dict): raise ProfileError("public_contact must be an object") contact_type = value.get("type") if contact_type not in {"github_issues", "github_discussions"}: raise ProfileError( "public_contact.type must be github_issues or github_discussions" ) return { "type": contact_type, "url": validate_url(value.get("url"), "public_contact.url", contact=True), } def public_vectors(vectors: dict) -> dict: return { name: { "score": values["score"], "level": values["level"], "confidence": values["confidence"], "evidence_count": values["evidence_count"], } for name, values in vectors.items() } def write_json(path: Path, data: dict, *, private: bool) -> None: if private and not path.name.endswith(".private.json"): raise ProfileError("private output filename must end in .private.json") path.parent.mkdir(parents=True, exist_ok=True) if path.is_symlink(): raise ProfileError(f"Refusing to write through symlink: {path}") flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, "O_NOFOLLOW", 0) fd = os.open(path, flags, 0o600 if private else 0o644) with os.fdopen(fd, "w", encoding="utf-8") as handle: os.fchmod(handle.fileno(), 0o600 if private else 0o644) json.dump(data, handle, indent=2, ensure_ascii=False, sort_keys=True) handle.write("\n") def build_assessment_components(data: dict) -> dict: alias = validate_alias(data.get("alias")) summary = validate_public_text( require_string(data.get("summary"), "summary", maximum=280), "summary" ) evidence, public_evidence = validate_evidence(data.get("evidence")) preferences = validate_preferences(data.get("preferences")) seeking = validate_seeking(data.get("seeking")) stage_vectors = compute_vectors(evidence, STAGES) function_vectors = compute_vectors(evidence, FUNCTIONS) generated_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat() return { "alias": alias, "summary": summary, "evidence": evidence, "public_evidence": public_evidence, "preferences": preferences, "seeking": seeking, "stage_vectors": stage_vectors, "function_vectors": function_vectors, "generated_at": generated_at, } def private_assessment_from_components(components: dict) -> dict: return { "schema_version": SCHEMA_VERSION, "alias": components["alias"], "generated_at": components["generated_at"], "evidence": components["evidence"], "stage_contributions": components["stage_vectors"], "functional_contributions": components["function_vectors"], "preferences": components["preferences"], "seeking": components["seeking"], "publication_state": "private_draft_only", } def build_private_assessment(data: dict) -> dict: """Build a private draft without requiring any publication consent.""" return private_assessment_from_components(build_assessment_components(data)) def build_profiles(data: dict) -> tuple[dict, dict]: components = build_assessment_components(data) contact = validate_contact(data.get("public_contact")) consent = validate_consent(data.get("consent")) public_profile = { "schema_version": SCHEMA_VERSION, "profile_type": "founder-collaboration", "alias": components["alias"], "summary": components["summary"], "generated_at": components["generated_at"], "expires_on": consent["expires_on"], "stage_contributions": public_vectors(components["stage_vectors"]), "functional_contributions": public_vectors(components["function_vectors"]), "preferences": components["preferences"], "seeking": components["seeking"], "public_evidence": components["public_evidence"], "contact": contact, "consent": consent, "interpretation": { "status": "owner-approved collaboration hypothesis", "not_for": [ "employment screening", "psychometric diagnosis", "sensitive-trait inference", ], }, } private_assessment = private_assessment_from_components(components) private_assessment["publication_state"] = "public_profile_approved" private_assessment["public_profile_preview"] = public_profile return public_profile, private_assessment def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Score owner-selected evidence and produce a safe public profile." ) parser.add_argument("input", type=Path, help="Private owner input JSON") parser.add_argument( "--public-output", type=Path, help="Write the public profile JSON here" ) parser.add_argument( "--private-output", type=Path, help="Optional private assessment; filename must end in .private.json", ) return parser.parse_args() def main() -> int: args = parse_args() try: data = load_json(args.input) if args.private_output and not args.public_output: private_assessment = build_private_assessment(data) write_json(args.private_output, private_assessment, private=True) return 0 public_profile, private_assessment = build_profiles(data) if args.public_output: write_json(args.public_output, public_profile, private=False) if args.private_output: write_json(args.private_output, private_assessment, private=True) if not args.public_output: json.dump( public_profile, sys.stdout, indent=2, ensure_ascii=False, sort_keys=True, ) sys.stdout.write("\n") except ProfileError as exc: print(f"error: {exc}", file=sys.stderr) return 2 return 0 if __name__ == "__main__": raise SystemExit(main())