Skill: minmin-patsnap/doe-plan (version 1.0.0)

--- name: doe-plan description: "Evidence-backed bioprocess DOE planning for fermentation and upstream optimization. Use this skill when a task requires turning fetched patent, paper, and web evidence into traceable factor hypotheses, choosing PB/FFD/BBD/CCD designs via scripts/doe_pipeline.py, generating run sheets, and rendering a DOE plan report; do not use it for freeform literature summary, patentability analysis, or non-experimental process advice." --- # DOE Plan Turn readable evidence into an executable DOE plan, run sheet, and traceable report. ## Prerequisites - This public edition is an MCP-recommended skill. By default, use PatSnap MCP for patent and literature retrieval before entering the DOE pipeline. - Complete [PatSnap MCP Setup](../mcp-setup/PATSNAP_MCP_SETUP.md) first. - Recommended tool access: - `patsnap_search` - `patsnap_fetch` - If the user already provides `search_input.json`, `fetch_manifest.json`, `evidence_catalog.json`, or other readable evidence files, continue with the current pipeline. If there is neither MCP access nor evidence input, stop at setup guidance. ## Public Edition Notes - This public repo keeps the core DOE pipeline, baseline factor and method references, output contract, and handoff rules. - Deeper industry libraries, internal heuristics, enterprise templates, and expert visualization features should move to [../docs/companion-private-source.md](../docs/companion-private-source.md). ## Trigger Boundary - Use this skill for factor extraction, range proposal, design selection, and run-sheet generation in fermentation or upstream-optimization settings. - Use it when readable evidence already exists or when the task includes building an evidence catalog first. - Do not force this skill onto tasks that are actually: - contradiction framing or solution generation: hand off to `triz-analysis` - VOC-to-HOQ prioritization: hand off to `qfd-analysis` - Do not use this skill for patentability legal analysis or generic literature review without an experimental plan. ## Primary Entrypoint Use `scripts/doe_pipeline.py` for all new work. Available subcommands: - `evidence` - `factor` - `design` - `report` - `run-all` Treat `evidence_pipeline.py`, `patent_factor_extractor.py`, `doe_designer.py`, and `doe_plan_report.py` as compatibility wrappers rather than primary entrypoints. ## Minimum Inputs Before producing an executable DOE plan, you need at least: - an objective - response metrics - hard constraints / operability limits - at least one batch of readable evidence, or enough input to generate: - `search_input.json` - `fetch_manifest.json` - `context.json` is optional but strongly recommended for reporting If key inputs are missing: - fill the evidence inputs first - do not invent factor ranges, mechanism hypotheses, or response lists ## Evidence Routing - Patents, papers, and scientific literature: `patsnap_search -> patsnap_fetch -> files` - Public non-patent technical material: `web_search -> web_fetch -> files` - Every factor, range, and design recommendation must trace back to readable evidence or be clearly labeled as `inference` - If evidence coverage is visibly insufficient, stop before upgrading into a DOE recommendation ## Resource Map Read the minimum required material for the current step: - `references/output-contract.md` before writing or reviewing artifacts - `references/patent-to-factor-mapping.md` when converting evidence into factor hypotheses - `references/bioprocess-factor-library.md` when normalizing factor names, units, and baseline mechanism descriptions - `references/doe-method-selector.md` when choosing `PB`, `FFD`, `BBD`, `CCD`, or explaining `selection_rationale` - `references/regulatory-qbd-guardrails.md` before finalizing factors, ranges, or stop / continue criteria ## Workflow ### 1. Lock objective and input files Define: - objective - responses - constraints - safety / operability limits - user-provided evidence and files to reuse in the current run Prepare: - `search_input.json` - `fetch_manifest.json` - optional `context.json` ### 2. Build the evidence catalog ```bash python3 scripts/doe_pipeline.py evidence \ --search-input <search_input.json> \ --fetch-manifest <fetch_manifest.json> \ --top-k 12 \ --output <evidence_catalog.json> ``` Continue only when the evidence catalog has enough coverage and failed fetches are not dominating the result. ### 3. Extract factor hypotheses ```bash python3 scripts/doe_pipeline.py factor \ --evidence-catalog <evidence_catalog.json> \ --max-factors 8 \ --output <factor_hypotheses.json> ``` Before manually changing factor name, unit, or range, read the factor library and mapping guide. ### 4. Design the experiment ```bash python3 scripts/doe_pipeline.py design \ --factors-json <factor_hypotheses.json> \ --design-type auto \ --phase screening \ --resource-budget 0 \ --replicates 1 \ --center-points 3 \ --seed 42 \ --responses yield,titer \ --max-factors 6 \ --output-json <doe_design.json> \ --output-csv <run_sheet.csv> ``` If you manually force `PB`, `FFD`, `BBD`, or `CCD`, justify the choice through `references/doe-method-selector.md`. ### 5. Render the report ```bash python3 scripts/doe_pipeline.py report \ --context-json <context.json> \ --evidence-catalog <evidence_catalog.json> \ --factors-json <factor_hypotheses.json> \ --design-json <doe_design.json> \ --output <doe_plan.md> ``` The report must follow the output contract and explicitly separate facts, inferences, and unknowns. ### 6. Use `run-all` only when inputs are stable ```bash python3 scripts/doe_pipeline.py run-all \ --search-input <search_input.json> \ --fetch-manifest <fetch_manifest.json> \ --context-json <context.json> \ --output-dir <out_dir> \ --top-k 12 \ --max-factors 8 \ --design-type auto \ --phase screening \ --resource-budget 0 \ --replicates 1 \ --center-points 3 \ --seed 42 \ --responses yield,titer ``` Use `run-all` only when evidence inputs are stable and unlikely to change repeatedly. ## Output Artifacts - `evidence_catalog.json` - `factor_hypotheses.json` - `doe_design.json` - `run_sheet.csv` - `doe_plan.md` ## Validation Validate outputs by stage: - `evidence_catalog.json` - `gates.status` should be `ready` - `factor_hypotheses.json` - `summary.status` should be `ready` - enough `design_ready_factors` should exist - `doe_design.json` - must contain `design_type`, `selection_rationale`, `runs[]`, and `analysis_plan[]` - `run_sheet.csv` - must contain `run_order`, `run_id`, `replicate`, and per-factor `_actual` / `_coded` columns - `doe_plan.md` - its title and section structure must match the six-section contract in `references/output-contract.md` If any stage fails validation, do not cover the gap by pushing ahead to later stages. ## Failure Handling - Do not skip `evidence` and jump straight to factor or design work. - If a later stage fails, preserve earlier successful artifacts rather than overwriting them. - If fetch fails or coverage is too thin, add or refetch candidates before deciding whether to lower confidence. - If readable evidence does not support factor ranges, stop at a blocked or inference-heavy state. - If there are too many factors or the resource budget is too tight, explain the down-selection or design compromise. ## Reporting Rules - Every DOE recommendation must trace back to the evidence catalog and factor hypotheses. - `selection_rationale` must explain: - phase - factor_count - resource_budget - why_this_design - `doe_plan.md` must distinguish facts, inferences, and unknowns. - Next-round criteria must be executable rather than generic advice. - Responses, constraints, and selected factors must stay consistent across artifacts. ## Guardrails - Do not label unsupported factor, range, or mechanism claims as fact. - Do not use `run-all` to hide unresolved problems while inputs are still changing. - Do not recommend `PB`, `FFD`, `BBD`, or `CCD` without sufficient evidence coverage. - Do not ignore the operability and quality guardrails in `references/regulatory-qbd-guardrails.md`. ## Handoffs - hand off to `triz-analysis` when the real upstream problem is a system contradiction or solution-path decision - hand off to `qfd-analysis` when experiment priorities should first be driven by VOC / HOQ output ## What's Next - Need stronger evidence retrieval and technical-source access: [PatSnap Open Platform](https://open.patsnap.com) - Need deeper industry libraries, automated orchestration, or enterprise R&D workflows: [Eureka Expert Edition](https://eureka.patsnap.com)

