← Files MightShapeARCHIVED FILE
skills/mightshape/scripts/create_study.py
6.55 KB · Oct 3, 2026 · 06:31 UTC
#!/usr/bin/env python3
"""Create a consent-forward Inquiry Lab study record."""
from __future__ import annotations
import argparse
import json
import secrets
import sys
from typing import Any
from dc_core import DesignCouncilError, json_output, load_json, now_utc, schema_validation
STUDY_TYPES = {"SYNTHETIC", "HUMAN", "MIXED", "ANALOGOUS", "REALITY_CHECK"}
PII_TERMS = {"name", "email", "phone", "address", "date of birth", "dob", "social security", "ssn"}
def create_study(spec: dict[str, Any], issue_public_token: bool = False) -> dict[str, Any]:
if not isinstance(spec, dict):
raise DesignCouncilError("study specification must be an object")
title = str(spec.get("title", "")).strip()
goal = str(spec.get("research_goal", "")).strip()
topics = spec.get("topics_to_cover", [])
if not title or not goal or not isinstance(topics, list) or not topics:
raise DesignCouncilError("title, research_goal, and at least one topics_to_cover item are required")
study_type = str(spec.get("study_type", "HUMAN")).upper()
if study_type not in STUDY_TYPES:
raise DesignCouncilError(f"study_type must be one of {sorted(STUDY_TYPES)}")
duration = int(spec.get("duration_minutes", 10))
if not 1 <= duration <= 180:
raise DesignCouncilError("duration_minutes must be between 1 and 180")
solution_blackout = bool(spec.get("solution_blackout", True))
privacy = dict(spec.get("privacy_configuration", {}))
privacy.setdefault("participant_identifiers", "P-### only")
privacy.setdefault("collect_names", False)
privacy.setdefault("collect_emails", False)
privacy.setdefault("minimize_pii", True)
privacy.setdefault("deletion_path", "Participant may stop; researcher can delete transcript by participant ID")
data_collected = spec.get("data_collected", ["consent status", "anonymous participant ID", "text transcript", "interview state"])
if not isinstance(data_collected, list):
raise DesignCouncilError("data_collected must be an array")
consent_spec = dict(spec.get("consent", {}))
consent = {
"version": str(consent_spec.get("version", "1.0")),
"ai_disclosure": str(consent_spec.get("ai_disclosure", "The interviewer is AI, not a human researcher.")),
"purpose": str(consent_spec.get("purpose", goal)),
"duration_minutes": duration,
"data_collected": [str(item) for item in data_collected],
"reviewers": str(consent_spec.get("reviewers", spec.get("reviewers", "The named design research team"))),
"deidentified_quotes": bool(consent_spec.get("deidentified_quotes", False)),
"may_stop": True,
"retention": str(consent_spec.get("retention", spec.get("retention", "Defined by the study owner before activation"))),
"contact": str(consent_spec.get("contact", spec.get("contact", "Study owner contact shown on the participant page"))),
}
if "ai" not in consent["ai_disclosure"].lower():
raise DesignCouncilError("consent.ai_disclosure must explicitly say the interviewer is AI")
timestamp = now_utc()
public_token = spec.get("public_token")
if issue_public_token and not public_token:
public_token = secrets.token_urlsafe(24)
study = {
"id": str(spec.get("id", "STUDY-001")),
"title": title,
"study_type": study_type,
"research_goal": goal,
"topics_to_cover": [str(item) for item in topics],
"covered_topics": [],
"emerging_threads": [],
"adaptive_follow_up_priorities": [str(item) for item in spec.get("adaptive_follow_up_priorities", topics[:2])],
"solution_blackout": solution_blackout,
"concept_reveal": dict(spec.get("concept_reveal", {"enabled": False, "condition": "After behavior reconstruction and only when methodologically justified"})),
"stop_conditions": [str(item) for item in spec.get("stop_conditions", ["participant asks to stop", "participant withdraws", "topics covered and no productive thread remains", f"approximately {duration} minutes elapsed"])],
"privacy_configuration": privacy,
"consent": consent,
"participant_id_prefix": "P-",
"public_token": public_token,
"status": str(spec.get("status", "draft")).lower(),
"created_at": str(spec.get("created_at", timestamp)),
"updated_at": str(spec.get("updated_at", timestamp)),
}
validation = schema_validation(study, "inquiry-study.schema.json")
if not validation["valid"]:
raise DesignCouncilError("Study failed validation: " + "; ".join(validation["errors"]))
return study
def study_warnings(study: dict[str, Any]) -> list[str]:
warnings = []
collected = " ".join(study["consent"]["data_collected"]).lower()
unnecessary = sorted(term for term in PII_TERMS if term in collected)
if unnecessary:
warnings.append("Study declares potential PII collection; justify and minimize: " + ", ".join(unnecessary))
if study["privacy_configuration"].get("collect_names") or study["privacy_configuration"].get("collect_emails"):
warnings.append("Direct identifiers are enabled; document why anonymous P-### identifiers are insufficient")
if study["status"] in {"ready", "active"} and "defined by" in study["consent"]["retention"].lower():
warnings.append("Set a concrete retention period before activating the study")
if study["public_token"] and study["status"] == "draft":
warnings.append("A bearer token exists for a draft; do not present it as a deployed interview link")
if study["status"] in {"ready", "active"} and "study owner contact" in study["consent"]["contact"].lower():
warnings.append("Set a real participant contact before activating the study")
return warnings
def main() -> int:
parser = argparse.ArgumentParser(description="Create an Inquiry Lab study")
parser.add_argument("input", nargs="?", help="Study specification JSON; stdin when omitted")
parser.add_argument("--issue-public-token", action="store_true", help="Create an opaque bearer token, not a deployment URL")
args = parser.parse_args()
try:
spec = load_json(args.input) if args.input else json.load(sys.stdin)
study = create_study(spec, args.issue_public_token)
json_output({"study": study, "warnings": study_warnings(study), "deployment_url": None, "note": "A token is not a hosted link; publish the optional Site explicitly."})
except (DesignCouncilError, json.JSONDecodeError, ValueError) as exc:
print(f"MightShape error: {exc}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 5ebeb49526bb2ba07753c99561f8894f7716a29acb60967de36c2af732272b98