Metadata-Version: 2.4
Name: expctl
Version: 0.3.0
Summary: Universal local-first experiment lifecycle manager inspired by Git
Author: CactysFedya
Maintainer: CactysFedya
License-Expression: MIT
Project-URL: Homepage, https://github.com/CactysFedya/expctl
Project-URL: Documentation, https://github.com/CactysFedya/expctl#readme
Project-URL: Issues, https://github.com/CactysFedya/expctl/issues
Project-URL: Changelog, https://github.com/CactysFedya/expctl/blob/main/CHANGELOG.md
Project-URL: Repository, https://github.com/CactysFedya/expctl
Project-URL: Discussions, https://github.com/CactysFedya/expctl/discussions
Keywords: experiments,experiment-tracking,reproducibility,research,git,cli,benchmark,mlops,robotics
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Version Control :: Git
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typer<1,>=0.16
Requires-Dist: rich<15,>=13
Provides-Extra: dev
Requires-Dist: build<2,>=1.2; extra == "dev"
Requires-Dist: pytest<10,>=8; extra == "dev"
Requires-Dist: twine<7,>=6; extra == "dev"
Dynamic: license-file

<p align="center">
  <img src="docs/assets/expctl-banner.svg" alt="expctl — Experiment Control" width="760">
</p>

<p align="center">
  <strong>A universal, local-first experiment lifecycle manager inspired by Git.</strong>
</p>

<p align="center">
  <a href="https://github.com/CactysFedya/expctl/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/CactysFedya/expctl/actions/workflows/ci.yml/badge.svg"></a>
  <a href="https://pypi.org/project/expctl/"><img alt="PyPI" src="https://img.shields.io/pypi/v/expctl"></a>
  <a href="https://www.python.org/"><img alt="Python 3.11+" src="https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white"></a>
  <a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/License-MIT-green.svg"></a>
</p>

<p align="center">
  English · <a href="docs/README.ru.md">Русская документация</a>
</p>

> Git tracks how code changes. **expctl tracks how experiments happen.**

`expctl` records experiments, immutable runs, changes, failures, retries, arbitrary results, artifacts, observations, and conclusions. It works for software, hardware, scientific, and manual experiments without embedding assumptions about a particular domain.

```text
EXP-... experiment
├── RUN-001 failed
│   └── CHG-001 fix
│       └── RUN-002 retry completed
├── RUN-003 completed · BEST
├── notes and next actions
└── derived experiments
```

## Why expctl?

A Git commit tells you which code existed. It does not tell you:

- which command or physical procedure was executed;
- whether the run failed, was interrupted, or produced incomplete outputs;
- which change led to a retry;
- which metrics, typed conditions, and artifacts were recorded;
- which result is currently considered best;
- what should happen next;
- whether the experiment can be transferred to another project.

`expctl` stores that missing experiment history while using Git when it is available.

## Principles

- **Universal:** no required ML, robotics, or scientific vocabulary.
- **Local-first:** SQLite plus human-readable JSON and Markdown records.
- **Git-aware, not Git-dependent:** software and manual experiments use the same core.
- **Immutable runs:** retry and resume create linked records rather than rewriting history.
- **Validated success:** exit code `0` can still become `incomplete` when required outputs are missing.
- **Extensible metadata:** free-form tags, arbitrary metrics, and typed custom fields.
- **Portable:** experiments can be exported and imported as ZIP bundles.

## Installation

For command-line applications, `pipx` is recommended:

```bash
pipx install expctl
```

Using pip is also supported:

```bash
python3 -m pip install expctl
```

Check the installation:

```bash
expctl --version
# expctl 0.3.0
```

## Quick start

```bash
mkdir my-experiment && cd my-experiment
expctl init --name my-project

expctl new \
  --name baseline \
  --goal "Check the current implementation" \
  --command "python3 task.py" \
  --metrics "out/metrics.json" \
  --required-file "out/result.bin" \
  --required-metric score \
  --tag baseline

expctl run
```

When code changes after a failed run:

```bash
expctl fix FAILED \
  -m "Fixed incorrect boundary handling" \
  --commit

expctl retry FAILED
```

## Navigation and history

Built-in references are resolved inside the current experiment:

```text
CURRENT_EXP  current selected experiment
LATEST       latest run of CURRENT_EXP
FAILED       latest unsuccessful run of CURRENT_EXP
BEST         selected best run of CURRENT_EXP
```