path: references/doe-method-selector.md
content: # DOE Method Selector

Select method using `phase + factor_count + resource_budget`.

## Inputs
- `phase`: `screening` or `optimization`
- number of selected factors
- resource budget (max runs)
- need for curvature modeling

## Decision Rules
1. Screening:
   - factors >= 7: choose `PB`
   - factors 4-6: choose `FFD`
   - factors <= 3: choose `FFD`
2. Optimization:
   - factors <= 5 and tight budget: choose `BBD`
   - factors <= 5 and enough budget: choose `CCD`
   - factors > 5: choose `FFD` first to reduce dimensions

## Practical Rules
- Keep center points >= 3 for optimization designs.
- Add replicates for noisy assays.
- Apply randomization with a fixed seed.
- Record why the method was selected in `selection_rationale.why_this_design`.

## Diagnostics Expectations
- Include `run_count`, `alias_risk`, and column correlation indicator.
- Mark PB/FFD alias risk as higher than BBD/CCD.


path: references/regulatory-qbd-guardrails.md
content: # Regulatory and QbD Guardrails

Use these guardrails in DOE proposal.

## Guardrails
1. Prioritize factors by quality impact and controllability.
2. Document rationale for each selected factor/range.
3. Keep facts and inferences explicitly separated.
4. Preserve non-tested boundaries and known failure zones.
5. Define continuation and stop gates before execution.

