jupytermind 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/.github/skills/ai-chemistry-scientist/SKILL.md +97 -0
  2. package/.github/skills/ai-chemistry-scientist/manifest.json +156 -0
  3. package/.github/skills/ai-data-scientist/SKILL.md +330 -0
  4. package/.github/skills/ai-genomics-scientist/SKILL.md +98 -0
  5. package/.github/skills/ai-genomics-scientist/manifest.json +93 -0
  6. package/.github/skills/ai-materials-scientist/SKILL.md +51 -0
  7. package/.github/skills/ai-materials-scientist/manifest.json +58 -0
  8. package/.github/skills/ai-scientist/SKILL.md +69 -0
  9. package/.github/skills/ai-scientist/manifest.json +61 -0
  10. package/.github/skills/ai-structural-biology-scientist/SKILL.md +67 -0
  11. package/.github/skills/ai-structural-biology-scientist/manifest.json +72 -0
  12. package/.github/skills/japanese-prose/NOTICE.md +17 -0
  13. package/.github/skills/japanese-prose/SKILL.md +111 -0
  14. package/.github/skills/japanese-prose/references/review-workflow.md +50 -0
  15. package/.github/skills/japanese-prose/references/scoring.md +24 -0
  16. package/.github/skills/japanese-prose/references/writing-guidelines.md +60 -0
  17. package/.github/skills/japanese-prose/scripts/core.py +192 -0
  18. package/.github/skills/japanese-prose/scripts/fixtures/natural.md +5 -0
  19. package/.github/skills/japanese-prose/scripts/fixtures/unnatural.md +5 -0
  20. package/.github/skills/japanese-prose/scripts/lint.py +378 -0
  21. package/.github/skills/japanese-prose/scripts/outline.py +68 -0
  22. package/.github/skills/japanese-prose/scripts/terms.py +112 -0
  23. package/.github/skills/japanese-prose/scripts/test_engine.py +117 -0
  24. package/.github/skills/presentation-planner/SKILL.md +257 -0
  25. package/.github/skills/presentation-planner/assets/design-templates/data-report.yaml +97 -0
  26. package/.github/skills/presentation-planner/assets/design-templates/executive-proposal.yaml +92 -0
  27. package/.github/skills/presentation-planner/assets/design-templates/technical-briefing.yaml +96 -0
  28. package/.github/skills/presentation-planner/assets/scenario-templates/data-report.md +47 -0
  29. package/.github/skills/presentation-planner/assets/scenario-templates/executive-decision.md +43 -0
  30. package/.github/skills/presentation-planner/assets/scenario-templates/technical-briefing.md +45 -0
  31. package/.github/skills/presentation-planner/references/customizing-design-templates.md +160 -0
  32. package/.github/skills/presentation-planner/references/design-spec-schema.md +72 -0
  33. package/.github/skills/presentation-planner/references/handoff-contract.md +49 -0
  34. package/.github/skills/presentation-planner/references/responsibility-boundary.md +32 -0
  35. package/.github/skills/presentation-planner/references/scenario-templates.md +55 -0
  36. package/.github/skills/tech-writer/SKILL.md +434 -0
  37. package/.github/skills/tech-writer/assets/templates/blueprint.md +187 -0
  38. package/.github/skills/tech-writer/assets/templates/design-doc.md +29 -0
  39. package/.github/skills/tech-writer/assets/templates/migration-plan.md +173 -0
  40. package/.github/skills/tech-writer/assets/templates/operations-runbook.md +202 -0
  41. package/.github/skills/tech-writer/assets/templates/pr-description.md +23 -0
  42. package/.github/skills/tech-writer/assets/templates/qiita.md +44 -0
  43. package/.github/skills/tech-writer/assets/templates/readme.md +38 -0
  44. package/.github/skills/tech-writer/assets/templates/requirements-definition.md +170 -0
  45. package/.github/skills/tech-writer/assets/templates/rfi.md +113 -0
  46. package/.github/skills/tech-writer/assets/templates/rfp.md +180 -0
  47. package/.github/skills/tech-writer/assets/templates/security-design.md +167 -0
  48. package/.github/skills/tech-writer/assets/templates/system-design.md +220 -0
  49. package/.github/skills/tech-writer/assets/templates/technical-proposal.md +112 -0
  50. package/.github/skills/tech-writer/assets/templates/test-plan.md +153 -0
  51. package/.github/skills/tech-writer/assets/templates/user-manual.md +22 -0
  52. package/.github/skills/tech-writer/assets/templates/white-paper.md +192 -0
  53. package/.github/skills/tech-writer/references/doctypes/api-docs.md +33 -0
  54. package/.github/skills/tech-writer/references/doctypes/blueprint.md +81 -0
  55. package/.github/skills/tech-writer/references/doctypes/code-comments.md +39 -0
  56. package/.github/skills/tech-writer/references/doctypes/design-doc.md +42 -0
  57. package/.github/skills/tech-writer/references/doctypes/migration-plan.md +63 -0
  58. package/.github/skills/tech-writer/references/doctypes/operations-runbook.md +63 -0
  59. package/.github/skills/tech-writer/references/doctypes/pr-commit.md +82 -0
  60. package/.github/skills/tech-writer/references/doctypes/qiita.md +75 -0
  61. package/.github/skills/tech-writer/references/doctypes/readme.md +43 -0
  62. package/.github/skills/tech-writer/references/doctypes/release-notes.md +30 -0
  63. package/.github/skills/tech-writer/references/doctypes/requirements-definition.md +61 -0
  64. package/.github/skills/tech-writer/references/doctypes/rfi.md +43 -0
  65. package/.github/skills/tech-writer/references/doctypes/rfp.md +46 -0
  66. package/.github/skills/tech-writer/references/doctypes/security-design.md +71 -0
  67. package/.github/skills/tech-writer/references/doctypes/system-design.md +74 -0
  68. package/.github/skills/tech-writer/references/doctypes/technical-proposal.md +49 -0
  69. package/.github/skills/tech-writer/references/doctypes/test-plan.md +67 -0
  70. package/.github/skills/tech-writer/references/doctypes/user-manual.md +58 -0
  71. package/.github/skills/tech-writer/references/doctypes/white-paper.md +84 -0
  72. package/.github/skills/tech-writer/references/doctypes/zenn.md +66 -0
  73. package/.github/skills/tech-writer/references/japanese-prose-optimization.md +110 -0
  74. package/.github/skills/tech-writer/references/style-constitution.md +104 -0
  75. package/.github/skills/tech-writer/scripts/lint.py +412 -0
  76. package/LICENSE +21 -0
  77. package/README.md +92 -0
  78. package/bin/ai-data-scientist.js +123 -0
  79. package/package.json +41 -0
  80. package/pyproject.toml +45 -0
  81. package/src/ai_chemistry_scientist/__init__.py +0 -0
  82. package/src/ai_chemistry_scientist/admet_prediction.py +71 -0
  83. package/src/ai_chemistry_scientist/bioactivity_classification.py +73 -0
  84. package/src/ai_chemistry_scientist/data/sample_molecules.csv +21 -0
  85. package/src/ai_chemistry_scientist/dispatch.py +369 -0
  86. package/src/ai_chemistry_scientist/docking_score.py +97 -0
  87. package/src/ai_chemistry_scientist/drug_likeness_rules.py +84 -0
  88. package/src/ai_chemistry_scientist/evidence.py +41 -0
  89. package/src/ai_chemistry_scientist/molecular_descriptors.py +97 -0
  90. package/src/ai_chemistry_scientist/molecular_formula_mass.py +40 -0
  91. package/src/ai_chemistry_scientist/molecular_similarity.py +78 -0
  92. package/src/ai_chemistry_scientist/qsar_modeling.py +105 -0
  93. package/src/ai_chemistry_scientist/salt_standardization.py +81 -0
  94. package/src/ai_chemistry_scientist/structural_alerts.py +76 -0
  95. package/src/ai_chemistry_scientist/structure_format_conversion.py +84 -0
  96. package/src/ai_chemistry_scientist/validation.py +70 -0
  97. package/src/ai_data_scientist/__init__.py +0 -0
  98. package/src/ai_data_scientist/analysis_assumptions.py +121 -0
  99. package/src/ai_data_scientist/anomaly_detection.py +39 -0
  100. package/src/ai_data_scientist/automl.py +109 -0
  101. package/src/ai_data_scientist/cleaning.py +56 -0
  102. package/src/ai_data_scientist/cli.py +90 -0
  103. package/src/ai_data_scientist/clustering.py +54 -0
  104. package/src/ai_data_scientist/dashboard.py +33 -0
  105. package/src/ai_data_scientist/data_definition.py +100 -0
  106. package/src/ai_data_scientist/data_quality.py +164 -0
  107. package/src/ai_data_scientist/dataset_validation.py +135 -0
  108. package/src/ai_data_scientist/dependency_pins.py +60 -0
  109. package/src/ai_data_scientist/eda.py +82 -0
  110. package/src/ai_data_scientist/experiment_evaluation.py +635 -0
  111. package/src/ai_data_scientist/explainability.py +340 -0
  112. package/src/ai_data_scientist/feature_engineering.py +163 -0
  113. package/src/ai_data_scientist/gate_config.py +32 -0
  114. package/src/ai_data_scientist/ingestion.py +127 -0
  115. package/src/ai_data_scientist/insight_engine.py +180 -0
  116. package/src/ai_data_scientist/japanese_nlp.py +43 -0
  117. package/src/ai_data_scientist/jupyter_launcher.py +137 -0
  118. package/src/ai_data_scientist/jupyter_mcp_client.py +94 -0
  119. package/src/ai_data_scientist/language_router.py +28 -0
  120. package/src/ai_data_scientist/lifecycle.py +221 -0
  121. package/src/ai_data_scientist/mcp_gateway.py +113 -0
  122. package/src/ai_data_scientist/mcp_runtime.py +194 -0
  123. package/src/ai_data_scientist/mcp_transport.py +53 -0
  124. package/src/ai_data_scientist/ml_modeling.py +451 -0
  125. package/src/ai_data_scientist/model_tuning.py +104 -0
  126. package/src/ai_data_scientist/notebook_audit.py +574 -0
  127. package/src/ai_data_scientist/project_manager.py +243 -0
  128. package/src/ai_data_scientist/report_export.py +73 -0
  129. package/src/ai_data_scientist/sensitivity.py +445 -0
  130. package/src/ai_data_scientist/signal_analysis.py +201 -0
  131. package/src/ai_data_scientist/skill_packaging.py +40 -0
  132. package/src/ai_data_scientist/stats_analysis.py +88 -0
  133. package/src/ai_data_scientist/text_nlp.py +44 -0
  134. package/src/ai_data_scientist/timeseries.py +68 -0
  135. package/src/ai_data_scientist/visualization.py +708 -0
  136. package/src/ai_genomics_scientist/__init__.py +1 -0
  137. package/src/ai_genomics_scientist/differential_expression.py +147 -0
  138. package/src/ai_genomics_scientist/dispatch.py +267 -0
  139. package/src/ai_genomics_scientist/evidence.py +45 -0
  140. package/src/ai_genomics_scientist/gene_set_enrichment.py +76 -0
  141. package/src/ai_genomics_scientist/sequence_alignment.py +97 -0
  142. package/src/ai_genomics_scientist/sequence_features.py +111 -0
  143. package/src/ai_genomics_scientist/splice_site_scoring.py +66 -0
  144. package/src/ai_genomics_scientist/validation.py +83 -0
  145. package/src/ai_genomics_scientist/variant_effect.py +147 -0
  146. package/src/ai_genomics_scientist/variant_pathogenicity.py +125 -0
  147. package/src/ai_materials_scientist/__init__.py +0 -0
  148. package/src/ai_materials_scientist/calphad.py +117 -0
  149. package/src/ai_materials_scientist/classical_monte_carlo.py +165 -0
  150. package/src/ai_materials_scientist/crystal_plasticity.py +184 -0
  151. package/src/ai_materials_scientist/dispatch.py +100 -0
  152. package/src/ai_materials_scientist/evidence.py +84 -0
  153. package/src/ai_materials_scientist/fem.py +279 -0
  154. package/src/ai_materials_scientist/kinetic_monte_carlo.py +145 -0
  155. package/src/ai_materials_scientist/molecular_dynamics.py +240 -0
  156. package/src/ai_materials_scientist/phase_field.py +167 -0
  157. package/src/ai_materials_scientist/validation.py +70 -0
  158. package/src/ai_scientist/__init__.py +1 -0
  159. package/src/ai_scientist/completion_gate.py +15 -0
  160. package/src/ai_scientist/data_analysis.py +46 -0
  161. package/src/ai_scientist/evidence_registry.py +99 -0
  162. package/src/ai_scientist/experimental_design.py +20 -0
  163. package/src/ai_scientist/language.py +14 -0
  164. package/src/ai_scientist/latex_renderer.py +41 -0
  165. package/src/ai_scientist/literature_review.py +37 -0
  166. package/src/ai_scientist/manifest.py +87 -0
  167. package/src/ai_scientist/manuscript.py +94 -0
  168. package/src/ai_scientist/mcp_config.py +76 -0
  169. package/src/ai_scientist/mcp_external.py +42 -0
  170. package/src/ai_scientist/mcp_failures.py +23 -0
  171. package/src/ai_scientist/mcp_gateway.py +38 -0
  172. package/src/ai_scientist/mcp_managed.py +180 -0
  173. package/src/ai_scientist/npm_packaging.py +49 -0
  174. package/src/ai_scientist/orchestrator.py +133 -0
  175. package/src/ai_scientist/peer_review.py +60 -0
  176. package/src/ai_scientist/phase_gate.py +74 -0
  177. package/src/ai_scientist/phase_state.py +230 -0
  178. package/src/ai_scientist/presentation.py +56 -0
  179. package/src/ai_scientist/project_config.py +31 -0
  180. package/src/ai_scientist/project_handle.py +74 -0
  181. package/src/ai_scientist/reproducibility.py +20 -0
  182. package/src/ai_scientist/research_planning.py +20 -0
  183. package/src/ai_scientist/skill_invocation.py +21 -0
  184. package/src/ai_scientist/tdd_gate.py +99 -0
  185. package/src/ai_structural_biology_scientist/__init__.py +0 -0
  186. package/src/ai_structural_biology_scientist/contact_map.py +87 -0
  187. package/src/ai_structural_biology_scientist/dispatch.py +269 -0
  188. package/src/ai_structural_biology_scientist/evidence.py +43 -0
  189. package/src/ai_structural_biology_scientist/hydrophobicity.py +101 -0
  190. package/src/ai_structural_biology_scientist/protein_docking_score.py +104 -0
  191. package/src/ai_structural_biology_scientist/secondary_structure.py +95 -0
  192. package/src/ai_structural_biology_scientist/structural_similarity.py +74 -0
  193. package/src/ai_structural_biology_scientist/validation.py +100 -0
