adduct 0.2.1__tar.gz
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.
- adduct-0.2.1/.github/ISSUE_TEMPLATE/bug_report.yml +71 -0
- adduct-0.2.1/.github/ISSUE_TEMPLATE/config.yml +8 -0
- adduct-0.2.1/.github/ISSUE_TEMPLATE/false_positive.yml +79 -0
- adduct-0.2.1/.github/pull_request_template.md +21 -0
- adduct-0.2.1/.github/workflows/ci.yml +92 -0
- adduct-0.2.1/.github/workflows/release.yml +99 -0
- adduct-0.2.1/.gitignore +39 -0
- adduct-0.2.1/CHANGELOG.md +179 -0
- adduct-0.2.1/CODE_OF_CONDUCT.md +132 -0
- adduct-0.2.1/CONTRIBUTING.md +100 -0
- adduct-0.2.1/LICENSE +202 -0
- adduct-0.2.1/NOTICE +4 -0
- adduct-0.2.1/PKG-INFO +274 -0
- adduct-0.2.1/README.md +215 -0
- adduct-0.2.1/SECURITY.md +55 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/REPORT.md +400 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/figures/fig1_detector_fire_rates.pdf +0 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/figures/fig1_detector_fire_rates.png +0 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/figures/fig2_dataset_severity.pdf +0 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/figures/fig2_dataset_severity.png +0 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/figures/fig3_runtime.pdf +0 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/figures/fig3_runtime.png +0 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/paper/adduct-benchmark.pdf +0 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/paper/adduct-benchmark.tex +480 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/results/sweep_default.json +1718 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/results/sweep_full.json +1732 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/scripts/make_figures.py +259 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/scripts/requirements.txt +1 -0
- adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/scripts/run_sweep.py +118 -0
- adduct-0.2.1/benchmarks/README.md +69 -0
- adduct-0.2.1/pyproject.toml +216 -0
- adduct-0.2.1/scripts/hub_smoke.py +129 -0
- adduct-0.2.1/src/adduct/__init__.py +37 -0
- adduct-0.2.1/src/adduct/_arrays.py +26 -0
- adduct-0.2.1/src/adduct/_compat.py +39 -0
- adduct-0.2.1/src/adduct/_plugins.py +39 -0
- adduct-0.2.1/src/adduct/adapters/__init__.py +21 -0
- adduct-0.2.1/src/adduct/adapters/_arraysource.py +198 -0
- adduct-0.2.1/src/adduct/adapters/_mapping.py +213 -0
- adduct-0.2.1/src/adduct/adapters/_video.py +157 -0
- adduct-0.2.1/src/adduct/adapters/base.py +89 -0
- adduct-0.2.1/src/adduct/adapters/hdf5.py +205 -0
- adduct-0.2.1/src/adduct/adapters/lerobot.py +499 -0
- adduct-0.2.1/src/adduct/adapters/numpy_dir.py +107 -0
- adduct-0.2.1/src/adduct/adapters/registry.py +97 -0
- adduct-0.2.1/src/adduct/adapters/rlds.py +224 -0
- adduct-0.2.1/src/adduct/adapters/zarr_replay.py +134 -0
- adduct-0.2.1/src/adduct/analysis/__init__.py +45 -0
- adduct-0.2.1/src/adduct/analysis/confident_learning.py +72 -0
- adduct-0.2.1/src/adduct/analysis/embeddings.py +135 -0
- adduct-0.2.1/src/adduct/analysis/neighbors.py +171 -0
- adduct-0.2.1/src/adduct/analysis/robust.py +70 -0
- adduct-0.2.1/src/adduct/analysis/shapes.py +186 -0
- adduct-0.2.1/src/adduct/analysis/twosample.py +110 -0
- adduct-0.2.1/src/adduct/api.py +95 -0
- adduct-0.2.1/src/adduct/bench/__init__.py +18 -0
- adduct-0.2.1/src/adduct/bench/harness.py +166 -0
- adduct-0.2.1/src/adduct/calibrate/__init__.py +7 -0
- adduct-0.2.1/src/adduct/calibrate/collect.py +131 -0
- adduct-0.2.1/src/adduct/calibrate/conformal.py +111 -0
- adduct-0.2.1/src/adduct/calibrate/corpus.py +203 -0
- adduct-0.2.1/src/adduct/calibrate/dynamics_model.py +225 -0
- adduct-0.2.1/src/adduct/calibrate/fdr.py +68 -0
- adduct-0.2.1/src/adduct/calibrate/gate.py +229 -0
- adduct-0.2.1/src/adduct/cli.py +436 -0
- adduct-0.2.1/src/adduct/config.py +118 -0
- adduct-0.2.1/src/adduct/detectors/__init__.py +8 -0
- adduct-0.2.1/src/adduct/detectors/_common.py +166 -0
- adduct-0.2.1/src/adduct/detectors/base.py +118 -0
- adduct-0.2.1/src/adduct/detectors/causal.py +189 -0
- adduct-0.2.1/src/adduct/detectors/consistency.py +212 -0
- adduct-0.2.1/src/adduct/detectors/coverage.py +364 -0
- adduct-0.2.1/src/adduct/detectors/dynamics.py +207 -0
- adduct-0.2.1/src/adduct/detectors/integrity.py +475 -0
- adduct-0.2.1/src/adduct/detectors/kinematics.py +467 -0
- adduct-0.2.1/src/adduct/detectors/label.py +182 -0
- adduct-0.2.1/src/adduct/detectors/multimodality.py +200 -0
- adduct-0.2.1/src/adduct/detectors/policy_data.py +344 -0
- adduct-0.2.1/src/adduct/detectors/registry.py +107 -0
- adduct-0.2.1/src/adduct/detectors/scale.py +200 -0
- adduct-0.2.1/src/adduct/detectors/smoothness.py +177 -0
- adduct-0.2.1/src/adduct/detectors/stats.py +235 -0
- adduct-0.2.1/src/adduct/detectors/temporal.py +378 -0
- adduct-0.2.1/src/adduct/detectors/vision.py +418 -0
- adduct-0.2.1/src/adduct/encoders/__init__.py +31 -0
- adduct-0.2.1/src/adduct/encoders/base.py +35 -0
- adduct-0.2.1/src/adduct/encoders/dino.py +82 -0
- adduct-0.2.1/src/adduct/encoders/tiled.py +49 -0
- adduct-0.2.1/src/adduct/engine.py +209 -0
- adduct-0.2.1/src/adduct/hub.py +122 -0
- adduct-0.2.1/src/adduct/ir/__init__.py +51 -0
- adduct-0.2.1/src/adduct/ir/episode.py +133 -0
- adduct-0.2.1/src/adduct/ir/schema.py +180 -0
- adduct-0.2.1/src/adduct/policy/__init__.py +9 -0
- adduct-0.2.1/src/adduct/policy/loader.py +321 -0
- adduct-0.2.1/src/adduct/policy/target.py +56 -0
- adduct-0.2.1/src/adduct/profile/__init__.py +14 -0
- adduct-0.2.1/src/adduct/profile/action_space.py +132 -0
- adduct-0.2.1/src/adduct/profile/dataset_profile.py +224 -0
- adduct-0.2.1/src/adduct/profile/episode_reservoir.py +130 -0
- adduct-0.2.1/src/adduct/profile/online.py +206 -0
- adduct-0.2.1/src/adduct/py.typed +0 -0
- adduct-0.2.1/src/adduct/report/__init__.py +31 -0
- adduct-0.2.1/src/adduct/report/base.py +22 -0
- adduct-0.2.1/src/adduct/report/html.py +201 -0
- adduct-0.2.1/src/adduct/report/messages.py +143 -0
- adduct-0.2.1/src/adduct/report/model.py +222 -0
- adduct-0.2.1/src/adduct/report/sarif.py +231 -0
- adduct-0.2.1/src/adduct/report/tty.py +124 -0
- adduct-0.2.1/src/adduct/synth/__init__.py +21 -0
- adduct-0.2.1/src/adduct/synth/pipeline.py +198 -0
- adduct-0.2.1/src/adduct/version.py +17 -0
- adduct-0.2.1/tests/_synth.py +897 -0
- adduct-0.2.1/tests/test_accuracy_research.py +116 -0
- adduct-0.2.1/tests/test_adapters_p3.py +246 -0
- adduct-0.2.1/tests/test_advanced.py +134 -0
- adduct-0.2.1/tests/test_bench_harness.py +125 -0
- adduct-0.2.1/tests/test_benchmark.py +498 -0
- adduct-0.2.1/tests/test_calibration_gate.py +475 -0
- adduct-0.2.1/tests/test_catalogue_honesty.py +124 -0
- adduct-0.2.1/tests/test_cli.py +91 -0
- adduct-0.2.1/tests/test_cli_errors.py +137 -0
- adduct-0.2.1/tests/test_config.py +48 -0
- adduct-0.2.1/tests/test_corpus_p4.py +85 -0
- adduct-0.2.1/tests/test_corrupt_data_robustness.py +199 -0
- adduct-0.2.1/tests/test_determinism.py +17 -0
- adduct-0.2.1/tests/test_dod.py +174 -0
- adduct-0.2.1/tests/test_episode_reservoir.py +169 -0
- adduct-0.2.1/tests/test_faults.py +168 -0
- adduct-0.2.1/tests/test_faults_p2.py +238 -0
- adduct-0.2.1/tests/test_faults_p3.py +236 -0
- adduct-0.2.1/tests/test_hub.py +112 -0
- adduct-0.2.1/tests/test_lag_conventions.py +298 -0
- adduct-0.2.1/tests/test_lerobot.py +228 -0
- adduct-0.2.1/tests/test_optional_backends.py +287 -0
- adduct-0.2.1/tests/test_pipeline.py +39 -0
- adduct-0.2.1/tests/test_policy_p3.py +193 -0
- adduct-0.2.1/tests/test_profile.py +39 -0
- adduct-0.2.1/tests/test_registry.py +108 -0
- adduct-0.2.1/tests/test_rlds_adapter.py +305 -0
- adduct-0.2.1/tests/test_roundtrip.py +34 -0
- adduct-0.2.1/tests/test_score.py +257 -0
- adduct-0.2.1/tests/test_streaming_scale.py +213 -0
- adduct-0.2.1/tests/test_triage.py +84 -0
- adduct-0.2.1/tests/test_trust_p4.py +202 -0
- adduct-0.2.1/tests/test_video_lerobot.py +313 -0
- adduct-0.2.1/uv.lock +3930 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Adduct crashed, failed to read a dataset, or did something clearly wrong.
|
|
3
|
+
title: "[bug] "
|
|
4
|
+
labels: ["bug"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
Thanks for reporting. The three version fields below are required because without
|
|
10
|
+
them almost every bug report is unactionable — we cannot tell a fixed bug from a
|
|
11
|
+
live one, or a format difference from a code difference.
|
|
12
|
+
|
|
13
|
+
**If adduct flagged something that is actually fine**, that is a false positive, not
|
|
14
|
+
a bug — please use the *False positive* template instead. It goes into detector
|
|
15
|
+
calibration rather than the bug queue.
|
|
16
|
+
|
|
17
|
+
- type: textarea
|
|
18
|
+
id: what-happened
|
|
19
|
+
attributes:
|
|
20
|
+
label: What happened?
|
|
21
|
+
description: What did you run, what did you expect, and what did you get instead?
|
|
22
|
+
placeholder: |
|
|
23
|
+
I ran `adduct scan ./my_data` and it exited with a traceback instead of a report.
|
|
24
|
+
validations:
|
|
25
|
+
required: true
|
|
26
|
+
|
|
27
|
+
- type: input
|
|
28
|
+
id: dataset
|
|
29
|
+
attributes:
|
|
30
|
+
label: Dataset
|
|
31
|
+
description: >
|
|
32
|
+
The Hugging Face repo id if it is public (e.g. `lerobot/pusht`) — that lets us
|
|
33
|
+
reproduce it exactly. If it is private, describe the format and layout instead.
|
|
34
|
+
placeholder: "lerobot/pusht — or: private LeRobot v2.1, 340 episodes, SO-101, 2 cameras"
|
|
35
|
+
validations:
|
|
36
|
+
required: true
|
|
37
|
+
|
|
38
|
+
- type: input
|
|
39
|
+
id: adduct-version
|
|
40
|
+
attributes:
|
|
41
|
+
label: Adduct version
|
|
42
|
+
description: "Output of `adduct --version`"
|
|
43
|
+
placeholder: "adduct 0.1.0"
|
|
44
|
+
validations:
|
|
45
|
+
required: true
|
|
46
|
+
|
|
47
|
+
- type: input
|
|
48
|
+
id: python-version
|
|
49
|
+
attributes:
|
|
50
|
+
label: Python version and OS
|
|
51
|
+
description: "Output of `python --version`, plus your OS"
|
|
52
|
+
placeholder: "Python 3.11.9, Ubuntu 22.04"
|
|
53
|
+
validations:
|
|
54
|
+
required: true
|
|
55
|
+
|
|
56
|
+
- type: textarea
|
|
57
|
+
id: output
|
|
58
|
+
attributes:
|
|
59
|
+
label: Full output
|
|
60
|
+
description: >
|
|
61
|
+
The complete terminal output, including any traceback. This is automatically
|
|
62
|
+
formatted as code, so no backticks needed.
|
|
63
|
+
render: shell
|
|
64
|
+
|
|
65
|
+
- type: checkboxes
|
|
66
|
+
id: checks
|
|
67
|
+
attributes:
|
|
68
|
+
label: Before submitting
|
|
69
|
+
options:
|
|
70
|
+
- label: I checked that no existing issue already reports this.
|
|
71
|
+
required: true
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
blank_issues_enabled: true
|
|
2
|
+
contact_links:
|
|
3
|
+
- name: Security vulnerability
|
|
4
|
+
url: https://github.com/prabhu-gopal/adduct/security/advisories/new
|
|
5
|
+
about: Please report security issues privately, never as a public issue. See SECURITY.md.
|
|
6
|
+
- name: Unsupported dataset format
|
|
7
|
+
url: https://github.com/prabhu-gopal/adduct/issues/new?labels=format-request&title=%5Bformat%5D+
|
|
8
|
+
about: Want adduct to read a format it does not support yet? Tell us which, and how common it is.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
name: False positive
|
|
2
|
+
description: Adduct flagged something on your data that is actually fine.
|
|
3
|
+
title: "[false positive] "
|
|
4
|
+
labels: ["false-positive", "calibration"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
**This is the most valuable report you can send us.**
|
|
10
|
+
|
|
11
|
+
Adduct's only real asset is that its findings are trustworthy. A detector that
|
|
12
|
+
cries wolf is worse than no detector, and we cannot find those on synthetic data —
|
|
13
|
+
we need yours. Every report here goes directly into threshold calibration.
|
|
14
|
+
|
|
15
|
+
You do **not** need to share your dataset. The detector id, the numbers adduct
|
|
16
|
+
reported, and your explanation of why it is wrong is enough to act on.
|
|
17
|
+
|
|
18
|
+
- type: input
|
|
19
|
+
id: detector-id
|
|
20
|
+
attributes:
|
|
21
|
+
label: Detector ID
|
|
22
|
+
description: >
|
|
23
|
+
The bracketed id printed with the finding, e.g. `[stats.dead_dimension]`. Run
|
|
24
|
+
`adduct explain <id>` to see what it thinks it is measuring.
|
|
25
|
+
placeholder: "stats.jitter_outlier"
|
|
26
|
+
validations:
|
|
27
|
+
required: true
|
|
28
|
+
|
|
29
|
+
- type: textarea
|
|
30
|
+
id: the-finding
|
|
31
|
+
attributes:
|
|
32
|
+
label: The finding, as adduct printed it
|
|
33
|
+
description: Copy the whole block, including the measured value and the episode count.
|
|
34
|
+
render: shell
|
|
35
|
+
validations:
|
|
36
|
+
required: true
|
|
37
|
+
|
|
38
|
+
- type: textarea
|
|
39
|
+
id: why-wrong
|
|
40
|
+
attributes:
|
|
41
|
+
label: Why is this wrong?
|
|
42
|
+
description: >
|
|
43
|
+
What does adduct not know about your setup? This is the part we cannot guess.
|
|
44
|
+
placeholder: |
|
|
45
|
+
Dimension 6 is a binary gripper open/close, so its variance is supposed to be near
|
|
46
|
+
zero between grasps. It is not a dead dimension — it is doing its job.
|
|
47
|
+
validations:
|
|
48
|
+
required: true
|
|
49
|
+
|
|
50
|
+
- type: textarea
|
|
51
|
+
id: dataset-context
|
|
52
|
+
attributes:
|
|
53
|
+
label: Dataset context
|
|
54
|
+
description: >
|
|
55
|
+
Robot/embodiment, control rate, action space (joint positions? EEF deltas?),
|
|
56
|
+
episode count, and the Hugging Face repo id if it is public.
|
|
57
|
+
placeholder: |
|
|
58
|
+
SO-101, 30 Hz, 6-DoF joint positions + binary gripper, 220 episodes, private.
|
|
59
|
+
validations:
|
|
60
|
+
required: true
|
|
61
|
+
|
|
62
|
+
- type: input
|
|
63
|
+
id: adduct-version
|
|
64
|
+
attributes:
|
|
65
|
+
label: Adduct version
|
|
66
|
+
description: "Output of `adduct --version`"
|
|
67
|
+
placeholder: "adduct 0.1.0"
|
|
68
|
+
validations:
|
|
69
|
+
required: true
|
|
70
|
+
|
|
71
|
+
- type: textarea
|
|
72
|
+
id: json
|
|
73
|
+
attributes:
|
|
74
|
+
label: Report JSON (optional, but very helpful)
|
|
75
|
+
description: >
|
|
76
|
+
`adduct scan <path> --json report.json` gives us the exact measured values and
|
|
77
|
+
thresholds. Attach it or paste the relevant finding. It contains no raw data from
|
|
78
|
+
your dataset — only statistics.
|
|
79
|
+
render: json
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## What does this change?
|
|
2
|
+
|
|
3
|
+
<!-- One or two sentences. The "why" matters more than the "what". -->
|
|
4
|
+
|
|
5
|
+
## Why?
|
|
6
|
+
|
|
7
|
+
<!-- What problem does this solve? Link the issue if there is one. -->
|
|
8
|
+
|
|
9
|
+
## Checklist
|
|
10
|
+
|
|
11
|
+
- [ ] Commits are signed off (`git commit -s`) — see [CONTRIBUTING.md](../CONTRIBUTING.md)
|
|
12
|
+
- [ ] `ruff check .`, `mypy`, and `pytest` pass locally
|
|
13
|
+
- [ ] Added a test that fails before this change and passes after
|
|
14
|
+
- [ ] Updated `CHANGELOG.md` under `## [Unreleased]` if this is user-visible
|
|
15
|
+
|
|
16
|
+
### If this adds or changes a detector
|
|
17
|
+
|
|
18
|
+
- [ ] The mechanism sentence says *why* the defect degrades a trained policy
|
|
19
|
+
- [ ] Linked evidence that the defect is real (issue, paper, or training run)
|
|
20
|
+
- [ ] Added a fault-injection scenario to the benchmark
|
|
21
|
+
- [ ] Measured recall/precision are in the PR description
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
name: py${{ matrix.python-version }} · ${{ matrix.os }}
|
|
19
|
+
runs-on: ${{ matrix.os }}
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
os: [ubuntu-latest, macos-latest]
|
|
24
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
- name: Install uv
|
|
29
|
+
uses: astral-sh/setup-uv@v5
|
|
30
|
+
with:
|
|
31
|
+
enable-cache: true
|
|
32
|
+
|
|
33
|
+
- name: Install the project
|
|
34
|
+
run: uv sync --extra dev --python ${{ matrix.python-version }}
|
|
35
|
+
|
|
36
|
+
- name: Lint
|
|
37
|
+
run: uv run ruff check .
|
|
38
|
+
|
|
39
|
+
- name: Format check
|
|
40
|
+
run: uv run ruff format --check .
|
|
41
|
+
|
|
42
|
+
- name: Type check
|
|
43
|
+
run: uv run mypy
|
|
44
|
+
|
|
45
|
+
- name: Test
|
|
46
|
+
# The suite is fully synthetic: no network, no GPU, no dataset downloads.
|
|
47
|
+
run: uv run pytest -q
|
|
48
|
+
|
|
49
|
+
# The CLI is what users actually touch, and a working test suite has never once caught a
|
|
50
|
+
# broken entry point or a missing runtime dependency. This installs the built wheel into a
|
|
51
|
+
# clean environment and runs the binary, which is the only way to catch that.
|
|
52
|
+
smoke:
|
|
53
|
+
name: installed-wheel smoke test
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
|
|
58
|
+
- name: Install uv
|
|
59
|
+
uses: astral-sh/setup-uv@v5
|
|
60
|
+
|
|
61
|
+
- name: Build the wheel
|
|
62
|
+
run: uv build
|
|
63
|
+
|
|
64
|
+
- name: Verify the wheel contains the package
|
|
65
|
+
run: |
|
|
66
|
+
ls -R dist/
|
|
67
|
+
python -c "
|
|
68
|
+
import zipfile, glob, sys
|
|
69
|
+
wheel = glob.glob('dist/*.whl')[0]
|
|
70
|
+
names = zipfile.ZipFile(wheel).namelist()
|
|
71
|
+
assert any(n.startswith('adduct/') for n in names), 'no adduct/ inside the wheel'
|
|
72
|
+
print(f'{wheel}: {len(names)} files, package present')
|
|
73
|
+
"
|
|
74
|
+
|
|
75
|
+
- name: Install into a clean environment and run it
|
|
76
|
+
run: |
|
|
77
|
+
python -m venv /tmp/fresh
|
|
78
|
+
/tmp/fresh/bin/pip install --quiet dist/*.whl
|
|
79
|
+
/tmp/fresh/bin/adduct --version
|
|
80
|
+
/tmp/fresh/bin/adduct --help
|
|
81
|
+
/tmp/fresh/bin/adduct list-detectors | head -5
|
|
82
|
+
/tmp/fresh/bin/adduct explain stats.dead_dimension
|
|
83
|
+
|
|
84
|
+
- name: Errors are messages, not tracebacks
|
|
85
|
+
run: |
|
|
86
|
+
set +e
|
|
87
|
+
out=$(/tmp/fresh/bin/adduct scan ./definitely-not-a-dataset 2>&1)
|
|
88
|
+
code=$?
|
|
89
|
+
set -e
|
|
90
|
+
echo "$out"
|
|
91
|
+
[ "$code" -eq 2 ] || { echo "expected exit 2 for a usage error, got $code"; exit 1; }
|
|
92
|
+
case "$out" in *Traceback*) echo "a traceback reached the user"; exit 1 ;; esac
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Tag-triggered publish. Never publish from a laptop: the tag is the only thing that can
|
|
4
|
+
# ship a version, so what is on PyPI always corresponds to a commit anyone can check out.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
inputs:
|
|
10
|
+
test_pypi:
|
|
11
|
+
description: "Publish to TestPyPI instead of PyPI"
|
|
12
|
+
type: boolean
|
|
13
|
+
default: true
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
build:
|
|
20
|
+
name: Build distributions
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Install uv
|
|
26
|
+
uses: astral-sh/setup-uv@v5
|
|
27
|
+
|
|
28
|
+
- name: Build
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Show what is in the distributions
|
|
32
|
+
# If `adduct/` is not inside the wheel, pyproject.toml is wrong and the upload
|
|
33
|
+
# would silently ship an empty package.
|
|
34
|
+
run: |
|
|
35
|
+
ls -R dist/
|
|
36
|
+
python -m zipfile -l dist/*.whl | head -20
|
|
37
|
+
|
|
38
|
+
- uses: actions/upload-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: dist
|
|
41
|
+
path: dist/
|
|
42
|
+
|
|
43
|
+
publish-testpypi:
|
|
44
|
+
name: Publish to TestPyPI
|
|
45
|
+
needs: build
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
if: github.event_name == 'workflow_dispatch' && inputs.test_pypi
|
|
48
|
+
environment:
|
|
49
|
+
name: testpypi
|
|
50
|
+
url: https://test.pypi.org/p/adduct
|
|
51
|
+
permissions:
|
|
52
|
+
# Required for Trusted Publishing: this is what lets uv mint a short-lived OIDC
|
|
53
|
+
# token instead of us storing a long-lived PyPI API token in GitHub Secrets.
|
|
54
|
+
id-token: write
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/download-artifact@v4
|
|
57
|
+
with:
|
|
58
|
+
name: dist
|
|
59
|
+
path: dist/
|
|
60
|
+
- name: Install uv
|
|
61
|
+
uses: astral-sh/setup-uv@v5
|
|
62
|
+
- name: Publish
|
|
63
|
+
run: uv publish --trusted-publishing always --publish-url https://test.pypi.org/legacy/
|
|
64
|
+
|
|
65
|
+
publish-pypi:
|
|
66
|
+
name: Publish to PyPI
|
|
67
|
+
needs: build
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
70
|
+
environment:
|
|
71
|
+
name: pypi
|
|
72
|
+
url: https://pypi.org/p/adduct
|
|
73
|
+
permissions:
|
|
74
|
+
id-token: write
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/download-artifact@v4
|
|
77
|
+
with:
|
|
78
|
+
name: dist
|
|
79
|
+
path: dist/
|
|
80
|
+
- name: Install uv
|
|
81
|
+
uses: astral-sh/setup-uv@v5
|
|
82
|
+
- name: Publish
|
|
83
|
+
run: uv publish --trusted-publishing always
|
|
84
|
+
|
|
85
|
+
github-release:
|
|
86
|
+
name: Attach distributions to the GitHub release
|
|
87
|
+
needs: publish-pypi
|
|
88
|
+
runs-on: ubuntu-latest
|
|
89
|
+
permissions:
|
|
90
|
+
contents: write
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/download-artifact@v4
|
|
93
|
+
with:
|
|
94
|
+
name: dist
|
|
95
|
+
path: dist/
|
|
96
|
+
- uses: softprops/action-gh-release@v2
|
|
97
|
+
with:
|
|
98
|
+
files: dist/*
|
|
99
|
+
generate_release_notes: true
|
adduct-0.2.1/.gitignore
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# Environments
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
.env
|
|
14
|
+
|
|
15
|
+
# Tooling caches
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.coverage
|
|
20
|
+
htmlcov/
|
|
21
|
+
coverage.xml
|
|
22
|
+
|
|
23
|
+
# Editors / OS
|
|
24
|
+
.vscode/
|
|
25
|
+
.idea/
|
|
26
|
+
.DS_Store
|
|
27
|
+
|
|
28
|
+
# Adduct scan artifacts (never commit real reports)
|
|
29
|
+
*_report.html
|
|
30
|
+
*_report.json
|
|
31
|
+
adduct-report.*
|
|
32
|
+
|
|
33
|
+
# Internal design docs: business strategy, roadmap, licensing/moat reasoning.
|
|
34
|
+
# Kept locally for reference, deliberately never pushed to the public repo.
|
|
35
|
+
/docs/
|
|
36
|
+
|
|
37
|
+
# The local working playbook (branch rules, CI checks, release process). Kept out of the
|
|
38
|
+
# public repo by choice, not because it's sensitive — see the decision in chat history.
|
|
39
|
+
/CLAUDE.md
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
The **report schema** is versioned separately from the package — see `schema_version` in
|
|
8
|
+
any `--json` output. It changes only when the serialized report shape changes.
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.2.1] — 2026-09-01
|
|
13
|
+
|
|
14
|
+
The first release published under the name `adduct`.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **A default scan now says that the policy↔data checks were skipped**, and names the flags
|
|
19
|
+
that unlock them (`--target bc|act|diffusion|openvla|pi0|octo`, or `--policy <checkpoint>`).
|
|
20
|
+
Those five checks need a model to compare against, so staying silent without one is
|
|
21
|
+
correct — but saying nothing made the flags undiscoverable: the only way to learn the
|
|
22
|
+
checks existed was to read `--help`. The line appears exactly when none of them ran,
|
|
23
|
+
including on a clean dataset, which is the most misleading moment to stay quiet about
|
|
24
|
+
checks that never ran.
|
|
25
|
+
|
|
26
|
+
## [0.2.0] — 2026-08-27
|
|
27
|
+
|
|
28
|
+
Published measured evidence on real public data, and acted on what it showed. Every
|
|
29
|
+
detector-facing change below is driven by that measurement rather than by taste.
|
|
30
|
+
|
|
31
|
+
**Upgrading from 0.1.0.** A default scan now returns fewer findings, and that is the point.
|
|
32
|
+
`dynamics.inverse_residual` no longer runs by default because it fired on 100% of a
|
|
33
|
+
20-dataset public corpus; across that corpus the change takes 23 HIGH findings down to 13.
|
|
34
|
+
If you gate CI on `--fail-on HIGH`, expect fewer failures, not more. Nothing is deleted:
|
|
35
|
+
`--all` runs every detector, and `--json`, `--sarif` and `--html` are unchanged and
|
|
36
|
+
complete. Scans should also be quiet now — the numpy and scikit-learn warnings some users
|
|
37
|
+
saw on stderr are fixed at source.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`benchmarks/` — published measured evidence on real public data.** The first run,
|
|
42
|
+
`benchmarks/2026-08-26-lerobot-20-v0.1.0/`, scans 20 curated public LeRobot datasets
|
|
43
|
+
(4,342 episodes, 545,964 frames, 2–40 action dims, 5–50 Hz, sim and real, 14 of them
|
|
44
|
+
Open X-Embodiment conversions): 20/20 parsed without error in 22.3 s on a laptop CPU,
|
|
45
|
+
producing 189 findings (23 HIGH, 121 MEDIUM, 45 LOW). Of the 46 default detectors, 27
|
|
46
|
+
fired at least once, 19 never fired, and only 4 ever reached HIGH. Ships the raw sweep
|
|
47
|
+
JSON for both the uncapped and the default triage pass, a report, the sweep and figure
|
|
48
|
+
scripts, and a LaTeX paper. The unit suite measures recall on synthetic fixtures; this
|
|
49
|
+
measures how often detectors complain about healthy real data, which the suite cannot.
|
|
50
|
+
- **Measured: the default 300-episode triage cap does not change the conclusions** on this
|
|
51
|
+
corpus. Comparing a capped scan against one that reads every episode, all 23 HIGH
|
|
52
|
+
findings are identical, as are the counts of detectors that fired and detectors reaching
|
|
53
|
+
HIGH; 2 MEDIUM findings out of 189 differ, on the 3 datasets that the cap subsamples.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **Scans no longer leak numerical warnings to stderr.** `LinAlgWarning: Ill-conditioned
|
|
58
|
+
matrix` and numpy `divide by zero` / `overflow` / `invalid value` warnings from
|
|
59
|
+
scikit-learn could appear during an ordinary `adduct scan`. The ill-conditioning had a
|
|
60
|
+
real cause: the dynamics fits stack consecutive states, which are collinear by
|
|
61
|
+
construction, under a ridge penalty of 1e-6 that left that collinearity essentially
|
|
62
|
+
unregularized. Features are now standardized per fold and the penalty is 1.0, and the
|
|
63
|
+
solve **abstains** rather than returning a residual from a fit LAPACK flags as
|
|
64
|
+
unreliable. The remaining warnings are IEEE flags raised inside BLAS during clustering,
|
|
65
|
+
on input already validated as finite; they are suppressed at one documented boundary in
|
|
66
|
+
the engine, with `ADDUCT_SHOW_NUMERIC_WARNINGS=1` to restore them for debugging. Genuine
|
|
67
|
+
non-finite data is still reported by `integrity.nan_inf`. Measured across the 20-dataset
|
|
68
|
+
benchmark: 42 warning lines before, 0 after.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- **`dynamics.inverse_residual` no longer runs in a default scan.** Measured on the
|
|
73
|
+
20-dataset benchmark: it fires on **100%** of the corpus (20/20) and reports HIGH on 50%
|
|
74
|
+
(10/20), and reaches the report's visible top-5 on 60% of datasets, more often than either
|
|
75
|
+
detector already excluded. Its HIGH rate is lower than theirs; the 100% fire rate is what
|
|
76
|
+
decides it, because a detector that fires on every curated public dataset cannot
|
|
77
|
+
discriminate whatever severity it attaches. Excluding it takes the corpus from 23 HIGH
|
|
78
|
+
findings to 13 and the datasets carrying at least one HIGH from 17 of 20 to 11 of 20.
|
|
79
|
+
Nothing is deleted: it stays fully implemented and reachable with `--all`. One explanation
|
|
80
|
+
was tested and rejected first (see Fixed, below); the two that survive are confounded in
|
|
81
|
+
that corpus, so recalibration waits on a corpus of natively-recorded community datasets.
|
|
82
|
+
`dynamics.forward_residual` is the next candidate and is deliberately **not** excluded
|
|
83
|
+
yet: it has the highest top-5 visibility of any detector (75%) but never reports HIGH.
|
|
84
|
+
- **A default `adduct scan` now details the top 5 findings instead of 6**, and names the
|
|
85
|
+
flag that carries the rest (`--html` or `--json`, never `--all`, which adds the
|
|
86
|
+
held-back over-reporting detectors). The benchmark measured a median of 9.5 findings per
|
|
87
|
+
dataset with no dataset ever coming back clean; at that density the terminal is a triage
|
|
88
|
+
surface, not the full record. Nothing is dropped from any machine-readable output.
|
|
89
|
+
- `smoothness.discontinuity_jump` and `integrity.declared_mismatch` (see "Known
|
|
90
|
+
limitations" under 0.1.0) no longer run in a default scan. Measured on the same
|
|
91
|
+
20-dataset sweep: when either fired, the report's own ranking (severity × blast radius)
|
|
92
|
+
put it in the visible top-6 findings 13 of 16 times, winning the #1 or #2 slot in 6 of
|
|
93
|
+
those — a first-time user's first impression was disproportionately likely to be one of
|
|
94
|
+
the two things already known to probably be wrong. Nothing is deleted, degraded, or
|
|
95
|
+
hidden: both are fully implemented and reachable with the new `--all` flag
|
|
96
|
+
(`adduct.scan(..., all_detectors=True)` in the Python API), and this default list lives
|
|
97
|
+
at `adduct.detectors.registry.DEFAULT_EXCLUDED`.
|
|
98
|
+
|
|
99
|
+
### Known limitations
|
|
100
|
+
|
|
101
|
+
- **Six detectors are effectively always-on** and their thresholds are not yet trustworthy.
|
|
102
|
+
Measured across the 20-dataset benchmark: `dynamics.inverse_residual` 100%,
|
|
103
|
+
`dynamics.forward_residual` 90%, `smoothness.jerk_outlier` 85%, `smoothness.curvature`
|
|
104
|
+
75%, `smoothness.path_efficiency` 70%, `temporal.non_markovian_pause` 70%. A detector
|
|
105
|
+
firing on 85% of curated public data carries almost no information whatever severity it
|
|
106
|
+
attaches, and these six are the bulk of a report's length. Recalibration needs a corpus
|
|
107
|
+
that represents the intended user, which the current one does not (see below).
|
|
108
|
+
- **`dynamics.inverse_residual`'s HIGH severity is not yet trustworthy** (HIGH on 50% of
|
|
109
|
+
the benchmark). One hypothesis was tested and rejected: fixing the ill-conditioned ridge
|
|
110
|
+
solve eliminated the numerical fault but left the fire rate and HIGH rate unchanged. The
|
|
111
|
+
two surviving explanations, control rate and format-conversion provenance, are perfectly
|
|
112
|
+
confounded in that corpus and cannot be separated there. See
|
|
113
|
+
`benchmarks/2026-08-26-lerobot-20-v0.1.0/REPORT.md` §6.
|
|
114
|
+
- **The benchmark corpus is not the population adduct is for.** 14 of its 20 datasets are
|
|
115
|
+
Open X-Embodiment conversions at 5 Hz, against an intended user recording natively at
|
|
116
|
+
30 Hz. The HIGH rate is 64% on the former and 17% on the latter, so a share of those
|
|
117
|
+
findings may reflect format conversion rather than data quality.
|
|
118
|
+
- **`--fpr` is inert out of the box, and public metadata makes it hard to fix.** The
|
|
119
|
+
conformal gate needs a calibration corpus keyed by embodiment (Mondrian), and none ships
|
|
120
|
+
with the package. The sweep also found that 18 of 20 public LeRobot datasets declare
|
|
121
|
+
`robot_type: "unknown"`, so an embodiment-keyed taxonomy built from Hub metadata
|
|
122
|
+
collapses to the single wildcard bucket and forfeits group-conditional validity.
|
|
123
|
+
|
|
124
|
+
## [0.1.0] — 2026-08-25
|
|
125
|
+
|
|
126
|
+
First public release.
|
|
127
|
+
|
|
128
|
+
### Added
|
|
129
|
+
|
|
130
|
+
- `adduct scan <path>` — analyze a robot-learning dataset and report the defects that hurt
|
|
131
|
+
training, with severity, affected episodes, the measured value, the mechanism, and a fix.
|
|
132
|
+
- **Hugging Face Hub support**: `adduct scan lerobot/pusht` fetches `meta/` and `data/`
|
|
133
|
+
(never video) and scans the local snapshot. This is the only network call in the tool.
|
|
134
|
+
- **Formats**: LeRobot v2.1 and v3.0 (autodetected), RLDS/Open-X, robomimic and raw HDF5,
|
|
135
|
+
Zarr replay buffers, NumPy directories.
|
|
136
|
+
- **Machine-readable output**: `--json` (versioned `schema_version`), `--sarif` (SARIF
|
|
137
|
+
2.1.0 for GitHub code scanning), and a self-contained `--html` report.
|
|
138
|
+
- **CI gating**: `--ci --fail-on <severity>` exits non-zero only when you ask it to.
|
|
139
|
+
- **Calibration**: `adduct calibrate` builds a conformal FDR corpus from your own
|
|
140
|
+
known-good data, after which `--fpr` governs the covered detector gates.
|
|
141
|
+
- `adduct list-detectors`, `adduct explain <detector-id>`, and `adduct init`.
|
|
142
|
+
- Adapters and detectors are plugins discovered through entry points — the same mechanism
|
|
143
|
+
the built-ins use.
|
|
144
|
+
|
|
145
|
+
### Fixed
|
|
146
|
+
|
|
147
|
+
- `dynamics.inverse_residual` reported HIGH unconditionally, whatever the measured extent.
|
|
148
|
+
Across 20 curated public LeRobot datasets it fired at HIGH on 19 of them, with flagged
|
|
149
|
+
fractions spanning 0.94% to 80.8% all reported identically. Severity now scales on extent
|
|
150
|
+
(≥20% of transitions) **or** magnitude (a residual as large as the signal itself, which is
|
|
151
|
+
physically unexplainable however rare). HIGH rate on real data: 95% → 50%,
|
|
152
|
+
measured with `scripts/hub_smoke.py`.
|
|
153
|
+
- A clean scan printed "No findings" twice.
|
|
154
|
+
|
|
155
|
+
### Known limitations
|
|
156
|
+
|
|
157
|
+
- `smoothness.discontinuity_jump` and `integrity.declared_mismatch` report HIGH on 70% and
|
|
158
|
+
60% of curated public datasets respectively, which is far more likely to be a threshold
|
|
159
|
+
problem than a real epidemic. Documented in the README rather than silently shipped as
|
|
160
|
+
trustworthy; re-tuning them needs data we do not have yet.
|
|
161
|
+
|
|
162
|
+
### Deliberately not included
|
|
163
|
+
|
|
164
|
+
- **No 0-100 health score.** An aggregate number implies a calibration against real
|
|
165
|
+
training outcomes that does not exist yet — nothing here measures how much each defect
|
|
166
|
+
actually costs a trained policy. The report gives severity counts and ranked findings,
|
|
167
|
+
every one of which is individually defensible. The scoring function
|
|
168
|
+
(`adduct.synth.pipeline.quality_score`) is still present and tested; the headline
|
|
169
|
+
returns when a corpus of training runs can back it.
|
|
170
|
+
|
|
171
|
+
### Notes
|
|
172
|
+
|
|
173
|
+
- Python 3.10 through 3.13 are supported and tested.
|
|
174
|
+
- No telemetry. Your data never leaves your machine.
|
|
175
|
+
- `schema_version` is `1.0` — the first published report contract. Nothing consumed an
|
|
176
|
+
earlier shape, since this was the first released version of adduct.
|
|
177
|
+
|
|
178
|
+
[Unreleased]: https://github.com/prabhu-gopal/adduct/compare/v0.2.1...HEAD
|
|
179
|
+
[0.2.1]: https://github.com/prabhu-gopal/adduct/releases/tag/v0.2.1
|