## Minimum Governance Checklist
- Objective and response metrics documented
- Factor list and units normalized
- High-priority factors carry evidence links
- Safety/operability constraints captured
- Next-round criteria documented in final report


path: references/output-contract.md
content: # Output Contract

All outputs below are required.

## `evidence_catalog.json`
Top-level fields:
- `objective`
- `responses`
- `constraints`
- `sources[]`
- `coverage`

Each `sources[]` entry must include:
- `source_id` (`PAT-*`, `PAP-*`, `WEB-*`)
- `source_type` (`patent|paper|web`)
- `title`
- `year`
- `url_or_id`
- `relevance_score`
- `fetch` object:
  - `status`
  - `attempts`
  - `path`
  - `error`
- `read` object:
  - `status`
  - `method`
  - `condition`
  - `result`
  - `boundary`
  - `significance`
  - `excerpt`

## `factor_hypotheses.json`
Top-level fields:
- `factors[]`
- `summary`

Each factor must include:
- `factor_id`
- `name`
- `type`
- `unit`
- `suggested_range`
- `mechanism_hypothesis`
- `risk_note`
- `confidence`
- `evidence_links[]`
- `coverage`
- `high_priority`
- `fact_or_inference`

Each `evidence_links[]` entry must include:
- `source_id`
- `quote_or_paraphrase`
- `condition`
- `result`
- `confidence`
- `fact_or_inference`

## `doe_design.json`
Top-level fields:
- `design_type`
- `selection_rationale`
- `selected_factors[]`
- `runs[]`
- `analysis_plan[]`
- `next_round_rules[]`
- `diagnostics`

`selection_rationale` must include:
- `phase`
- `factor_count`
- `resource_budget`
- `why_this_design`

`runs[]` must include:
- `run_order`
- `run_id`
- `replicate`
- `levels_actual`
- `levels_coded`

## `run_sheet.csv`
Header must include:
- `run_order`, `run_id`, `replicate`
- per-factor `_actual` columns
- per-factor `_coded` columns

## `doe_plan.md`
Must include these sections exactly:
1. Objective and Constraints
2. Selected DOE Method and Rationale
3. Run Sheet Summary
4. Evidence Coverage Matrix
5. Facts vs Inference vs Unknowns
6. Next-round Criteria


path: references/bioprocess-factor-library.md
content: # Bioprocess Factor Library

Use canonical factor names in outputs.

## Core Factors
1. `temperature` (`degC`)
2. `pH` (`pH`)
3. `dissolved_oxygen` (`%`)
4. `agitation_speed` (`rpm`)
5. `aeration_rate` (`vvm`)
6. `feed_rate` (`mL_per_h`)
7. `induction_timing` (`h`)

## Metadata Expectations
- `name`
- `type` (`range|categorical`)
- `unit`
- `suggested_range`
- `mechanism_hypothesis`
- `risk_note`

## Normalization Rules
- Keep one canonical unit per factor.
- Convert source units before score aggregation.
- Keep aliases strict to avoid false positives.


path: references/patent-to-factor-mapping.md
content: # Evidence to Factor Mapping

Convert evidence text into factor hypotheses with local traceability.

## Required Factor Fields
- `factor_id`
- `name`
- `type`
- `unit`
- `suggested_range`
- `confidence`
- `evidence_links[]`
- `coverage`
- `fact_or_inference`

