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.
Files changed (147) hide show
  1. adduct-0.2.1/.github/ISSUE_TEMPLATE/bug_report.yml +71 -0
  2. adduct-0.2.1/.github/ISSUE_TEMPLATE/config.yml +8 -0
  3. adduct-0.2.1/.github/ISSUE_TEMPLATE/false_positive.yml +79 -0
  4. adduct-0.2.1/.github/pull_request_template.md +21 -0
  5. adduct-0.2.1/.github/workflows/ci.yml +92 -0
  6. adduct-0.2.1/.github/workflows/release.yml +99 -0
  7. adduct-0.2.1/.gitignore +39 -0
  8. adduct-0.2.1/CHANGELOG.md +179 -0
  9. adduct-0.2.1/CODE_OF_CONDUCT.md +132 -0
  10. adduct-0.2.1/CONTRIBUTING.md +100 -0
  11. adduct-0.2.1/LICENSE +202 -0
  12. adduct-0.2.1/NOTICE +4 -0
  13. adduct-0.2.1/PKG-INFO +274 -0
  14. adduct-0.2.1/README.md +215 -0
  15. adduct-0.2.1/SECURITY.md +55 -0
  16. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/REPORT.md +400 -0
  17. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/figures/fig1_detector_fire_rates.pdf +0 -0
  18. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/figures/fig1_detector_fire_rates.png +0 -0
  19. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/figures/fig2_dataset_severity.pdf +0 -0
  20. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/figures/fig2_dataset_severity.png +0 -0
  21. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/figures/fig3_runtime.pdf +0 -0
  22. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/figures/fig3_runtime.png +0 -0
  23. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/paper/adduct-benchmark.pdf +0 -0
  24. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/paper/adduct-benchmark.tex +480 -0
  25. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/results/sweep_default.json +1718 -0
  26. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/results/sweep_full.json +1732 -0
  27. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/scripts/make_figures.py +259 -0
  28. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/scripts/requirements.txt +1 -0
  29. adduct-0.2.1/benchmarks/2026-08-26-lerobot-20-v0.1.0/scripts/run_sweep.py +118 -0
  30. adduct-0.2.1/benchmarks/README.md +69 -0
  31. adduct-0.2.1/pyproject.toml +216 -0
  32. adduct-0.2.1/scripts/hub_smoke.py +129 -0
  33. adduct-0.2.1/src/adduct/__init__.py +37 -0
  34. adduct-0.2.1/src/adduct/_arrays.py +26 -0
  35. adduct-0.2.1/src/adduct/_compat.py +39 -0
  36. adduct-0.2.1/src/adduct/_plugins.py +39 -0
  37. adduct-0.2.1/src/adduct/adapters/__init__.py +21 -0
  38. adduct-0.2.1/src/adduct/adapters/_arraysource.py +198 -0
  39. adduct-0.2.1/src/adduct/adapters/_mapping.py +213 -0
  40. adduct-0.2.1/src/adduct/adapters/_video.py +157 -0
  41. adduct-0.2.1/src/adduct/adapters/base.py +89 -0
  42. adduct-0.2.1/src/adduct/adapters/hdf5.py +205 -0
  43. adduct-0.2.1/src/adduct/adapters/lerobot.py +499 -0
  44. adduct-0.2.1/src/adduct/adapters/numpy_dir.py +107 -0
  45. adduct-0.2.1/src/adduct/adapters/registry.py +97 -0
  46. adduct-0.2.1/src/adduct/adapters/rlds.py +224 -0
  47. adduct-0.2.1/src/adduct/adapters/zarr_replay.py +134 -0
  48. adduct-0.2.1/src/adduct/analysis/__init__.py +45 -0
  49. adduct-0.2.1/src/adduct/analysis/confident_learning.py +72 -0
  50. adduct-0.2.1/src/adduct/analysis/embeddings.py +135 -0
  51. adduct-0.2.1/src/adduct/analysis/neighbors.py +171 -0
  52. adduct-0.2.1/src/adduct/analysis/robust.py +70 -0
  53. adduct-0.2.1/src/adduct/analysis/shapes.py +186 -0
  54. adduct-0.2.1/src/adduct/analysis/twosample.py +110 -0
  55. adduct-0.2.1/src/adduct/api.py +95 -0
  56. adduct-0.2.1/src/adduct/bench/__init__.py +18 -0
  57. adduct-0.2.1/src/adduct/bench/harness.py +166 -0
  58. adduct-0.2.1/src/adduct/calibrate/__init__.py +7 -0
  59. adduct-0.2.1/src/adduct/calibrate/collect.py +131 -0
  60. adduct-0.2.1/src/adduct/calibrate/conformal.py +111 -0
  61. adduct-0.2.1/src/adduct/calibrate/corpus.py +203 -0
  62. adduct-0.2.1/src/adduct/calibrate/dynamics_model.py +225 -0
  63. adduct-0.2.1/src/adduct/calibrate/fdr.py +68 -0
  64. adduct-0.2.1/src/adduct/calibrate/gate.py +229 -0
  65. adduct-0.2.1/src/adduct/cli.py +436 -0
  66. adduct-0.2.1/src/adduct/config.py +118 -0
  67. adduct-0.2.1/src/adduct/detectors/__init__.py +8 -0
  68. adduct-0.2.1/src/adduct/detectors/_common.py +166 -0
  69. adduct-0.2.1/src/adduct/detectors/base.py +118 -0
  70. adduct-0.2.1/src/adduct/detectors/causal.py +189 -0
  71. adduct-0.2.1/src/adduct/detectors/consistency.py +212 -0
  72. adduct-0.2.1/src/adduct/detectors/coverage.py +364 -0
  73. adduct-0.2.1/src/adduct/detectors/dynamics.py +207 -0
  74. adduct-0.2.1/src/adduct/detectors/integrity.py +475 -0
  75. adduct-0.2.1/src/adduct/detectors/kinematics.py +467 -0
  76. adduct-0.2.1/src/adduct/detectors/label.py +182 -0
  77. adduct-0.2.1/src/adduct/detectors/multimodality.py +200 -0
  78. adduct-0.2.1/src/adduct/detectors/policy_data.py +344 -0
  79. adduct-0.2.1/src/adduct/detectors/registry.py +107 -0
  80. adduct-0.2.1/src/adduct/detectors/scale.py +200 -0
  81. adduct-0.2.1/src/adduct/detectors/smoothness.py +177 -0
  82. adduct-0.2.1/src/adduct/detectors/stats.py +235 -0
  83. adduct-0.2.1/src/adduct/detectors/temporal.py +378 -0
  84. adduct-0.2.1/src/adduct/detectors/vision.py +418 -0
  85. adduct-0.2.1/src/adduct/encoders/__init__.py +31 -0
  86. adduct-0.2.1/src/adduct/encoders/base.py +35 -0
  87. adduct-0.2.1/src/adduct/encoders/dino.py +82 -0
  88. adduct-0.2.1/src/adduct/encoders/tiled.py +49 -0
  89. adduct-0.2.1/src/adduct/engine.py +209 -0
  90. adduct-0.2.1/src/adduct/hub.py +122 -0
  91. adduct-0.2.1/src/adduct/ir/__init__.py +51 -0
  92. adduct-0.2.1/src/adduct/ir/episode.py +133 -0
  93. adduct-0.2.1/src/adduct/ir/schema.py +180 -0
  94. adduct-0.2.1/src/adduct/policy/__init__.py +9 -0
  95. adduct-0.2.1/src/adduct/policy/loader.py +321 -0
  96. adduct-0.2.1/src/adduct/policy/target.py +56 -0
  97. adduct-0.2.1/src/adduct/profile/__init__.py +14 -0
  98. adduct-0.2.1/src/adduct/profile/action_space.py +132 -0
  99. adduct-0.2.1/src/adduct/profile/dataset_profile.py +224 -0
  100. adduct-0.2.1/src/adduct/profile/episode_reservoir.py +130 -0
  101. adduct-0.2.1/src/adduct/profile/online.py +206 -0
  102. adduct-0.2.1/src/adduct/py.typed +0 -0
  103. adduct-0.2.1/src/adduct/report/__init__.py +31 -0
  104. adduct-0.2.1/src/adduct/report/base.py +22 -0
  105. adduct-0.2.1/src/adduct/report/html.py +201 -0
  106. adduct-0.2.1/src/adduct/report/messages.py +143 -0
  107. adduct-0.2.1/src/adduct/report/model.py +222 -0
  108. adduct-0.2.1/src/adduct/report/sarif.py +231 -0
  109. adduct-0.2.1/src/adduct/report/tty.py +124 -0
  110. adduct-0.2.1/src/adduct/synth/__init__.py +21 -0
  111. adduct-0.2.1/src/adduct/synth/pipeline.py +198 -0
  112. adduct-0.2.1/src/adduct/version.py +17 -0
  113. adduct-0.2.1/tests/_synth.py +897 -0
  114. adduct-0.2.1/tests/test_accuracy_research.py +116 -0
  115. adduct-0.2.1/tests/test_adapters_p3.py +246 -0
  116. adduct-0.2.1/tests/test_advanced.py +134 -0
  117. adduct-0.2.1/tests/test_bench_harness.py +125 -0
  118. adduct-0.2.1/tests/test_benchmark.py +498 -0
  119. adduct-0.2.1/tests/test_calibration_gate.py +475 -0
  120. adduct-0.2.1/tests/test_catalogue_honesty.py +124 -0
  121. adduct-0.2.1/tests/test_cli.py +91 -0
  122. adduct-0.2.1/tests/test_cli_errors.py +137 -0
  123. adduct-0.2.1/tests/test_config.py +48 -0
  124. adduct-0.2.1/tests/test_corpus_p4.py +85 -0
  125. adduct-0.2.1/tests/test_corrupt_data_robustness.py +199 -0
  126. adduct-0.2.1/tests/test_determinism.py +17 -0
  127. adduct-0.2.1/tests/test_dod.py +174 -0
  128. adduct-0.2.1/tests/test_episode_reservoir.py +169 -0
  129. adduct-0.2.1/tests/test_faults.py +168 -0
  130. adduct-0.2.1/tests/test_faults_p2.py +238 -0
  131. adduct-0.2.1/tests/test_faults_p3.py +236 -0
  132. adduct-0.2.1/tests/test_hub.py +112 -0
  133. adduct-0.2.1/tests/test_lag_conventions.py +298 -0
  134. adduct-0.2.1/tests/test_lerobot.py +228 -0
  135. adduct-0.2.1/tests/test_optional_backends.py +287 -0
  136. adduct-0.2.1/tests/test_pipeline.py +39 -0
  137. adduct-0.2.1/tests/test_policy_p3.py +193 -0
  138. adduct-0.2.1/tests/test_profile.py +39 -0
  139. adduct-0.2.1/tests/test_registry.py +108 -0
  140. adduct-0.2.1/tests/test_rlds_adapter.py +305 -0
  141. adduct-0.2.1/tests/test_roundtrip.py +34 -0
  142. adduct-0.2.1/tests/test_score.py +257 -0
  143. adduct-0.2.1/tests/test_streaming_scale.py +213 -0
  144. adduct-0.2.1/tests/test_triage.py +84 -0
  145. adduct-0.2.1/tests/test_trust_p4.py +202 -0
  146. adduct-0.2.1/tests/test_video_lerobot.py +313 -0
  147. 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
@@ -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