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.
- package/.github/skills/ai-chemistry-scientist/SKILL.md +97 -0
- package/.github/skills/ai-chemistry-scientist/manifest.json +156 -0
- package/.github/skills/ai-data-scientist/SKILL.md +330 -0
- package/.github/skills/ai-genomics-scientist/SKILL.md +98 -0
- package/.github/skills/ai-genomics-scientist/manifest.json +93 -0
- package/.github/skills/ai-materials-scientist/SKILL.md +51 -0
- package/.github/skills/ai-materials-scientist/manifest.json +58 -0
- package/.github/skills/ai-scientist/SKILL.md +69 -0
- package/.github/skills/ai-scientist/manifest.json +61 -0
- package/.github/skills/ai-structural-biology-scientist/SKILL.md +67 -0
- package/.github/skills/ai-structural-biology-scientist/manifest.json +72 -0
- package/.github/skills/japanese-prose/NOTICE.md +17 -0
- package/.github/skills/japanese-prose/SKILL.md +111 -0
- package/.github/skills/japanese-prose/references/review-workflow.md +50 -0
- package/.github/skills/japanese-prose/references/scoring.md +24 -0
- package/.github/skills/japanese-prose/references/writing-guidelines.md +60 -0
- package/.github/skills/japanese-prose/scripts/core.py +192 -0
- package/.github/skills/japanese-prose/scripts/fixtures/natural.md +5 -0
- package/.github/skills/japanese-prose/scripts/fixtures/unnatural.md +5 -0
- package/.github/skills/japanese-prose/scripts/lint.py +378 -0
- package/.github/skills/japanese-prose/scripts/outline.py +68 -0
- package/.github/skills/japanese-prose/scripts/terms.py +112 -0
- package/.github/skills/japanese-prose/scripts/test_engine.py +117 -0
- package/.github/skills/presentation-planner/SKILL.md +257 -0
- package/.github/skills/presentation-planner/assets/design-templates/data-report.yaml +97 -0
- package/.github/skills/presentation-planner/assets/design-templates/executive-proposal.yaml +92 -0
- package/.github/skills/presentation-planner/assets/design-templates/technical-briefing.yaml +96 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/data-report.md +47 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/executive-decision.md +43 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/technical-briefing.md +45 -0
- package/.github/skills/presentation-planner/references/customizing-design-templates.md +160 -0
- package/.github/skills/presentation-planner/references/design-spec-schema.md +72 -0
- package/.github/skills/presentation-planner/references/handoff-contract.md +49 -0
- package/.github/skills/presentation-planner/references/responsibility-boundary.md +32 -0
- package/.github/skills/presentation-planner/references/scenario-templates.md +55 -0
- package/.github/skills/tech-writer/SKILL.md +434 -0
- package/.github/skills/tech-writer/assets/templates/blueprint.md +187 -0
- package/.github/skills/tech-writer/assets/templates/design-doc.md +29 -0
- package/.github/skills/tech-writer/assets/templates/migration-plan.md +173 -0
- package/.github/skills/tech-writer/assets/templates/operations-runbook.md +202 -0
- package/.github/skills/tech-writer/assets/templates/pr-description.md +23 -0
- package/.github/skills/tech-writer/assets/templates/qiita.md +44 -0
- package/.github/skills/tech-writer/assets/templates/readme.md +38 -0
- package/.github/skills/tech-writer/assets/templates/requirements-definition.md +170 -0
- package/.github/skills/tech-writer/assets/templates/rfi.md +113 -0
- package/.github/skills/tech-writer/assets/templates/rfp.md +180 -0
- package/.github/skills/tech-writer/assets/templates/security-design.md +167 -0
- package/.github/skills/tech-writer/assets/templates/system-design.md +220 -0
- package/.github/skills/tech-writer/assets/templates/technical-proposal.md +112 -0
- package/.github/skills/tech-writer/assets/templates/test-plan.md +153 -0
- package/.github/skills/tech-writer/assets/templates/user-manual.md +22 -0
- package/.github/skills/tech-writer/assets/templates/white-paper.md +192 -0
- package/.github/skills/tech-writer/references/doctypes/api-docs.md +33 -0
- package/.github/skills/tech-writer/references/doctypes/blueprint.md +81 -0
- package/.github/skills/tech-writer/references/doctypes/code-comments.md +39 -0
- package/.github/skills/tech-writer/references/doctypes/design-doc.md +42 -0
- package/.github/skills/tech-writer/references/doctypes/migration-plan.md +63 -0
- package/.github/skills/tech-writer/references/doctypes/operations-runbook.md +63 -0
- package/.github/skills/tech-writer/references/doctypes/pr-commit.md +82 -0
- package/.github/skills/tech-writer/references/doctypes/qiita.md +75 -0
- package/.github/skills/tech-writer/references/doctypes/readme.md +43 -0
- package/.github/skills/tech-writer/references/doctypes/release-notes.md +30 -0
- package/.github/skills/tech-writer/references/doctypes/requirements-definition.md +61 -0
- package/.github/skills/tech-writer/references/doctypes/rfi.md +43 -0
- package/.github/skills/tech-writer/references/doctypes/rfp.md +46 -0
- package/.github/skills/tech-writer/references/doctypes/security-design.md +71 -0
- package/.github/skills/tech-writer/references/doctypes/system-design.md +74 -0
- package/.github/skills/tech-writer/references/doctypes/technical-proposal.md +49 -0
- package/.github/skills/tech-writer/references/doctypes/test-plan.md +67 -0
- package/.github/skills/tech-writer/references/doctypes/user-manual.md +58 -0
- package/.github/skills/tech-writer/references/doctypes/white-paper.md +84 -0
- package/.github/skills/tech-writer/references/doctypes/zenn.md +66 -0
- package/.github/skills/tech-writer/references/japanese-prose-optimization.md +110 -0
- package/.github/skills/tech-writer/references/style-constitution.md +104 -0
- package/.github/skills/tech-writer/scripts/lint.py +412 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/bin/ai-data-scientist.js +123 -0
- package/package.json +41 -0
- package/pyproject.toml +45 -0
- package/src/ai_chemistry_scientist/__init__.py +0 -0
- package/src/ai_chemistry_scientist/admet_prediction.py +71 -0
- package/src/ai_chemistry_scientist/bioactivity_classification.py +73 -0
- package/src/ai_chemistry_scientist/data/sample_molecules.csv +21 -0
- package/src/ai_chemistry_scientist/dispatch.py +369 -0
- package/src/ai_chemistry_scientist/docking_score.py +97 -0
- package/src/ai_chemistry_scientist/drug_likeness_rules.py +84 -0
- package/src/ai_chemistry_scientist/evidence.py +41 -0
- package/src/ai_chemistry_scientist/molecular_descriptors.py +97 -0
- package/src/ai_chemistry_scientist/molecular_formula_mass.py +40 -0
- package/src/ai_chemistry_scientist/molecular_similarity.py +78 -0
- package/src/ai_chemistry_scientist/qsar_modeling.py +105 -0
- package/src/ai_chemistry_scientist/salt_standardization.py +81 -0
- package/src/ai_chemistry_scientist/structural_alerts.py +76 -0
- package/src/ai_chemistry_scientist/structure_format_conversion.py +84 -0
- package/src/ai_chemistry_scientist/validation.py +70 -0
- package/src/ai_data_scientist/__init__.py +0 -0
- package/src/ai_data_scientist/analysis_assumptions.py +121 -0
- package/src/ai_data_scientist/anomaly_detection.py +39 -0
- package/src/ai_data_scientist/automl.py +109 -0
- package/src/ai_data_scientist/cleaning.py +56 -0
- package/src/ai_data_scientist/cli.py +90 -0
- package/src/ai_data_scientist/clustering.py +54 -0
- package/src/ai_data_scientist/dashboard.py +33 -0
- package/src/ai_data_scientist/data_definition.py +100 -0
- package/src/ai_data_scientist/data_quality.py +164 -0
- package/src/ai_data_scientist/dataset_validation.py +135 -0
- package/src/ai_data_scientist/dependency_pins.py +60 -0
- package/src/ai_data_scientist/eda.py +82 -0
- package/src/ai_data_scientist/experiment_evaluation.py +635 -0
- package/src/ai_data_scientist/explainability.py +340 -0
- package/src/ai_data_scientist/feature_engineering.py +163 -0
- package/src/ai_data_scientist/gate_config.py +32 -0
- package/src/ai_data_scientist/ingestion.py +127 -0
- package/src/ai_data_scientist/insight_engine.py +180 -0
- package/src/ai_data_scientist/japanese_nlp.py +43 -0
- package/src/ai_data_scientist/jupyter_launcher.py +137 -0
- package/src/ai_data_scientist/jupyter_mcp_client.py +94 -0
- package/src/ai_data_scientist/language_router.py +28 -0
- package/src/ai_data_scientist/lifecycle.py +221 -0
- package/src/ai_data_scientist/mcp_gateway.py +113 -0
- package/src/ai_data_scientist/mcp_runtime.py +194 -0
- package/src/ai_data_scientist/mcp_transport.py +53 -0
- package/src/ai_data_scientist/ml_modeling.py +451 -0
- package/src/ai_data_scientist/model_tuning.py +104 -0
- package/src/ai_data_scientist/notebook_audit.py +574 -0
- package/src/ai_data_scientist/project_manager.py +243 -0
- package/src/ai_data_scientist/report_export.py +73 -0
- package/src/ai_data_scientist/sensitivity.py +445 -0
- package/src/ai_data_scientist/signal_analysis.py +201 -0
- package/src/ai_data_scientist/skill_packaging.py +40 -0
- package/src/ai_data_scientist/stats_analysis.py +88 -0
- package/src/ai_data_scientist/text_nlp.py +44 -0
- package/src/ai_data_scientist/timeseries.py +68 -0
- package/src/ai_data_scientist/visualization.py +708 -0
- package/src/ai_genomics_scientist/__init__.py +1 -0
- package/src/ai_genomics_scientist/differential_expression.py +147 -0
- package/src/ai_genomics_scientist/dispatch.py +267 -0
- package/src/ai_genomics_scientist/evidence.py +45 -0
- package/src/ai_genomics_scientist/gene_set_enrichment.py +76 -0
- package/src/ai_genomics_scientist/sequence_alignment.py +97 -0
- package/src/ai_genomics_scientist/sequence_features.py +111 -0
- package/src/ai_genomics_scientist/splice_site_scoring.py +66 -0
- package/src/ai_genomics_scientist/validation.py +83 -0
- package/src/ai_genomics_scientist/variant_effect.py +147 -0
- package/src/ai_genomics_scientist/variant_pathogenicity.py +125 -0
- package/src/ai_materials_scientist/__init__.py +0 -0
- package/src/ai_materials_scientist/calphad.py +117 -0
- package/src/ai_materials_scientist/classical_monte_carlo.py +165 -0
- package/src/ai_materials_scientist/crystal_plasticity.py +184 -0
- package/src/ai_materials_scientist/dispatch.py +100 -0
- package/src/ai_materials_scientist/evidence.py +84 -0
- package/src/ai_materials_scientist/fem.py +279 -0
- package/src/ai_materials_scientist/kinetic_monte_carlo.py +145 -0
- package/src/ai_materials_scientist/molecular_dynamics.py +240 -0
- package/src/ai_materials_scientist/phase_field.py +167 -0
- package/src/ai_materials_scientist/validation.py +70 -0
- package/src/ai_scientist/__init__.py +1 -0
- package/src/ai_scientist/completion_gate.py +15 -0
- package/src/ai_scientist/data_analysis.py +46 -0
- package/src/ai_scientist/evidence_registry.py +99 -0
- package/src/ai_scientist/experimental_design.py +20 -0
- package/src/ai_scientist/language.py +14 -0
- package/src/ai_scientist/latex_renderer.py +41 -0
- package/src/ai_scientist/literature_review.py +37 -0
- package/src/ai_scientist/manifest.py +87 -0
- package/src/ai_scientist/manuscript.py +94 -0
- package/src/ai_scientist/mcp_config.py +76 -0
- package/src/ai_scientist/mcp_external.py +42 -0
- package/src/ai_scientist/mcp_failures.py +23 -0
- package/src/ai_scientist/mcp_gateway.py +38 -0
- package/src/ai_scientist/mcp_managed.py +180 -0
- package/src/ai_scientist/npm_packaging.py +49 -0
- package/src/ai_scientist/orchestrator.py +133 -0
- package/src/ai_scientist/peer_review.py +60 -0
- package/src/ai_scientist/phase_gate.py +74 -0
- package/src/ai_scientist/phase_state.py +230 -0
- package/src/ai_scientist/presentation.py +56 -0
- package/src/ai_scientist/project_config.py +31 -0
- package/src/ai_scientist/project_handle.py +74 -0
- package/src/ai_scientist/reproducibility.py +20 -0
- package/src/ai_scientist/research_planning.py +20 -0
- package/src/ai_scientist/skill_invocation.py +21 -0
- package/src/ai_scientist/tdd_gate.py +99 -0
- package/src/ai_structural_biology_scientist/__init__.py +0 -0
- package/src/ai_structural_biology_scientist/contact_map.py +87 -0
- package/src/ai_structural_biology_scientist/dispatch.py +269 -0
- package/src/ai_structural_biology_scientist/evidence.py +43 -0
- package/src/ai_structural_biology_scientist/hydrophobicity.py +101 -0
- package/src/ai_structural_biology_scientist/protein_docking_score.py +104 -0
- package/src/ai_structural_biology_scientist/secondary_structure.py +95 -0
- package/src/ai_structural_biology_scientist/structural_similarity.py +74 -0
- 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)
|