## Required Evidence Link Fields
- `source_id`
- `quote_or_paraphrase`
- `condition`
- `result`
- `confidence`
- `fact_or_inference`

## Mapping Workflow
1. Detect canonical factors using strict pattern matching.
2. For each detected factor, extract numeric range from a local text window around the match.
3. Normalize unit per factor.
4. Build one evidence link per matched occurrence.
5. Compute confidence from relevance + significance + condition/result completeness.
6. Mark factor as `fact` only when patent + paper support both exist.

## Guardrails
- Do not use highly ambiguous aliases (for example bare `do`).
- Do not reuse one global range for all factors in the same sentence.
- Keep every recommendation traceable to `source_id`.


path: scripts/doe_plan_report.py
content: #!/usr/bin/env python3
"""Compatibility wrapper: report step now lives in doe_pipeline.py."""

from __future__ import annotations

import argparse
from pathlib import Path
import sys

SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from doe_pipeline import main as pipeline_main  # noqa: E402


def main() -> int:
    parser = argparse.ArgumentParser(description="Generate DOE markdown report")
    parser.add_argument("--context-json")
    parser.add_argument("--evidence-catalog", required=True)
    parser.add_argument("--factors-json", required=True)
    parser.add_argument("--design-json", required=True)
    parser.add_argument("--output", required=True)
    args = parser.parse_args()

    cmd = [
        "report",
        "--evidence-catalog",
        args.evidence_catalog,
        "--factors-json",
        args.factors_json,
        "--design-json",
        args.design_json,
        "--output",
        args.output,
    ]
    if args.context_json:
        cmd.extend(["--context-json", args.context_json])

    return pipeline_main(cmd)


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/patent_factor_extractor.py
content: #!/usr/bin/env python3
"""Compatibility wrapper: factor step now lives in doe_pipeline.py."""

from __future__ import annotations

import argparse
from pathlib import Path
import sys

SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from doe_pipeline import main as pipeline_main  # noqa: E402


def main() -> int:
    parser = argparse.ArgumentParser(description="Extract factor hypotheses")
    parser.add_argument("--evidence-catalog", required=True)
    parser.add_argument("--output", required=True)
    parser.add_argument("--max-factors", type=int, default=8)
    args = parser.parse_args()

    return pipeline_main(
        [
            "factor",
            "--evidence-catalog",
            args.evidence_catalog,
            "--max-factors",
            str(args.max_factors),
            "--output",
            args.output,
        ]
    )


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/tests/doe_pipeline_test.py
content: from __future__ import annotations

import json
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path


SCRIPTS_DIR = Path(__file__).resolve().parents[1]
PIPELINE = SCRIPTS_DIR / "doe_pipeline.py"
WRAPPERS = {
    "evidence": SCRIPTS_DIR / "evidence_pipeline.py",
    "factor": SCRIPTS_DIR / "patent_factor_extractor.py",
    "design": SCRIPTS_DIR / "doe_designer.py",
    "report": SCRIPTS_DIR / "doe_plan_report.py",
}
EXPECTED_REPORT_HEADINGS = [
    "1. Objective and Constraints",
    "2. Selected DOE Method and Rationale",
    "3. Run Sheet Summary",
    "4. Evidence Coverage Matrix",
    "5. Facts vs Inference vs Unknowns",
    "6. Next-round Criteria",
]


def run_script(script: Path, *args: str) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        [sys.executable, str(script), *args],
        cwd=str(SCRIPTS_DIR),
        capture_output=True,
        text=True,
        check=False,
    )


def write_json(path: Path, payload: object) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload), encoding="utf-8")