Custom aliases are supported:

```bash
expctl alias set baseline LATEST
expctl show baseline
```

Display a Git-inspired tree:

```bash
expctl log CURRENT_EXP --graph --metric score
```

Example:

```text
EXP-20260723-001  baseline  [completed] · BEST=RUN-20260723-002
└── RUN-20260723-001  run  [failed]
    └── CHG-20260723-001  fix  Fixed boundary handling
        └── RUN-20260723-002  retry  [completed] · score=0.91
```

## Compare and select results

```bash
expctl compare RUN-001 RUN-002 RUN-003 \
  --metric score \
  --metric latency \
  --field device
```

Choose the best run manually:

```bash
expctl best set RUN-003 -m "Best balance of quality and latency"
```

Or use one explicit numeric rule:

```bash
expctl best select CURRENT_EXP --metric latency --minimize
```

## Tags and typed fields

Tags remain free-form:

```bash
expctl tag add LATEST verified
expctl tag add CURRENT_EXP outdoor
```

Typed custom fields avoid domain-specific columns in the core:

```bash
expctl field set LATEST device "Raspberry Pi 5" --type string
expctl field set LATEST temperature 71.5 --type number --unit C
expctl field set CURRENT_EXP controlled true --type boolean
expctl field set CURRENT_EXP started 2026-07-23T14:30:00 --type datetime
```

Supported field types:

```text
string, number, integer, boolean, datetime, path, json
```

## Observations and next actions

Timestamped observations do not alter run status:

```bash
expctl note LATEST "Vibration appeared after five minutes" --kind observation
```

Minimal next actions stay intentionally smaller than a task manager:

```bash
expctl next add LATEST "Repeat outdoors"
expctl next list CURRENT_EXP
expctl next done ACT-...
```

## Export and import

Export metadata, logs, patches, and records:

```bash
expctl export CURRENT_EXP --output experiment.zip
```

Include copied artifact contents:

```bash
expctl export CURRENT_EXP \
  --output experiment-with-artifacts.zip \
  --include-artifacts
```

Import into another initialized project:

```bash
expctl init --name target-project
expctl import experiment-with-artifacts.zip
```

Imports preserve experiment, run, and change IDs and therefore reject collisions.

## Manual physical experiment

Git and a child command are optional:

```bash
expctl new \
  --name outdoor-range \
  --goal "Measure maximum stable range" \
  --required-metric max_range \
  --tag hardware

expctl run --manual

expctl result LATEST \
  --mark-completed \
  --metric max_range=42.7:m \
  --metric packet_loss=0.8:percent \
  --artifact measurement.txt \
  --copy
```

## Validation model

```text
exit code != 0
→ failed

exit code = 0, but a required file or metric is missing
→ incomplete

exit code = 0 and all configured checks pass
→ completed
```

## Commands in 0.3.0

| Area | Commands |
|---|---|
| Project | `init`, `config`, `doctor`, `status` |
| Experiments | `new`, `use`, `list`, `show`, `log`, `derive`, `archive` |
| Runs | `run`, `retry`, `resume`, `cancel`, `recover`, `compare` |
| Changes | `change`, `fix`, `diff` |
| Results | `result`, `validate`, `conclude`, `best` |
| Metadata | `tag`, `field`, `alias`, `note`, `next` |
| Portability | `export`, `import` |

## Development

```bash
git clone https://github.com/CactysFedya/expctl.git
cd expctl
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -e ".[dev]"
pytest
make demo
```

Expected for `0.3.0`:

```text
9 passed
expctl 0.3.0
```

## Database upgrade

Opening a `0.2.0` project with `0.3.0` automatically migrates the local database from schema version 2 to 3. Back up `.expctl/expctl.db` before upgrading important projects. Downgrading the migrated database to `0.2.0` is not supported. See [docs/UPGRADING.md](docs/UPGRADING.md).

## Security

`expctl` executes commands supplied by the user and can copy files into experiment bundles. Review commands and paths from untrusted projects before running or importing them. See [SECURITY.md](SECURITY.md).

## Project status

`0.3.0` is alpha software. The data model and CLI may evolve before `1.0.0`.

## Contributing

Bug reports, design proposals, documentation improvements, and pull requests are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md).

## License

MIT. See [LICENSE](LICENSE).
