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,60 @@
1
+ """Peer-review routing based on persisted manuscript language metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+
7
+ from ai_scientist.evidence_registry import latest_evidence, record_evidence
8
+ from ai_scientist.project_handle import ResearchProjectHandle
9
+ from ai_scientist.skill_invocation import SkillInvoker
10
+
11
+ PEER_REVIEW_DEPENDENCIES = {
12
+ "ja": {"skillId": "japanese-prose", "version": "0.3.0"},
13
+ "en": {"skillId": "tech-writer", "version": "0.3.0"},
14
+ }
15
+
16
+
17
+ class PeerReviewBlockedError(ValueError):
18
+ """Peer review cannot proceed without supported manuscript language metadata."""
19
+
20
+
21
+ def _now() -> str:
22
+ return datetime.now(timezone.utc).isoformat()
23
+
24
+
25
+ # @id CODE-AISCI-012
26
+ # @implements REQ-AISCI-011, REQ-AISCI-012, REQ-AISCI-013
27
+ # @design DES-AISCI-008
28
+ def review_manuscript(handle: ResearchProjectHandle, invoker: SkillInvoker) -> dict:
29
+ """Delegate peer review using the persisted manuscript language metadata."""
30
+ manuscript = latest_evidence(handle, "manuscript-writing")
31
+ language = manuscript.metadata.get("language") if manuscript is not None else None
32
+ if language not in PEER_REVIEW_DEPENDENCIES:
33
+ raise PeerReviewBlockedError(
34
+ "Supported manuscript language metadata is required before peer-review."
35
+ )
36
+ dependency = PEER_REVIEW_DEPENDENCIES[language]
37
+ response = invoker.invoke(
38
+ dependency["skillId"],
39
+ dependency["version"],
40
+ "review",
41
+ {"manuscriptPath": manuscript.artifact_path},
42
+ )
43
+ artifact = handle.review_dir / "peer-review.md"
44
+ artifact.write_text(response["content"], encoding="utf-8")
45
+ record = record_evidence(
46
+ handle,
47
+ "peer-review",
48
+ artifact,
49
+ "markdown",
50
+ _now(),
51
+ metadata={"language": language},
52
+ )
53
+ return {
54
+ "phase": "peer-review",
55
+ "artifact": {
56
+ "phase": record.phase,
57
+ "path": record.artifact_path,
58
+ "metadata": record.metadata,
59
+ },
60
+ }
@@ -0,0 +1,74 @@
1
+ """Phase gate enforcement for ai_scientist."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timezone
7
+
8
+ from ai_scientist.phase_state import PHASE_ORDER, load_phase_state, record_override
9
+ from ai_scientist.project_handle import ResearchProjectHandle
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class GateDecision:
14
+ """Outcome of a phase gate check."""
15
+
16
+ allowed: bool
17
+ message: str
18
+ blocked_on: str | None = None
19
+ override_applied: bool = False
20
+
21
+
22
+ def _now() -> str:
23
+ return datetime.now(timezone.utc).isoformat()
24
+
25
+
26
+ # @id CODE-AISCI-006
27
+ # @implements REQ-AISCI-006
28
+ # @design DES-AISCI-004
29
+ # @id CODE-AISCI-008
30
+ # @implements REQ-AISCI-007
31
+ # @design DES-AISCI-004
32
+ def check_gate(
33
+ handle: ResearchProjectHandle,
34
+ requested_phase: str,
35
+ override: dict[str, str] | None = None,
36
+ ) -> GateDecision:
37
+ """Allow only the active phase unless an explicit single-request override exists."""
38
+ state = load_phase_state(handle)
39
+ current = state.active_phase
40
+ if requested_phase == current:
41
+ return GateDecision(True, f"{requested_phase} is active and allowed.")
42
+
43
+ predecessors = [
44
+ phase
45
+ for phase in PHASE_ORDER[: PHASE_ORDER.index(requested_phase)]
46
+ if phase not in state.completed_phases
47
+ ]
48
+ if not predecessors:
49
+ # requested_phase is an earlier phase whose own predecessors are all
50
+ # complete (e.g. re-running an already-completed phase); there is no
51
+ # incomplete predecessor to block on, so allow it without an override.
52
+ return GateDecision(True, f"{requested_phase}'s predecessors are complete; allowed.")
53
+ blocked_on = predecessors[0]
54
+
55
+ if override is None:
56
+ return GateDecision(
57
+ allowed=False,
58
+ blocked_on=blocked_on,
59
+ message=f"Cannot run {requested_phase} because {blocked_on} is incomplete.",
60
+ )
61
+
62
+ record_override(
63
+ handle,
64
+ requested_phase=requested_phase,
65
+ reason=override["reason"],
66
+ incomplete_predecessors=predecessors,
67
+ timestamp=_now(),
68
+ )
69
+ return GateDecision(
70
+ allowed=True,
71
+ blocked_on=None,
72
+ override_applied=True,
73
+ message=f"Override applied for {requested_phase}.",
74
+ )
@@ -0,0 +1,230 @@
1
+ """Persistent phase state for ai_scientist projects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import json
7
+ import sys
8
+ from dataclasses import asdict, dataclass, field
9
+ from pathlib import Path
10
+ from uuid import uuid4
11
+
12
+ from ai_scientist.completion_gate import validate_completion_evidence
13
+ from ai_scientist.project_handle import ResearchProjectHandle
14
+
15
+ if sys.platform == "win32": # pragma: no cover - exercised only on Windows
16
+ import msvcrt
17
+ else:
18
+ import fcntl
19
+
20
+ PHASE_ORDER = (
21
+ "research-planning",
22
+ "literature-review",
23
+ "experimental-design",
24
+ "data-analysis",
25
+ "manuscript-writing",
26
+ "peer-review",
27
+ "reproducibility-check",
28
+ "presentation",
29
+ )
30
+ DEFAULT_STATE_PATH = ".ai_scientist_phase_state.json"
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class OverrideRecord:
35
+ """A durable single-request override audit record."""
36
+
37
+ request_id: str
38
+ timestamp: str
39
+ requested_phase: str
40
+ reason: str
41
+ incomplete_predecessors: list[str]
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class PhaseState:
46
+ """Persisted lifecycle state."""
47
+
48
+ active_phase: str | None
49
+ completed_phases: list[str]
50
+ incomplete_phases: list[str]
51
+ overrides: list[OverrideRecord] = field(default_factory=list)
52
+
53
+
54
+ def _state_path(handle: ResearchProjectHandle) -> Path:
55
+ return handle.root / DEFAULT_STATE_PATH
56
+
57
+
58
+ def _lock_path(handle: ResearchProjectHandle) -> Path:
59
+ return _state_path(handle).with_suffix(".lock")
60
+
61
+
62
+ # Test seam: called once per mutating read-modify-write cycle, after the
63
+ # state has been read under the exclusive lock and before it is written
64
+ # back. A no-op in production; tests may monkeypatch it to deterministically
65
+ # exercise concurrent-writer scenarios (DES-AISCI-003 cross-process safety).
66
+ def _after_locked_read_hook() -> None:
67
+ return None
68
+
69
+
70
+ @contextlib.contextmanager
71
+ def _locked(handle: ResearchProjectHandle):
72
+ """Serialize read-modify-write access to this project's phase state file.
73
+
74
+ An OS-level advisory exclusive lock on a sibling ``.lock`` file makes the
75
+ read-decide-write cycle in :func:`mark_phase_complete` and
76
+ :func:`record_override` safe across separate CLI invocations (DES-AISCI-003:
77
+ "Must be read-modify-write safe across separate CLI invocations"),
78
+ preventing one invocation's transition from silently clobbering another's.
79
+
80
+ Closes jupytermind#64 / CHANGE-019 (ADR-0075): see TEST-AISCI-041.
81
+ The lock is scoped per phase-state file, so unrelated projects never
82
+ contend with each other.
83
+ """
84
+ lock_path = _lock_path(handle)
85
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
86
+ with open(lock_path, "a+b") as lock_file:
87
+ if sys.platform == "win32": # pragma: no cover - exercised only on Windows
88
+ msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)
89
+ try:
90
+ yield
91
+ finally:
92
+ lock_file.seek(0)
93
+ msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
94
+ else:
95
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
96
+ try:
97
+ yield
98
+ finally:
99
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
100
+
101
+
102
+ def _initial_state() -> PhaseState:
103
+ return PhaseState(
104
+ active_phase=PHASE_ORDER[0],
105
+ completed_phases=[],
106
+ incomplete_phases=list(PHASE_ORDER[1:]),
107
+ overrides=[],
108
+ )
109
+
110
+
111
+ def _read_state(handle: ResearchProjectHandle) -> PhaseState:
112
+ path = _state_path(handle)
113
+ if not path.exists():
114
+ return _initial_state()
115
+ payload = json.loads(path.read_text(encoding="utf-8"))
116
+ return PhaseState(
117
+ active_phase=payload["active_phase"],
118
+ completed_phases=payload["completed_phases"],
119
+ incomplete_phases=payload["incomplete_phases"],
120
+ overrides=[OverrideRecord(**item) for item in payload.get("overrides", [])],
121
+ )
122
+
123
+
124
+ def _write_state(handle: ResearchProjectHandle, state: PhaseState) -> None:
125
+ path = _state_path(handle)
126
+ temp_path = path.with_suffix(".tmp")
127
+ temp_path.write_text(
128
+ json.dumps(
129
+ {
130
+ "active_phase": state.active_phase,
131
+ "completed_phases": state.completed_phases,
132
+ "incomplete_phases": state.incomplete_phases,
133
+ "overrides": [asdict(record) for record in state.overrides],
134
+ },
135
+ indent=2,
136
+ ensure_ascii=False,
137
+ ),
138
+ encoding="utf-8",
139
+ )
140
+ temp_path.replace(path)
141
+
142
+
143
+ # @id CODE-AISCI-004
144
+ # @implements REQ-AISCI-004
145
+ # @design DES-AISCI-003
146
+ def load_phase_state(handle: ResearchProjectHandle) -> PhaseState:
147
+ """Load or initialize the project's phase state."""
148
+ state = _read_state(handle)
149
+ if not _state_path(handle).exists():
150
+ _write_state(handle, state)
151
+ return state
152
+
153
+
154
+ def active_phase(handle: ResearchProjectHandle) -> str | None:
155
+ """Return the current active phase."""
156
+ return load_phase_state(handle).active_phase
157
+
158
+
159
+ def _next_phase(phase: str) -> str | None:
160
+ index = PHASE_ORDER.index(phase)
161
+ if index + 1 >= len(PHASE_ORDER):
162
+ return None
163
+ return PHASE_ORDER[index + 1]
164
+
165
+
166
+ # @id CODE-AISCI-005
167
+ # @implements REQ-AISCI-005
168
+ # @design DES-AISCI-003
169
+ def mark_phase_complete(handle: ResearchProjectHandle, phase: str) -> PhaseState:
170
+ """Mark ``phase`` complete when it is the active phase and matching evidence exists."""
171
+ if phase not in PHASE_ORDER:
172
+ raise ValueError(f"Unknown phase: {phase!r}.")
173
+ with _locked(handle):
174
+ state = load_phase_state(handle)
175
+ _after_locked_read_hook()
176
+ if phase != state.active_phase:
177
+ raise ValueError(
178
+ f"Cannot complete {phase}: it is not the active phase ({state.active_phase})."
179
+ )
180
+ if not validate_completion_evidence(handle, phase):
181
+ raise ValueError(f"Cannot complete {phase} without matching evidence.")
182
+ completed = list(state.completed_phases)
183
+ if phase not in completed:
184
+ completed.append(phase)
185
+ next_active = _next_phase(phase)
186
+ incomplete = [item for item in PHASE_ORDER if item not in completed and item != next_active]
187
+ updated = PhaseState(
188
+ active_phase=next_active,
189
+ completed_phases=completed,
190
+ incomplete_phases=incomplete,
191
+ overrides=state.overrides,
192
+ )
193
+ _write_state(handle, updated)
194
+ return updated
195
+
196
+
197
+ # @id CODE-AISCI-007
198
+ # @implements REQ-AISCI-007
199
+ # @design DES-AISCI-004
200
+ def record_override(
201
+ handle: ResearchProjectHandle,
202
+ requested_phase: str,
203
+ reason: str,
204
+ incomplete_predecessors: list[str],
205
+ timestamp: str,
206
+ ) -> OverrideRecord:
207
+ """Persist one override audit entry without mutating active phase."""
208
+ with _locked(handle):
209
+ state = load_phase_state(handle)
210
+ _after_locked_read_hook()
211
+ record = OverrideRecord(
212
+ request_id=str(uuid4()),
213
+ timestamp=timestamp,
214
+ requested_phase=requested_phase,
215
+ reason=reason,
216
+ incomplete_predecessors=incomplete_predecessors,
217
+ )
218
+ updated = PhaseState(
219
+ active_phase=state.active_phase,
220
+ completed_phases=state.completed_phases,
221
+ incomplete_phases=state.incomplete_phases,
222
+ overrides=[*state.overrides, record],
223
+ )
224
+ _write_state(handle, updated)
225
+ return record
226
+
227
+
228
+ def list_overrides(handle: ResearchProjectHandle) -> list[OverrideRecord]:
229
+ """Return persisted override audit records."""
230
+ return load_phase_state(handle).overrides
@@ -0,0 +1,56 @@
1
+ """Presentation-planner delegation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+
7
+ from ai_scientist.evidence_registry import latest_evidence, query_evidence, record_evidence
8
+ from ai_scientist.project_handle import ResearchProjectHandle
9
+ from ai_scientist.skill_invocation import SkillInvoker
10
+
11
+ PRESENTATION_DEPENDENCY = {"skillId": "presentation-planner", "version": "0.3.0"}
12
+
13
+
14
+ def _now() -> str:
15
+ return datetime.now(timezone.utc).isoformat()
16
+
17
+
18
+ # @id CODE-AISCI-013
19
+ # @implements REQ-AISCI-014
20
+ # @design DES-AISCI-009
21
+ def build_presentation(handle: ResearchProjectHandle, invoker: SkillInvoker) -> dict:
22
+ """Delegate presentation planning and record the resulting outline."""
23
+ manuscript = latest_evidence(handle, "manuscript-writing")
24
+ if manuscript is None:
25
+ raise ValueError("A manuscript artifact is required before the presentation phase.")
26
+ evidence_manifest = [
27
+ {
28
+ "phase": record.phase,
29
+ "artifactPath": record.artifact_path,
30
+ "artifactKind": record.artifact_kind,
31
+ "createdAt": record.created_at,
32
+ "metadata": record.metadata,
33
+ }
34
+ for record in query_evidence(handle)
35
+ if record.phase != "presentation"
36
+ ]
37
+ response = invoker.invoke(
38
+ PRESENTATION_DEPENDENCY["skillId"],
39
+ PRESENTATION_DEPENDENCY["version"],
40
+ "plan",
41
+ {
42
+ "manuscriptPath": manuscript.artifact_path if manuscript is not None else "",
43
+ "evidenceManifest": evidence_manifest,
44
+ },
45
+ )
46
+ artifact = handle.presentation_dir / "presentation-plan.md"
47
+ artifact.write_text(response["content"], encoding="utf-8")
48
+ record = record_evidence(handle, "presentation", artifact, "markdown", _now())
49
+ return {
50
+ "phase": "presentation",
51
+ "artifact": {
52
+ "phase": record.phase,
53
+ "path": record.artifact_path,
54
+ "metadata": record.metadata,
55
+ },
56
+ }
@@ -0,0 +1,31 @@
1
+ """Project-scoped configuration for ai_scientist."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from ai_scientist.project_handle import ResearchProjectHandle
9
+
10
+ DEFAULT_CONFIG_PATH = ".ai_scientist_project.json"
11
+
12
+
13
+ def _config_path(handle: ResearchProjectHandle) -> Path:
14
+ return handle.root / DEFAULT_CONFIG_PATH
15
+
16
+
17
+ def load_project_config(handle: ResearchProjectHandle) -> dict:
18
+ """Load project configuration, defaulting to an empty mapping."""
19
+ path = _config_path(handle)
20
+ if not path.exists():
21
+ return {}
22
+ return json.loads(path.read_text(encoding="utf-8"))
23
+
24
+
25
+ def save_project_config(handle: ResearchProjectHandle, config: dict) -> Path:
26
+ """Persist project configuration atomically."""
27
+ path = _config_path(handle)
28
+ temp_path = path.with_suffix(".tmp")
29
+ temp_path.write_text(json.dumps(config, indent=2, ensure_ascii=False), encoding="utf-8")
30
+ temp_path.replace(path)
31
+ return path
@@ -0,0 +1,74 @@
1
+ """Research workspace resolution for ai_scientist."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from ai_data_scientist.project_manager import (
9
+ InvalidProjectNameError,
10
+ ProjectHandle,
11
+ resolve_project,
12
+ )
13
+
14
+ PHASE_DIRECTORIES = {
15
+ "planning": "planning",
16
+ "literature": "literature",
17
+ "design": "design",
18
+ "manuscript": "manuscript",
19
+ "review": "review",
20
+ "reproducibility": "reproducibility",
21
+ "presentation": "presentation",
22
+ }
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ResearchProjectHandle:
27
+ """Stable ai_scientist project paths derived from ai-data-scientist."""
28
+
29
+ name: str
30
+ root: Path
31
+ notebook_path: Path
32
+ planning_dir: Path
33
+ literature_dir: Path
34
+ design_dir: Path
35
+ manuscript_dir: Path
36
+ review_dir: Path
37
+ reproducibility_dir: Path
38
+ presentation_dir: Path
39
+
40
+
41
+ def _ensure_phase_dirs(handle: ProjectHandle) -> ResearchProjectHandle:
42
+ phase_paths = {name: handle.root / directory for name, directory in PHASE_DIRECTORIES.items()}
43
+ for path in phase_paths.values():
44
+ path.mkdir(parents=True, exist_ok=True)
45
+ return ResearchProjectHandle(
46
+ name=handle.name,
47
+ root=handle.root,
48
+ notebook_path=handle.notebook_path,
49
+ planning_dir=phase_paths["planning"],
50
+ literature_dir=phase_paths["literature"],
51
+ design_dir=phase_paths["design"],
52
+ manuscript_dir=phase_paths["manuscript"],
53
+ review_dir=phase_paths["review"],
54
+ reproducibility_dir=phase_paths["reproducibility"],
55
+ presentation_dir=phase_paths["presentation"],
56
+ )
57
+
58
+
59
+ # @id CODE-AISCI-002
60
+ # @implements REQ-AISCI-002
61
+ # @design DES-AISCI-002
62
+ # @id CODE-AISCI-003
63
+ # @implements REQ-AISCI-003
64
+ # @design DES-AISCI-002
65
+ def resolve_research_project(
66
+ name: str,
67
+ projects_root: Path | str | None = None,
68
+ ) -> ResearchProjectHandle:
69
+ """Resolve the shared stable project handle and create ai_scientist dirs."""
70
+ if not isinstance(name, str):
71
+ raise InvalidProjectNameError(
72
+ f"Project name must be a string, got {type(name).__name__} ({name!r})."
73
+ )
74
+ return _ensure_phase_dirs(resolve_project(name, projects_root=projects_root))
@@ -0,0 +1,20 @@
1
+ """Reproducibility check phase handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+
7
+ from ai_scientist.evidence_registry import record_evidence
8
+ from ai_scientist.project_handle import ResearchProjectHandle
9
+
10
+
11
+ def _now() -> str:
12
+ return datetime.now(timezone.utc).isoformat()
13
+
14
+
15
+ def handle_reproducibility_check(handle: ResearchProjectHandle, instruction: str) -> dict:
16
+ """Create a reproducibility artifact and record it."""
17
+ artifact = handle.reproducibility_dir / "reproducibility-check.md"
18
+ artifact.write_text(instruction, encoding="utf-8")
19
+ record_evidence(handle, "reproducibility-check", artifact, "markdown", _now())
20
+ return {"phase": "reproducibility-check", "artifact_path": str(artifact)}
@@ -0,0 +1,20 @@
1
+ """Research planning phase handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+
7
+ from ai_scientist.evidence_registry import record_evidence
8
+ from ai_scientist.project_handle import ResearchProjectHandle
9
+
10
+
11
+ def _now() -> str:
12
+ return datetime.now(timezone.utc).isoformat()
13
+
14
+
15
+ def handle_research_planning(handle: ResearchProjectHandle, instruction: str) -> dict:
16
+ """Create a planning artifact and record it as evidence."""
17
+ artifact = handle.planning_dir / "research-plan.md"
18
+ artifact.write_text(instruction, encoding="utf-8")
19
+ record_evidence(handle, "research-planning", artifact, "markdown", _now())
20
+ return {"phase": "research-planning", "artifact_path": str(artifact)}
@@ -0,0 +1,21 @@
1
+ """Skill invocation abstraction for sibling Copilot skills."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol
6
+
7
+
8
+ class SkillInvoker(Protocol):
9
+ """Minimal sibling-skill invocation contract."""
10
+
11
+ def invoke(self, skill_id: str, version: str, mode: str, payload: dict) -> dict: ...
12
+
13
+
14
+ class DefaultSkillInvoker:
15
+ """Placeholder implementation for non-test Python execution contexts."""
16
+
17
+ def invoke(self, skill_id: str, version: str, mode: str, payload: dict) -> dict:
18
+ raise NotImplementedError(
19
+ "Sibling skill invocation must be provided by the Copilot agent runtime. "
20
+ "See .github/skills/ai-scientist/SKILL.md for the delegation contract."
21
+ )
@@ -0,0 +1,99 @@
1
+ """Local helper for REQ-AISCI-024's configured test-suite gate.
2
+
3
+ The configured suite is the release proof that every REQ-AISCI requirement,
4
+ including npm-packaging coverage added in CHANGE-023, has executable
5
+ verification in the ai_scientist test corpus.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ import subprocess
14
+ import tempfile
15
+ from collections.abc import Sequence
16
+ from pathlib import Path
17
+
18
+ _SKIPPED_TEST_RE = re.compile(r"\b\d+\s+skipped\b", re.IGNORECASE)
19
+
20
+
21
+ class TestSuiteGateError(RuntimeError):
22
+ """Configured test-suite run failed the ai_scientist TDD gate."""
23
+
24
+
25
+ def _is_pytest_invocation(command: Sequence[str]) -> bool:
26
+ """True only for an actual pytest invocation: an executable literally
27
+ named ``pytest``/``pytest-<suffix>``, or ``-m pytest``. A substring match
28
+ against arbitrary command parts would also match an unrelated script or
29
+ test-file path that merely contains "pytest" in its name. Closes a
30
+ rubber-duck finding from the CHANGE-019 review; see TEST-AISCI-047."""
31
+ parts = [str(part) for part in command]
32
+ for index, part in enumerate(parts):
33
+ basename = Path(part).name
34
+ if basename == "pytest" or basename.startswith("pytest-"):
35
+ return True
36
+ if part == "-m" and index + 1 < len(parts) and parts[index + 1] == "pytest":
37
+ return True
38
+ return False
39
+
40
+
41
+ def _unapproved_skips(report_path: str, approved_skips: frozenset[str]) -> list[str]:
42
+ """Return skipped test nodeids from the musubix-json pytest report that
43
+ are not present in ``approved_skips``, structurally (not by text-parsing
44
+ console output). Closes jupytermind#67 / CHANGE-019 (ADR-0091); see
45
+ TEST-AISCI-044 and TEST-AISCI-045."""
46
+ with open(report_path, encoding="utf-8") as handle:
47
+ report = json.load(handle)
48
+ skipped = [
49
+ test["nodeid"] for test in report.get("tests", []) if test.get("outcome") == "skipped"
50
+ ]
51
+ return sorted(nodeid for nodeid in skipped if nodeid not in approved_skips)
52
+
53
+
54
+ # @id CODE-AISCI-024
55
+ # @implements REQ-AISCI-024
56
+ # @design DES-AISCI-019
57
+ def run_configured_test_suite(
58
+ command: Sequence[str],
59
+ *,
60
+ approved_skips: frozenset[str] = frozenset(),
61
+ ) -> subprocess.CompletedProcess[str]:
62
+ """Execute the configured test suite command and return its completed
63
+ process, failing on any non-zero exit or any unapproved skipped test.
64
+
65
+ Skip detection is structural (via a ``pytest-json-report`` machine
66
+ report) rather than console-text parsing whenever the command invokes
67
+ pytest, so a differently worded summary line cannot hide a real skip and
68
+ an explicitly approved skip does not fail the gate. CHANGE-023 relies on
69
+ the same suite gate to certify the new REQ-AISCI-025 packaging
70
+ regression alongside the pre-existing REQ-AISCI coverage set.
71
+ """
72
+ is_pytest = _is_pytest_invocation(command)
73
+ report_path: str | None = None
74
+ full_command = list(command)
75
+ if is_pytest:
76
+ fd, report_path = tempfile.mkstemp(suffix=".json")
77
+ os.close(fd)
78
+ full_command = [*command, "--json-report", f"--json-report-file={report_path}"]
79
+
80
+ try:
81
+ result = subprocess.run(full_command, check=False, capture_output=True, text=True)
82
+ if result.returncode != 0:
83
+ raise TestSuiteGateError("Configured test suite failed.")
84
+
85
+ if report_path is not None:
86
+ unapproved = _unapproved_skips(report_path, approved_skips)
87
+ if unapproved:
88
+ raise TestSuiteGateError(
89
+ "Configured test suite reported unapproved skipped tests: "
90
+ + ", ".join(unapproved)
91
+ )
92
+ else:
93
+ combined_output = "\n".join(part for part in (result.stdout, result.stderr) if part)
94
+ if _SKIPPED_TEST_RE.search(combined_output):
95
+ raise TestSuiteGateError("Configured test suite reported skipped tests.")
96
+ return result
97
+ finally:
98
+ if report_path is not None:
99
+ Path(report_path).unlink(missing_ok=True)