def write_text(path: Path, content: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")


class DoePipelineCliTests(unittest.TestCase):
    def setUp(self) -> None:
        self.tempdir = tempfile.TemporaryDirectory()
        self.root = Path(self.tempdir.name)
        self.inputs = self.root / "inputs"
        self.outputs = self.root / "outputs"
        self.outputs.mkdir(parents=True, exist_ok=True)
        self.search_input = self.inputs / "search_input.json"
        self.fetch_manifest = self.inputs / "fetch_manifest.json"
        self.context_json = self.inputs / "context.json"
        self._write_happy_path_inputs()

    def tearDown(self) -> None:
        self.tempdir.cleanup()

    def _write_happy_path_inputs(self) -> None:
        patent_text = (
            "Fed-batch fermentation method. Temperature 30-34 degC, pH 6.2-7.0, "
            "dissolved oxygen setpoint 25-55 %, and agitation 300-700 rpm "
            "improved titer significantly (p<0.05)."
        )
        paper_text = (
            "Bioreactor cultivation protocol. Temperature between 31 and 35 degC with "
            "pH 6.4-6.9, dissolved oxygen 30-50 %, and agitation 350-650 rpm "
            "improved yield and viability significantly."
        )
        web_text = (
            "Operator guidance notes that feed rate 2-4 ml/h can support higher productivity, "
            "but this source is supplemental."
        )
        patent_file = self.inputs / "patent.txt"
        paper_file = self.inputs / "paper.txt"
        web_file = self.inputs / "web.txt"
        write_text(patent_file, patent_text)
        write_text(paper_file, paper_text)
        write_text(web_file, web_text)

        write_json(
            self.search_input,
            {
                "objective": "Maximize titer while maintaining viability",
                "responses": ["titer", "viability"],
                "constraints": ["Maintain process robustness", "DO <= 70%"],
                "patents": [
                    {
                        "id": "PAT-001",
                        "title": "Fermentation patent",
                        "year": 2024,
                        "relevance_score": 0.95,
                    }
                ],
                "papers": [
                    {
                        "id": "PAP-001",
                        "title": "Bioreactor paper",
                        "year": 2023,
                        "relevance_score": 0.92,
                    }
                ],
                "web": [
                    {
                        "id": "WEB-001",
                        "title": "Operator note",
                        "year": 2025,
                        "relevance_score": 0.61,
                    }
                ],
            },
        )
        write_json(
            self.fetch_manifest,
            {
                "items": [
                    {"source_key": "PAT-001", "status": "success", "path": str(patent_file)},
                    {"source_key": "PAP-001", "status": "success", "path": str(paper_file)},
                    {"source_key": "WEB-001", "status": "success", "path": str(web_file)},
                ]
            },
        )
        write_json(
            self.context_json,
            {
                "objective": "Maximize titer while maintaining viability",
                "responses": ["titer", "viability"],
                "constraints": ["Maintain process robustness", "DO <= 70%"],
            },
        )

    def _artifact_paths(self) -> dict[str, Path]:
        return {
            "evidence": self.outputs / "evidence_catalog.json",
            "factors": self.outputs / "factor_hypotheses.json",
            "design": self.outputs / "doe_design.json",
            "run_sheet": self.outputs / "run_sheet.csv",
            "report": self.outputs / "doe_plan.md",
        }

    def test_stepwise_cli_happy_path_matches_contract(self) -> None:
        paths = self._artifact_paths()

        evidence_result = run_script(
            PIPELINE,
            "evidence",
            "--search-input",
            str(self.search_input),
            "--fetch-manifest",
            str(self.fetch_manifest),
            "--output",
            str(paths["evidence"]),
        )
        self.assertEqual(evidence_result.returncode, 0, msg=evidence_result.stderr)
        evidence_payload = json.loads(paths["evidence"].read_text(encoding="utf-8"))
        self.assertEqual(evidence_payload["gates"]["status"], "ready")

        factor_result = run_script(
            PIPELINE,
            "factor",
            "--evidence-catalog",
            str(paths["evidence"]),
            "--max-factors",
            "4",
            "--output",
            str(paths["factors"]),
        )
        self.assertEqual(factor_result.returncode, 0, msg=factor_result.stderr)
        factors_payload = json.loads(paths["factors"].read_text(encoding="utf-8"))
        self.assertEqual(factors_payload["summary"]["status"], "ready")
        self.assertGreaterEqual(factors_payload["summary"]["design_ready_factors"], 3)

        design_result = run_script(
            PIPELINE,
            "design",
            "--factors-json",
            str(paths["factors"]),
            "--design-type",
            "auto",
            "--phase",
            "screening",
            "--responses",
            "titer,viability",
            "--max-factors",
            "4",
            "--output-json",
            str(paths["design"]),
            "--output-csv",
            str(paths["run_sheet"]),
        )
        self.assertEqual(design_result.returncode, 0, msg=design_result.stderr)
        design_payload = json.loads(paths["design"].read_text(encoding="utf-8"))
        self.assertEqual(design_payload["response_metrics"], ["titer", "viability"])
        self.assertTrue(paths["run_sheet"].exists())

        report_result = run_script(
            PIPELINE,
            "report",
            "--context-json",
            str(self.context_json),
            "--evidence-catalog",
            str(paths["evidence"]),
            "--factors-json",
            str(paths["factors"]),
            "--design-json",
            str(paths["design"]),
            "--output",
            str(paths["report"]),
        )
        self.assertEqual(report_result.returncode, 0, msg=report_result.stderr)
        report_text = paths["report"].read_text(encoding="utf-8")
        headings = re.findall(r"^## (.+)$", report_text, flags=re.MULTILINE)
        self.assertEqual(headings, EXPECTED_REPORT_HEADINGS)

    def test_run_all_writes_full_workspace_outputs(self) -> None:
        run_all_dir = self.root / "run_all"
        result = run_script(
            PIPELINE,
            "run-all",
            "--search-input",
            str(self.search_input),
            "--fetch-manifest",
            str(self.fetch_manifest),
            "--context-json",
            str(self.context_json),
            "--output-dir",
            str(run_all_dir),
            "--max-factors",
            "4",
            "--responses",
            "titer,viability",
        )
        self.assertEqual(result.returncode, 0, msg=result.stderr)
        for filename in [
            "evidence_catalog.json",
            "factor_hypotheses.json",
            "doe_design.json",
            "run_sheet.csv",
            "doe_plan.md",
        ]:
            self.assertTrue((run_all_dir / filename).exists(), msg=filename)

    def test_factor_step_blocks_on_insufficient_evidence_without_synthetic_defaults(self) -> None:
        patent_only = self.root / "patent_only_search.json"
        manifest = self.root / "patent_only_manifest.json"
        patent_file = self.root / "single_patent.txt"
        evidence_path = self.root / "blocked_evidence.json"
        factor_path = self.root / "blocked_factors.json"

        write_text(
            patent_file,
            "Fermentation method. Temperature 32-34 degC improved yield significantly.",
        )
        write_json(
            patent_only,
            {
                "objective": "Improve yield",
                "responses": ["yield"],
                "constraints": [],
                "patents": [{"id": "PAT-ONLY", "title": "Only patent", "year": 2024, "relevance_score": 0.9}],
            },
        )
        write_json(
            manifest,
            {"items": [{"source_key": "PAT-ONLY", "status": "success", "path": str(patent_file)}]},
        )

        evidence_result = run_script(
            PIPELINE,
            "evidence",
            "--search-input",
            str(patent_only),
            "--fetch-manifest",
            str(manifest),
            "--output",
            str(evidence_path),
        )
        self.assertEqual(evidence_result.returncode, 0, msg=evidence_result.stderr)

        factor_result = run_script(
            PIPELINE,
            "factor",
            "--evidence-catalog",
            str(evidence_path),
            "--output",
            str(factor_path),
        )
        self.assertNotEqual(factor_result.returncode, 0)
        payload = json.loads(factor_path.read_text(encoding="utf-8"))
        self.assertEqual(payload["summary"]["status"], "blocked")
        self.assertEqual(payload["factors"], [])
        self.assertNotIn("temperature", factor_path.read_text(encoding="utf-8"))
        error_payload = json.loads(factor_result.stderr.strip())
        self.assertEqual(error_payload["error_code"], "DOE_EVIDENCE_INSUFFICIENT")

    def test_compatibility_wrappers_forward_to_unified_pipeline(self) -> None:
        paths = self._artifact_paths()

        evidence_result = run_script(
            WRAPPERS["evidence"],
            "--search-input",
            str(self.search_input),
            "--fetch-manifest",
            str(self.fetch_manifest),
            "--output",
            str(paths["evidence"]),
        )
        self.assertEqual(evidence_result.returncode, 0, msg=evidence_result.stderr)

        factor_result = run_script(
            WRAPPERS["factor"],
            "--evidence-catalog",
            str(paths["evidence"]),
            "--max-factors",
            "4",
            "--output",
            str(paths["factors"]),
        )
        self.assertEqual(factor_result.returncode, 0, msg=factor_result.stderr)

        design_result = run_script(
            WRAPPERS["design"],
            "--factors-json",
            str(paths["factors"]),
            "--responses",
            "titer,viability",
            "--max-factors",
            "4",
            "--output-json",
            str(paths["design"]),
            "--output-csv",
            str(paths["run_sheet"]),
        )
        self.assertEqual(design_result.returncode, 0, msg=design_result.stderr)

        report_result = run_script(
            WRAPPERS["report"],
            "--context-json",
            str(self.context_json),
            "--evidence-catalog",
            str(paths["evidence"]),
            "--factors-json",
            str(paths["factors"]),
            "--design-json",
            str(paths["design"]),
            "--output",
            str(paths["report"]),
        )
        self.assertEqual(report_result.returncode, 0, msg=report_result.stderr)
        self.assertTrue(paths["report"].exists())


if __name__ == "__main__":
    unittest.main()


path: scripts/doe_designer.py
content: #!/usr/bin/env python3
"""Compatibility wrapper: design step now lives in doe_pipeline.py."""

from __future__ import annotations

import argparse
from pathlib import Path
import sys

SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from doe_pipeline import main as pipeline_main  # noqa: E402


def main() -> int:
    parser = argparse.ArgumentParser(description="Generate DOE design and run sheet")
    parser.add_argument("--factors-json", required=True)
    parser.add_argument("--design-type", default="auto", choices=["auto", "pb", "ffd", "bbd", "ccd"])
    parser.add_argument("--phase", default="screening", choices=["screening", "optimization"])
    parser.add_argument("--resource-budget", type=int, default=0)
    parser.add_argument("--replicates", type=int, default=1)
    parser.add_argument("--center-points", type=int, default=3)
    parser.add_argument("--seed", type=int, default=42)
    parser.add_argument("--responses", default="yield,titer")
    parser.add_argument("--max-factors", type=int, default=6)
    parser.add_argument("--output-json", required=True)
    parser.add_argument("--output-csv", required=True)
    args = parser.parse_args()

    return pipeline_main(
        [
            "design",
            "--factors-json",
            args.factors_json,
            "--design-type",
            args.design_type,
            "--phase",
            args.phase,
            "--resource-budget",
            str(args.resource_budget),
            "--replicates",
            str(args.replicates),
            "--center-points",
            str(args.center_points),
            "--seed",
            str(args.seed),
            "--responses",
            args.responses,
            "--max-factors",
            str(args.max_factors),
            "--output-json",
            args.output_json,
            "--output-csv",
            args.output_csv,
        ]
    )


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/evidence_pipeline.py
content: #!/usr/bin/env python3
"""Compatibility wrapper: evidence step now lives in doe_pipeline.py."""

from __future__ import annotations

import argparse
from pathlib import Path
import sys

SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from doe_pipeline import main as pipeline_main  # noqa: E402


def main() -> int:
    parser = argparse.ArgumentParser(description="Build evidence catalog")
    parser.add_argument("--search-input", required=True)
    parser.add_argument("--fetch-manifest", required=True)
    parser.add_argument("--output", required=True)
    parser.add_argument("--top-k", type=int, default=12)
    args = parser.parse_args()

    return pipeline_main(
        [
            "evidence",
            "--search-input",
            args.search_input,
            "--fetch-manifest",
            args.fetch_manifest,
            "--top-k",
            str(args.top_k),
            "--output",
            args.output,
        ]
    )


if __name__ == "__main__":
    raise SystemExit(main())


path: skill.manifest.json
content: {
  "name": "doe-plan",
  "description": "Evidence-to-DOE workflow with stepwise CLI stages, traceable artifacts, and runtime metadata.",
  "runtime": {
    "python_version": "3.12",
    "dependencies": ["numpy", "pyDOE3"],
    "isolated_env": false
  },
  "mcp_dependencies": ["patsnap"],
  "mcp": {
    "mode": "recommended",
    "server": "patsnap",
    "tools": ["patsnap_search", "patsnap_fetch"],
    "setup_doc": "../mcp-setup/PATSNAP_MCP_SETUP.md"
  },
  "tests": ["cli"],
  "outputs": [
    "evidence_catalog.json",
    "factor_hypotheses.json",
    "doe_design.json",
    "run_sheet.csv",
    "doe_plan.md"
  ]
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)