@@ -0,0 +1,243 @@
1
+ """Project resolution and notebook lifecycle management.
2
+
3
+ Implements DES-AIDS-003: project identifier validation (ADR-0005),
4
+ notebook creation/reuse, and a single-writer queue that serializes
5
+ concurrent notebook writes (ADR-0004).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import re
12
+ import tempfile
13
+ import threading
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+
17
+ import nbformat
18
+
19
+ _SLUG_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
20
+
21
+ # Captured once, at import time, before any skill code can os.chdir() into a
22
+ # notebook/dataset directory. This anchors resolve_project's default
23
+ # projects_root to a stable location (REQ-AIDS-044 / DES-AIDS-032), instead
24
+ # of re-resolving "projects" relative to whatever the cwd happens to be at
25
+ # call time (which previously created nested
26
+ # projects/<slug>/notebooks/projects/<slug> paths when the kernel cwd drifted
27
+ # into a project's own notebooks directory).
28
+ _IMPORT_TIME_CWD = Path.cwd()
29
+
30
+ _PROJECTS_ROOT_ENV_VAR = "AI_DATA_SCIENTIST_PROJECTS_ROOT"
31
+
32
+
33
+ def _default_projects_root() -> Path:
34
+ """Resolve the stable default projects root.
35
+
36
+ Prefers the ``AI_DATA_SCIENTIST_PROJECTS_ROOT`` environment variable when
37
+ set (for callers that want to pin an explicit workspace root); otherwise
38
+ falls back to ``<import-time cwd>/projects``, which stays constant for
39
+ the lifetime of the process regardless of later ``os.chdir`` calls.
40
+ """
41
+ env_root = os.environ.get(_PROJECTS_ROOT_ENV_VAR)
42
+ if env_root:
43
+ return Path(env_root).resolve()
44
+ return (_IMPORT_TIME_CWD / "projects").resolve()
45
+
46
+
47
+ def next_execution_count(notebook) -> int:
48
+ """Compute the next monotonically increasing execution_count for ``notebook``.
49
+
50
+ Shared by any writer that appends an "executed" code cell (mcp_gateway's
51
+ run_and_record, visualization's record_chart) so every such cell carries
52
+ a real, non-null, incrementing execution_count instead of leaving it
53
+ unset (REQ-AIDS-009's evidentiary-cell acceptance criterion).
54
+ """
55
+ existing = [
56
+ cell.get("execution_count") for cell in notebook.cells if cell.get("cell_type") == "code"
57
+ ]
58
+ return max((count for count in existing if isinstance(count, int)), default=0) + 1
59
+
60
+
61
+ class InvalidProjectNameError(ValueError):
62
+ """Raised when a project name does not satisfy the ADR-0005 slug policy."""
63
+
64
+
65
+ class StablePathResolutionError(FileNotFoundError):
66
+ """Raised when a path resolves under neither cwd nor the stable workspace root."""
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class ProjectHandle:
71
+ """Resolved, validated project identity and its notebook path."""
72
+
73
+ name: str
74
+ root: Path
75
+ notebook_path: Path
76
+ data_dir: Path
77
+
78
+
79
+ # A process-wide lock per notebook path, guaranteeing a single writer at a
80
+ # time regardless of how many callers invoke enqueue_write concurrently.
81
+ _write_locks: dict[Path, threading.Lock] = {}
82
+ _write_locks_guard = threading.Lock()
83
+
84
+
85
+ def _lock_for(path: Path) -> threading.Lock:
86
+ with _write_locks_guard:
87
+ lock = _write_locks.get(path)
88
+ if lock is None:
89
+ lock = threading.Lock()
90
+ _write_locks[path] = lock
91
+ return lock
92
+
93
+
94
+ # @id CODE-AIDS-028
95
+ # @implements REQ-AIDS-028
96
+ # @design DES-AIDS-003
97
+ # @id CODE-AIDS-052
98
+ # @implements REQ-AIDS-044
99
+ # @design DES-AIDS-032
100
+ def resolve_project(name: str, projects_root: Path | str | None = None) -> ProjectHandle:
101
+ """Validate ``name`` and resolve its on-disk project handle.
102
+
103
+ Rejects path traversal / non-slug names before any filesystem access,
104
+ per ADR-0005.
105
+
106
+ When ``projects_root`` is omitted, the default root is stable across
107
+ process working-directory changes: it honors the
108
+ ``AI_DATA_SCIENTIST_PROJECTS_ROOT`` environment variable when set, and
109
+ otherwise anchors to the directory this module was imported from, not
110
+ the caller's current working directory at call time (REQ-AIDS-044).
111
+ Passing an explicit ``projects_root`` is unchanged from before.
112
+ """
113
+ if not _SLUG_PATTERN.match(name):
114
+ raise InvalidProjectNameError(
115
+ f"'{name}' is not a valid project name. Allowed pattern: "
116
+ f"lowercase ASCII letters, digits and single hyphens, e.g. 'sales-2024' "
117
+ f"(プロジェクト名は小文字英数字とハイフンのみ使用できます: 例 'sales-2024')."
118
+ )
119
+ root = Path(projects_root).resolve() if projects_root is not None else _default_projects_root()
120
+ project_dir = (root / name).resolve()
121
+ if project_dir.parent != root:
122
+ # Defense in depth: even a slug-valid name must stay inside projects_root.
123
+ raise InvalidProjectNameError(f"'{name}' resolves outside the projects directory.")
124
+ notebook_path = project_dir / "notebooks" / f"{name}.ipynb"
125
+ data_dir = project_dir / "data"
126
+ return ProjectHandle(
127
+ name=name, root=project_dir, notebook_path=notebook_path, data_dir=data_dir
128
+ )
129
+
130
+
131
+ # @id CODE-AIDS-055
132
+ # @implements REQ-AIDS-049
133
+ # @design DES-AIDS-037
134
+ def ensure_data_dir(handle: ProjectHandle) -> Path:
135
+ """Create ``handle.data_dir`` (and any missing parents) if absent; return it."""
136
+ handle.data_dir.mkdir(parents=True, exist_ok=True)
137
+ return handle.data_dir
138
+
139
+
140
+ # @id CODE-AIDS-056
141
+ # @implements REQ-AIDS-047
142
+ # @design DES-AIDS-035
143
+ def resolve_stable_path(path: Path | str) -> Path:
144
+ """Resolve ``path`` against cwd, falling back to the stable workspace root.
145
+
146
+ Mirrors ``_default_projects_root``'s stability rule for any caller (such
147
+ as ``notebook_audit.audit_notebook``) that receives a relative path which
148
+ may have been computed against the workspace root but is later evaluated
149
+ from a kernel whose working directory has drifted into a project's own
150
+ notebook directory (REQ-AIDS-047).
151
+
152
+ An absolute ``path`` is returned resolved as-is (existence is left to the
153
+ caller, matching prior behavior). A relative ``path`` is first checked
154
+ against the current working directory; if that candidate does not exist,
155
+ it is re-checked against ``_IMPORT_TIME_CWD`` (the same stable base
156
+ ``_default_projects_root`` anchors to). If neither candidate exists,
157
+ ``StablePathResolutionError`` is raised naming both attempted locations.
158
+ """
159
+ candidate = Path(path)
160
+ if candidate.is_absolute():
161
+ return candidate.resolve()
162
+
163
+ cwd_candidate = (Path.cwd() / candidate).resolve()
164
+ if cwd_candidate.exists():
165
+ return cwd_candidate
166
+
167
+ stable_candidate = (_IMPORT_TIME_CWD / candidate).resolve()
168
+ if stable_candidate.exists():
169
+ return stable_candidate
170
+
171
+ raise StablePathResolutionError(
172
+ f"Could not resolve '{path}' relative to the current working directory "
173
+ f"({cwd_candidate}) or the stable workspace root ({stable_candidate})."
174
+ )
175
+
176
+
177
+ # @id CODE-AIDS-002
178
+ # @implements REQ-AIDS-002
179
+ # @design DES-AIDS-003
180
+ def ensure_notebook(handle: ProjectHandle) -> Path:
181
+ """Create the project notebook if missing; otherwise reuse it."""
182
+ handle.notebook_path.parent.mkdir(parents=True, exist_ok=True)
183
+ if not handle.notebook_path.exists():
184
+ notebook = nbformat.v4.new_notebook()
185
+ with handle.notebook_path.open("w", encoding="utf-8") as fh:
186
+ nbformat.write(notebook, fh)
187
+ return handle.notebook_path
188
+
189
+
190
+ # @id CODE-AIDS-011
191
+ # @implements REQ-AIDS-011
192
+ # @design DES-AIDS-003
193
+ # @id CODE-AIDS-029
194
+ # @implements REQ-AIDS-029 REQ-AIDS-059
195
+ # @design DES-AIDS-003
196
+ def enqueue_write(handle: ProjectHandle, cell_mutation) -> Path:
197
+ """Serialize a notebook mutation through a per-notebook single-writer lock.
198
+
199
+ ``cell_mutation`` is a callable receiving the loaded ``NotebookNode`` and
200
+ mutating it in place (e.g. appending a cell). The notebook is re-read and
201
+ re-written under the lock so every writer observes the latest on-disk
202
+ state, guaranteeing no cell from a concurrent writer is lost, and the
203
+ result always round-trips through ``nbformat.validate``.
204
+
205
+ The write itself is atomic (GitHub #27): the mutated notebook is
206
+ serialized to a string with ``nbformat.writes`` *before* anything on
207
+ disk is touched, so a mutation that ``nbformat.validate`` accepts but
208
+ that fails at JSON-serialization time (e.g. non-JSON-serializable
209
+ metadata) raises without ever truncating or corrupting the existing
210
+ file. The serialized string is then written to a temporary file in the
211
+ same directory, flushed and fsynced, and atomically swapped into place
212
+ with ``os.replace`` so a crash or error mid-write never leaves a
213
+ partially written notebook on disk.
214
+
215
+ **Concurrent-write risk with Jupyter MCP (GitHub #34, REQ-AIDS-059)**:
216
+ this lock only serializes concurrent callers of this function within
217
+ the current process; it does not coordinate with a separate Jupyter
218
+ MCP session that has the same notebook file open in memory. If such an
219
+ MCP session later saves its own in-memory copy, it can silently
220
+ overwrite whatever this function already wrote to disk. Prefer routing
221
+ writes through the active MCP session when one is open against this
222
+ notebook, or pause MCP-side saves while calling this function directly.
223
+ """
224
+ lock = _lock_for(handle.notebook_path)
225
+ with lock:
226
+ notebook = nbformat.read(handle.notebook_path, as_version=4)
227
+ cell_mutation(notebook)
228
+ nbformat.validate(notebook)
229
+ serialized = nbformat.writes(notebook)
230
+ directory = handle.notebook_path.parent
231
+ fd, tmp_name = tempfile.mkstemp(
232
+ dir=directory, prefix=f".{handle.notebook_path.name}.", suffix=".tmp"
233
+ )
234
+ try:
235
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
236
+ fh.write(serialized)
237
+ fh.flush()
238
+ os.fsync(fh.fileno())
239
+ os.replace(tmp_name, handle.notebook_path)
240
+ except BaseException:
241
+ Path(tmp_name).unlink(missing_ok=True)
242
+ raise
243
+ return handle.notebook_path
@@ -0,0 +1,73 @@
1
+ """Report export module.
2
+
3
+ Implements DES-AIDS-024 (REQ-AIDS-025, REQ-AIDS-033, ADR-0007): exports the
4
+ current project notebook to PDF, HTML, or slide form using a local
5
+ nbconvert-based conversion tool, without invoking any Jupyter MCP
6
+ execution call.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ import nbformat
15
+ from nbconvert import HTMLExporter, PDFExporter, SlidesExporter
16
+
17
+ from ai_data_scientist.project_manager import ProjectHandle
18
+
19
+ _SUPPORTED_FORMATS = ("html", "pdf", "slides")
20
+ _EXPORTERS = {"html": HTMLExporter, "pdf": PDFExporter, "slides": SlidesExporter}
21
+ _EXTENSIONS = {"html": "html", "pdf": "pdf", "slides": "slides.html"}
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class ReportPath:
26
+ path: Path
27
+ format: str
28
+
29
+
30
+ # @id CODE-AIDS-025
31
+ # @implements REQ-AIDS-025
32
+ # @design DES-AIDS-024
33
+ # @id CODE-AIDS-033
34
+ # @implements REQ-AIDS-033
35
+ # @design DES-AIDS-024
36
+ def export_report(
37
+ handle: ProjectHandle, report_format: str = "html", name: str | None = None
38
+ ) -> ReportPath:
39
+ """Export ``handle``'s notebook to ``report_format`` via nbconvert only.
40
+
41
+ Reads the already-executed notebook and renders it with a local
42
+ nbconvert exporter only; no Jupyter MCP client is invoked, satisfying
43
+ REQ-AIDS-033's read-only, non-MCP export boundary.
44
+ """
45
+ if report_format not in _SUPPORTED_FORMATS:
46
+ raise ValueError(f"Unsupported report format: {report_format!r}")
47
+
48
+ notebook = nbformat.read(handle.notebook_path, as_version=4)
49
+ exporter = _EXPORTERS[report_format]()
50
+ try:
51
+ body, _resources = exporter.from_notebook_node(notebook)
52
+ except OSError as exc:
53
+ if report_format == "pdf" and "xelatex" in str(exc).lower():
54
+ raise RuntimeError(
55
+ "PDF export requires a system TeX/xelatex installation, which "
56
+ "is not bundled with this package. See the 'PDF export "
57
+ "prerequisites' section of README.md for install instructions, "
58
+ "or use report_format='html' instead."
59
+ ) from exc
60
+ raise
61
+
62
+ reports_dir = handle.root / "reports"
63
+ reports_dir.mkdir(parents=True, exist_ok=True)
64
+ report_name = name or handle.name
65
+ extension = _EXTENSIONS[report_format]
66
+ report_path = reports_dir / f"{report_name}.{extension}"
67
+
68
+ mode = "wb" if isinstance(body, bytes) else "w"
69
+ encoding = None if isinstance(body, bytes) else "utf-8"
70
+ with report_path.open(mode, encoding=encoding) as fh:
71
+ fh.write(body)
72
+
73
+ return ReportPath(path=report_path, format=report_format)