Skill Tracer
Skills › Scientific Agent Skills › scientific-brainstorming › session_scaffold.py

session_scaffold.py

248 lines · skills/scientific-brainstorming/scripts/session_scaffold.py at 5f2bc03

1"""Generate a deterministic JSON scaffold for a brainstorming session."""
2
3from __future__ import annotations
4
5import argparse
6import sys
7from collections.abc import Sequence
8from typing import Any
9
10from _common import (
11 CliError,
12 bounded_strings,
13 emit_json,
14 require_identifier,
15 require_iso_date,
16 require_text,
19WORKFLOW_STAGES = [
20 "scope",
21 "independent-generation",
22 "structured-sharing",
23 "clustering",
24 "criteria-definition",
25 "independent-evaluation",
26 "adversarial-review",
27 "literature-check",
28 "feasibility-and-ethics-gate",
29 "decision-log",
33def build_scaffold(
34 *,
35 session_id: str,
36 title: str,
37 question: str,
38 participant_ids: Sequence[str],
39 session_date: str | None = None,
40 in_scope: Sequence[str] = (),
41 out_of_scope: Sequence[str] = (),
42 constraints: Sequence[str] = (),
43 perspectives: Sequence[str] = (),
44) -> dict[str, Any]:
45 """Build a reproducible, empty session register from explicit inputs."""
47 clean_session_id = require_identifier(session_id, "session ID")
48 clean_title = require_text(title, "title", maximum=200)
49 clean_question = require_text(question, "question", maximum=2_000)
50 participants = [
51 require_identifier(value, f"participant ID {index + 1}")
52 for index, value in enumerate(participant_ids)
53 ]
54 if not participants:
55 raise CliError("at least one participant ID is required")
56 if len(participants) > 100:
57 raise CliError("participant count exceeds the limit of 100")
58 if len(set(participants)) != len(participants):
59 raise CliError("participant IDs must be unique")
61 clean_date = (
62 require_iso_date(session_date, "session date")
63 if session_date is not None
64 else None
65 )
66 clean_in_scope = bounded_strings(in_scope, "in-scope items")
67 clean_out_of_scope = bounded_strings(out_of_scope, "out-of-scope items")
68 clean_constraints = bounded_strings(constraints, "constraints")
69 clean_perspectives = bounded_strings(perspectives, "perspectives")
71 return {
72 "schema_version": "1.1",
73 "session": {
74 "id": clean_session_id,
75 "title": clean_title,
76 "question": clean_question,
77 "date": clean_date,
78 "decision_owner_id": None,
79 "facilitator_id": None,
80 "participant_ids": participants,
81 "represented_perspectives": clean_perspectives,
82 "missing_perspectives": [],
83 "conflicts_or_power_dynamics": [],
84 },
85 "scope": {
86 "in_scope": clean_in_scope,
87 "out_of_scope": clean_out_of_scope,
88 "constraints": [
89 {
90 "statement": statement,
91 "classification": "unclassified",
92 }
93 for statement in clean_constraints
94 ],
95 "prohibited_outputs": [],
96 },
97 "information_governance": {
98 "classification": "not-assessed",
99 "approved_record_location": None,
100 "contains_personal_or_sensitive_data": False,
101 "contains_unpublished_or_proprietary_data": False,
102 "contains_controlled_or_security_sensitive_data": False,
103 "external_ai_use_permitted": "not-assessed",
104 },
105 "workflow": [
106 {
107 "order": index + 1,
108 "stage": stage,
109 "status": "pending",
110 "method_or_deviation": None,
111 }
112 for index, stage in enumerate(WORKFLOW_STAGES)
113 ],
114 "ideas": [],
115 "assumptions": [],
116 "clusters": [],
117 "criteria": [],
118 "adversarial_reviews": [],
119 "literature_checks": [],
120 "feasibility_and_ethics_reviews": [],
121 "decision_log": [],
122 "templates": {
123 "idea": {
124 "id": "I001",
125 "statement": "Replace with a concise candidate direction.",
126 "provenance": {
127 "origin": "human",
128 "contributor_ids": [participants[0]],
129 "recorded_stage": "independent",
130 "source_refs": [],
131 "ai_tool": None,
132 },
133 "assumption_ids": ["A001"],
134 "predicted_observations": [],
135 "uncertainties": [],
136 "evidence_status": "not-checked",
137 "status": "candidate",
138 },
139 "assumption": {
140 "id": "A001",
141 "statement": "Replace with one explicit assumption.",
142 "category": "measurement",
143 "status": "untested",
144 "test_or_check": "Describe how this could be checked.",
145 "owner_id": participants[0],
146 "evidence_refs": [],
147 },
148 },
149 "notices": [
150 "Ideas and scores are not evidence or scientific conclusions.",
151 (
152 "Clinical, ethics, biosafety, dual-use, regulatory, and "
153 "institutional review remain separate."
154 ),
155 (
156 "Do not store sensitive prompt content or restricted information "
157 "in this register."
158 ),
159 ],
160 }
163def build_parser() -> argparse.ArgumentParser:
164 """Create the command-line parser."""
166 parser = argparse.ArgumentParser(
167 description=(
168 "Generate a deterministic JSON session scaffold for evidence-aware "
169 "scientific brainstorming. No network or LLM calls are made."
170 )
171 )
172 parser.add_argument("--session-id", required=True, help="Stable session ID")
173 parser.add_argument("--title", required=True, help="Session title")
174 parser.add_argument("--question", required=True, help="One focal question")
175 parser.add_argument(
176 "--participant",
177 action="append",
178 required=True,
179 dest="participants",
180 help="Pseudonymous participant ID; repeat for multiple participants",
181 )
182 parser.add_argument(
183 "--date",
184 dest="session_date",
185 help="Optional explicit ISO date (YYYY-MM-DD); no date is invented",
186 )
187 parser.add_argument(
188 "--in-scope",
189 action="append",
190 default=[],
191 help="In-scope item; repeat as needed",
192 )
193 parser.add_argument(
194 "--out-of-scope",
195 action="append",
196 default=[],
197 help="Out-of-scope item; repeat as needed",
198 )
199 parser.add_argument(
200 "--constraint",
201 action="append",
202 default=[],
203 help="Constraint to classify later; repeat as needed",
204 )
205 parser.add_argument(
206 "--perspective",
207 action="append",
208 default=[],
209 help="Represented perspective; repeat as needed",
210 )
211 parser.add_argument(
212 "--output",
213 required=True,
214 help="Destination .json file in an existing directory",
215 )
216 parser.add_argument(
217 "--force",
218 action="store_true",
219 help="Explicitly replace an existing regular output file",
220 )
221 return parser
224def main(argv: Sequence[str] | None = None) -> int:
225 """Run the scaffold generator."""
227 args = build_parser().parse_args(argv)
228 try:
229 payload = build_scaffold(
230 session_id=args.session_id,
231 title=args.title,
232 question=args.question,
233 participant_ids=args.participants,
234 session_date=args.session_date,
235 in_scope=args.in_scope,
236 out_of_scope=args.out_of_scope,
237 constraints=args.constraint,
238 perspectives=args.perspective,
239 )
240 emit_json(payload, args.output, force=args.force)
241 except CliError as exc:
242 print(f"error: {exc}", file=sys.stderr)
243 return 2
244 return 0
247if __name__ == "__main__":
248 raise SystemExit(main())