pbcheck 0.1.0__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 (137) hide show
  1. pbcheck-0.1.0/.gitattributes +24 -0
  2. pbcheck-0.1.0/.github/dependabot.yml +12 -0
  3. pbcheck-0.1.0/.github/workflows/census-candidates.yml +136 -0
  4. pbcheck-0.1.0/.github/workflows/release.yml +152 -0
  5. pbcheck-0.1.0/.github/workflows/tests.yml +171 -0
  6. pbcheck-0.1.0/.gitignore +57 -0
  7. pbcheck-0.1.0/.pre-commit-config.yaml +52 -0
  8. pbcheck-0.1.0/.zenodo.json +29 -0
  9. pbcheck-0.1.0/CHANGELOG.md +731 -0
  10. pbcheck-0.1.0/CITATION.cff +26 -0
  11. pbcheck-0.1.0/CONTRIBUTING.md +91 -0
  12. pbcheck-0.1.0/LICENSE +28 -0
  13. pbcheck-0.1.0/PKG-INFO +345 -0
  14. pbcheck-0.1.0/README.md +301 -0
  15. pbcheck-0.1.0/SECURITY.md +14 -0
  16. pbcheck-0.1.0/demo/README.md +121 -0
  17. pbcheck-0.1.0/demo/example_reference_two_arm/pbcheck_audit.json +738 -0
  18. pbcheck-0.1.0/demo/example_reference_two_arm/pbcheck_report.html +94 -0
  19. pbcheck-0.1.0/demo/example_reference_two_arm/pbcheck_report.md +413 -0
  20. pbcheck-0.1.0/demo/kang2018_design_gate/pbcheck_audit.json +228 -0
  21. pbcheck-0.1.0/demo/kang2018_design_gate/pbcheck_report.html +72 -0
  22. pbcheck-0.1.0/demo/kang2018_design_gate/pbcheck_report.md +196 -0
  23. pbcheck-0.1.0/docs/AMENDMENTS.md +3221 -0
  24. pbcheck-0.1.0/docs/ENV_NOTES.md +33 -0
  25. pbcheck-0.1.0/docs/PHASE0_SPEC.md +250 -0
  26. pbcheck-0.1.0/docs/PILOT_FINDINGS.md +205 -0
  27. pbcheck-0.1.0/docs/PREREGISTRATION_STRATUM_LIST.md +1084 -0
  28. pbcheck-0.1.0/docs/USAGE.md +240 -0
  29. pbcheck-0.1.0/pilot/README.md +184 -0
  30. pbcheck-0.1.0/pilot/gate/synthetic_gate_2026-08-15.json +266 -0
  31. pbcheck-0.1.0/pilot/preregistration/a2_feasibility_2026-08-16.csv +3521 -0
  32. pbcheck-0.1.0/pilot/preregistration/a2_feasibility_2026-08-16.json +98067 -0
  33. pbcheck-0.1.0/pilot/preregistration/census_candidates_run31910799023_2026-08-15.csv +2191 -0
  34. pbcheck-0.1.0/pilot/preregistration/census_candidates_run31910799023_2026-08-15.json +187984 -0
  35. pbcheck-0.1.0/pilot/preregistration/census_release_datasets_2025-01-30.json +17355 -0
  36. pbcheck-0.1.0/pilot/preregistration/discover_index_2026-08-16.json +58180 -0
  37. pbcheck-0.1.0/pilot/preregistration/stratum_list_2026-08-16.csv +358 -0
  38. pbcheck-0.1.0/pilot/preregistration/stratum_list_2026-08-16.json +30104 -0
  39. pbcheck-0.1.0/pilot/preregistration/stratum_list_proposal_2026-08-16.redacted.md +909 -0
  40. pbcheck-0.1.0/pilot/results/.gitkeep +0 -0
  41. pbcheck-0.1.0/pilot/testsel/summary.csv +147 -0
  42. pbcheck-0.1.0/pilot/testsel/summary.json +2776 -0
  43. pbcheck-0.1.0/pilot/upper_bound_check/upper_bound_check_2026-08-16.csv +11 -0
  44. pbcheck-0.1.0/pilot/upper_bound_check/upper_bound_check_2026-08-16.json +3471 -0
  45. pbcheck-0.1.0/pyproject.toml +107 -0
  46. pbcheck-0.1.0/requirements.lock +95 -0
  47. pbcheck-0.1.0/scripts/a2_feasibility.py +1103 -0
  48. pbcheck-0.1.0/scripts/analyze_test_selection.py +174 -0
  49. pbcheck-0.1.0/scripts/census_candidates.py +964 -0
  50. pbcheck-0.1.0/scripts/changelog_section.py +61 -0
  51. pbcheck-0.1.0/scripts/check_docstring_only_diff.py +150 -0
  52. pbcheck-0.1.0/scripts/check_upper_bound_claim.py +714 -0
  53. pbcheck-0.1.0/scripts/check_version_consistency.py +240 -0
  54. pbcheck-0.1.0/scripts/compare_gate_scalars.py +147 -0
  55. pbcheck-0.1.0/scripts/demo_common.py +240 -0
  56. pbcheck-0.1.0/scripts/demo_kang2018.py +117 -0
  57. pbcheck-0.1.0/scripts/demo_two_arm.py +257 -0
  58. pbcheck-0.1.0/scripts/fetch_preregistration_evidence.py +350 -0
  59. pbcheck-0.1.0/scripts/freeze_stratum_list.py +2442 -0
  60. pbcheck-0.1.0/scripts/measure_audit_runtime.py +300 -0
  61. pbcheck-0.1.0/scripts/pb_calibration_probe.py +1375 -0
  62. pbcheck-0.1.0/scripts/proof_of_life.py +140 -0
  63. pbcheck-0.1.0/scripts/protocol_safety_check.py +546 -0
  64. pbcheck-0.1.0/scripts/run_test_selection_grid.py +295 -0
  65. pbcheck-0.1.0/scripts/synthetic_gate.py +393 -0
  66. pbcheck-0.1.0/src/pbcheck/__init__.py +72 -0
  67. pbcheck-0.1.0/src/pbcheck/__main__.py +10 -0
  68. pbcheck-0.1.0/src/pbcheck/audit.py +1223 -0
  69. pbcheck-0.1.0/src/pbcheck/audit_schema.py +755 -0
  70. pbcheck-0.1.0/src/pbcheck/census_select.py +1084 -0
  71. pbcheck-0.1.0/src/pbcheck/cli.py +257 -0
  72. pbcheck-0.1.0/src/pbcheck/design.py +184 -0
  73. pbcheck-0.1.0/src/pbcheck/example.py +120 -0
  74. pbcheck-0.1.0/src/pbcheck/gate_config.py +210 -0
  75. pbcheck-0.1.0/src/pbcheck/gene_universe.py +74 -0
  76. pbcheck-0.1.0/src/pbcheck/io_counts.py +1483 -0
  77. pbcheck-0.1.0/src/pbcheck/methods/__init__.py +12 -0
  78. pbcheck-0.1.0/src/pbcheck/methods/de.py +50 -0
  79. pbcheck-0.1.0/src/pbcheck/methods/moderated.py +410 -0
  80. pbcheck-0.1.0/src/pbcheck/methods/naive.py +86 -0
  81. pbcheck-0.1.0/src/pbcheck/methods/naive_engine.py +434 -0
  82. pbcheck-0.1.0/src/pbcheck/methods/pseudobulk.py +254 -0
  83. pbcheck-0.1.0/src/pbcheck/metrics.py +217 -0
  84. pbcheck-0.1.0/src/pbcheck/mtc.py +151 -0
  85. pbcheck-0.1.0/src/pbcheck/permutation.py +358 -0
  86. pbcheck-0.1.0/src/pbcheck/product_constants.py +24 -0
  87. pbcheck-0.1.0/src/pbcheck/render/__init__.py +74 -0
  88. pbcheck-0.1.0/src/pbcheck/render/html.py +89 -0
  89. pbcheck-0.1.0/src/pbcheck/render/markdown.py +61 -0
  90. pbcheck-0.1.0/src/pbcheck/render/sections.py +682 -0
  91. pbcheck-0.1.0/src/pbcheck/render/text.py +550 -0
  92. pbcheck-0.1.0/synthetic/oracles.py +189 -0
  93. pbcheck-0.1.0/tests/conftest.py +102 -0
  94. pbcheck-0.1.0/tests/fixtures/audit_payload_coarse_null.html +96 -0
  95. pbcheck-0.1.0/tests/fixtures/audit_payload_coarse_null.json +389 -0
  96. pbcheck-0.1.0/tests/fixtures/audit_payload_coarse_null.md +341 -0
  97. pbcheck-0.1.0/tests/fixtures/audit_payload_complete.html +95 -0
  98. pbcheck-0.1.0/tests/fixtures/audit_payload_complete.json +415 -0
  99. pbcheck-0.1.0/tests/fixtures/audit_payload_complete.md +365 -0
  100. pbcheck-0.1.0/tests/fixtures/audit_payload_design_only.html +74 -0
  101. pbcheck-0.1.0/tests/fixtures/audit_payload_design_only.json +234 -0
  102. pbcheck-0.1.0/tests/fixtures/audit_payload_design_only.md +198 -0
  103. pbcheck-0.1.0/tests/fixtures/audit_payload_naive_only.html +87 -0
  104. pbcheck-0.1.0/tests/fixtures/audit_payload_naive_only.json +348 -0
  105. pbcheck-0.1.0/tests/fixtures/audit_payload_naive_only.md +288 -0
  106. pbcheck-0.1.0/tests/test_a2_feasibility.py +317 -0
  107. pbcheck-0.1.0/tests/test_audit.py +1069 -0
  108. pbcheck-0.1.0/tests/test_audit_schema.py +448 -0
  109. pbcheck-0.1.0/tests/test_census_candidates.py +792 -0
  110. pbcheck-0.1.0/tests/test_census_select.py +800 -0
  111. pbcheck-0.1.0/tests/test_checklist_scripts.py +400 -0
  112. pbcheck-0.1.0/tests/test_cli.py +357 -0
  113. pbcheck-0.1.0/tests/test_demo_scripts.py +359 -0
  114. pbcheck-0.1.0/tests/test_design.py +115 -0
  115. pbcheck-0.1.0/tests/test_docs.py +608 -0
  116. pbcheck-0.1.0/tests/test_engine_oracles.py +68 -0
  117. pbcheck-0.1.0/tests/test_example.py +81 -0
  118. pbcheck-0.1.0/tests/test_gate_config.py +115 -0
  119. pbcheck-0.1.0/tests/test_gene_universe.py +65 -0
  120. pbcheck-0.1.0/tests/test_grid_summary.py +130 -0
  121. pbcheck-0.1.0/tests/test_io_counts.py +783 -0
  122. pbcheck-0.1.0/tests/test_measure_audit_runtime.py +34 -0
  123. pbcheck-0.1.0/tests/test_methods.py +202 -0
  124. pbcheck-0.1.0/tests/test_metrics.py +144 -0
  125. pbcheck-0.1.0/tests/test_moderated.py +146 -0
  126. pbcheck-0.1.0/tests/test_naive_engine.py +702 -0
  127. pbcheck-0.1.0/tests/test_oracles.py +141 -0
  128. pbcheck-0.1.0/tests/test_packaging.py +443 -0
  129. pbcheck-0.1.0/tests/test_permutation_mtc.py +220 -0
  130. pbcheck-0.1.0/tests/test_properties.py +103 -0
  131. pbcheck-0.1.0/tests/test_protocol_safety_check.py +533 -0
  132. pbcheck-0.1.0/tests/test_pseudobulk.py +117 -0
  133. pbcheck-0.1.0/tests/test_render.py +241 -0
  134. pbcheck-0.1.0/tests/test_render_output.py +135 -0
  135. pbcheck-0.1.0/tests/test_render_text.py +363 -0
  136. pbcheck-0.1.0/tests/test_stratum_list_freeze.py +1646 -0
  137. pbcheck-0.1.0/tests/test_synthetic_gate.py +128 -0
@@ -0,0 +1,24 @@
1
+ # The pre-registration artifacts are hash-pinned, not read as text.
2
+ #
3
+ # `pilot/preregistration/` holds the frozen §1 stratum list and the CI candidate manifest it was
4
+ # derived from. `docs/PREREGISTRATION_STRATUM_LIST.md` records their sha256 sums, and
5
+ # `scripts/freeze_stratum_list.py` refuses to run against a source whose hash does not match —
6
+ # so their bytes are the evidence, not a rendering of it.
7
+ #
8
+ # Without `-text`, git's end-of-line conversion would rewrite them per platform: the committed CSV
9
+ # came off a Linux runner with CRLF terminators, `core.autocrlf=true` on a Windows checkout would
10
+ # rewrite the JSON to CRLF, and either change breaks the recorded hash. The byte-identity test in
11
+ # `tests/test_stratum_list_freeze.py` would then pass on one CI leg and fail on another, which is
12
+ # worse than failing everywhere. Diffing these files is not how they are audited; hashing them is.
13
+ #
14
+ # `pilot/gate/` and `pilot/testsel/` are cited evidence too — `.pre-commit-config.yaml` excludes
15
+ # them from the fixing hooks on exactly that ground — and they are deliberately NOT marked here.
16
+ # They were committed with LF and are checked out with CRLF under `core.autocrlf=true`, so adding
17
+ # `-text` would make the working tree's CRLF bytes canonical and the next `git add` would rewrite
18
+ # all three blobs: measured, at the commit that added this line, 8909 -> 9174 bytes for
19
+ # `pilot/gate/synthetic_gate_2026-08-15.json`, 21811 -> 21958 for `pilot/testsel/summary.csv` and
20
+ # 63081 -> 65856 for `pilot/testsel/summary.json`. Protecting evidence by rewriting it is not
21
+ # protecting it. The asymmetry is therefore real and intended: `pilot/preregistration/` is pinned by
22
+ # hash and must be byte-stable across platforms; the other two are protected from the fixing hooks
23
+ # only, and their bytes stay whatever git already stores.
24
+ pilot/preregistration/** -text
@@ -0,0 +1,12 @@
1
+ version: 2
2
+ updates:
3
+ # github-actions only, monthly. pip is deliberately NOT enrolled here: this is a bleeding-edge
4
+ # scientific stack (numpy 2.4 / pandas 3.0 / scanpy 1.12 — see docs/ENV_NOTES.md) where a weekly
5
+ # pip-update PR would mostly be noise a solo maintainer has to triage, not a real signal. The
6
+ # CI "Report the resolved scientific stack" step (.github/workflows/tests.yml, `test` job) is
7
+ # the drift detector by design instead: it prints what actually resolved on every push, so
8
+ # drift is visible without a bot filing a PR nobody asked for.
9
+ - package-ecosystem: "github-actions"
10
+ directory: "/"
11
+ schedule:
12
+ interval: "monthly"
@@ -0,0 +1,136 @@
1
+ # The one job in this repo that touches real data. It reads the whole pinned CELLxGENE Census
2
+ # (~60 M cells after the §1 value_filter) and produces the CANDIDATE stratum manifest — not the
3
+ # §1 pre-registration, and not an admission of anything: every row carries admitted_to_sweep=False.
4
+ #
5
+ # Manual only. It is multi-hour, it reaches the network for gigabytes, and its result is an
6
+ # artifact rather than a commit, so there is nothing for a push or a pull request to gain from it.
7
+ # The regular `tests` workflow deliberately does NOT install the [census] extra; this one does, and
8
+ # it is the only place a cellxgene-census / tiledbsoma resolution is exercised at all.
9
+ name: census candidates
10
+
11
+ on:
12
+ workflow_dispatch:
13
+ inputs:
14
+ dry_run:
15
+ description: "Dry run: pass 2 reads only a handful of datasets (spread across the size range) to calibrate the full run. Uncheck for the real thing."
16
+ type: boolean
17
+ default: true
18
+ max_datasets:
19
+ description: "Cap the datasets pass 2 reads, largest first (0 = no cap)."
20
+ type: number
21
+ default: 0
22
+ artifact_retention_days:
23
+ description: "How long to keep the manifest + log artifact."
24
+ type: number
25
+ default: 30
26
+ reader_buffer_mb:
27
+ description: "Per-column Census read buffer, MiB (py./soma.init_buffer_bytes). The census default is 1024, which four string columns do not fit into a 16 GB runner. Lower this if the run is killed at exit 143."
28
+ type: number
29
+ default: 128
30
+
31
+ # Deliberately NOT cancel-in-progress (the `tests` workflow is): a five-hour run that is killed
32
+ # by a second dispatch produces nothing at all, and the Census is pinned, so two concurrent runs
33
+ # would compute the same answer twice. Queue instead.
34
+ concurrency:
35
+ group: ${{ github.workflow }}
36
+ cancel-in-progress: false
37
+
38
+ # The Census is public and anonymous — no secrets, no credentials, nothing to write.
39
+ permissions:
40
+ contents: read
41
+
42
+ jobs:
43
+ candidates:
44
+ name: candidate manifest (dry_run=${{ inputs.dry_run }})
45
+ runs-on: ubuntu-latest
46
+ # A public runner is killed at 6 h with no artifact. Stopping at 5.5 h leaves the upload step
47
+ # (if: always()) time to run, so a run that hits the wall still returns its partial log — which
48
+ # is what makes the next dispatch's budget an informed decision rather than a guess.
49
+ timeout-minutes: 330
50
+
51
+ steps:
52
+ - uses: actions/checkout@v7
53
+
54
+ - uses: actions/setup-python@v7
55
+ with:
56
+ python-version: "3.12"
57
+ cache: pip
58
+ cache-dependency-path: pyproject.toml
59
+
60
+ # cellxgene-census is unpinned in pyproject on purpose: the SDK is a client for a pinned
61
+ # dataset, and package_versions() records whatever actually resolved into the manifest
62
+ # header. The pin that matters — the Census version — is in census_select.py, not here.
63
+ - name: Install the package with the [census] extra
64
+ run: |
65
+ python -m pip install --upgrade pip
66
+ python -m pip install -e ".[census]"
67
+
68
+ # The counterpart of the `tests` workflow's stack report, for the half of the stack that
69
+ # workflow never installs. The Census version and the value_filter are printed from the
70
+ # package so the log states the protocol the run was made under, not a copy of it. It is
71
+ # tee'd into the same log the artifact carries, so a run that dies in the driver still ships
72
+ # the stack it died with — the log is the post-mortem, and it starts here.
73
+ - name: Report the resolved Census stack and the pinned protocol
74
+ shell: bash
75
+ run: |
76
+ {
77
+ python -c "import cellxgene_census, tiledbsoma, pyarrow, pandas, numpy; [print(f'{m.__name__}=={m.__version__}') for m in (cellxgene_census, tiledbsoma, pyarrow, pandas, numpy)]"
78
+ python -c "from pbcheck import census_select as cs; print('census_version:', cs.CENSUS_VERSION); print('value_filter (applied):', cs.VALUE_FILTER); print('value_filter (spec text):', cs.SPEC_VALUE_FILTER); print('organism:', cs.CENSUS_ORGANISM)"
79
+ free -m || true
80
+ nproc || true
81
+ } 2>&1 | tee census-candidates.log
82
+
83
+ # GitHub runs `run:` as `bash -e -o pipefail`, so the driver failing anywhere in the pipe
84
+ # fails this step — which is what we want, and is why the job summary is NOT written here.
85
+ # The previous version put the summary after the pipeline in this same step: `-e` (which
86
+ # `set -uo pipefail` does not clear) aborted the step at the pipe, and neither the summary
87
+ # nor the intended exit code ever ran. Reporting belongs in a step that cannot be skipped.
88
+ - name: Build the candidate manifest
89
+ shell: bash
90
+ env:
91
+ DRY_RUN: ${{ inputs.dry_run }}
92
+ MAX_DATASETS: ${{ inputs.max_datasets }}
93
+ READER_BUFFER_MB: ${{ inputs.reader_buffer_mb }}
94
+ # Cell-type labels are ontology free text and carry non-ASCII ("Muller cell" is spelled
95
+ # with an umlaut in CL), and the driver prints them in its top-candidates summary. If the
96
+ # runner's stdout ever resolved to ASCII, that print would die AFTER the manifest was
97
+ # written — a red run, and a lost log, for a formatting reason.
98
+ PYTHONIOENCODING: utf-8
99
+ run: |
100
+ args=(--max-datasets "${MAX_DATASETS:-0}" --reader-buffer-mb "${READER_BUFFER_MB:-128}")
101
+ if [ "${DRY_RUN}" = "true" ]; then
102
+ args+=(--dry-run)
103
+ fi
104
+ echo "python scripts/census_candidates.py ${args[*]}" | tee -a census-candidates.log
105
+ python -u scripts/census_candidates.py "${args[@]}" 2>&1 | tee -a census-candidates.log
106
+
107
+ # if: always() — this is the step that reports, so it must run when the driver dies, when it
108
+ # times out, and when it succeeds. A memory kill (SIGTERM, exit 143) produces no traceback:
109
+ # the tail of this log is the entire post-mortem.
110
+ - name: Summarise the run log
111
+ if: always()
112
+ shell: bash
113
+ run: |
114
+ {
115
+ echo "### Census candidate manifest (dry_run=${{ inputs.dry_run }}, buffers ${{ inputs.reader_buffer_mb }} MiB)"
116
+ echo
117
+ echo '```'
118
+ tail -n 80 census-candidates.log 2>/dev/null || echo "(no log — the run died before writing one)"
119
+ echo '```'
120
+ } >> "$GITHUB_STEP_SUMMARY"
121
+
122
+ # if: always() — a partial log from a run that timed out or died mid-pass is the timing
123
+ # calibration for the next dispatch, and losing it costs another five hours to recover.
124
+ # The manifest itself is uploaded, never committed: pilot/results/ is gitignored, and a
125
+ # candidate list that lands in git by way of a CI job has pre-registered itself by accident.
126
+ - name: Upload the manifest and the run log
127
+ uses: actions/upload-artifact@v6
128
+ if: always()
129
+ with:
130
+ name: census-candidates-${{ github.run_id }}
131
+ path: |
132
+ pilot/results/census_candidates/*.json
133
+ pilot/results/census_candidates/*.csv
134
+ census-candidates.log
135
+ retention-days: ${{ inputs.artifact_retention_days }}
136
+ if-no-files-found: warn
@@ -0,0 +1,152 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+ workflow_dispatch:
7
+ inputs:
8
+ target:
9
+ type: choice
10
+ options: [testpypi]
11
+ default: testpypi
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ check:
18
+ name: lint + version check + tests
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v7
22
+ with:
23
+ # A tag push checks out the tag alone; tests/test_protocol_safety_check.py needs
24
+ # origin/main to diff against, so fetch the full history here as tests.yml does.
25
+ fetch-depth: 0
26
+
27
+ - uses: actions/setup-python@v7
28
+ with:
29
+ python-version: "3.12"
30
+ cache: pip
31
+ cache-dependency-path: pyproject.toml
32
+
33
+ - name: Install package (editable) + dev extra
34
+ run: |
35
+ python -m pip install --upgrade pip
36
+ python -m pip install -e ".[dev]"
37
+
38
+ - name: ruff check
39
+ run: python -m ruff check .
40
+
41
+ - name: Check CITATION.cff / CHANGELOG.md version match the package
42
+ run: python scripts/check_version_consistency.py
43
+
44
+ - name: Reject malformed release tags
45
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
46
+ run: |
47
+ if ! [[ "$GITHUB_REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
48
+ echo "tag '$GITHUB_REF_NAME' does not match v[0-9]+.[0-9]+.[0-9]+" >&2
49
+ exit 1
50
+ fi
51
+
52
+ - name: Check the tag matches the released version
53
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
54
+ run: python scripts/check_version_consistency.py --tag "$GITHUB_REF_NAME" --require-date
55
+
56
+ - name: Run tests
57
+ run: python -m pytest -q -m "not slow"
58
+
59
+ build:
60
+ name: build sdist/wheel + twine check
61
+ needs: check
62
+ runs-on: ubuntu-latest
63
+ steps:
64
+ - uses: actions/checkout@v7
65
+
66
+ - uses: actions/setup-python@v7
67
+ with:
68
+ python-version: "3.12"
69
+ cache: pip
70
+ cache-dependency-path: pyproject.toml
71
+
72
+ - name: Install packaging tools
73
+ run: |
74
+ python -m pip install --upgrade pip
75
+ python -m pip install build twine
76
+
77
+ - name: Build sdist + wheel
78
+ run: python -m build
79
+
80
+ - name: twine check
81
+ run: python -m twine check dist/*
82
+
83
+ - uses: actions/upload-artifact@v6
84
+ with:
85
+ name: dist
86
+ path: dist/
87
+
88
+ publish-testpypi:
89
+ name: publish to TestPyPI
90
+ needs: build
91
+ if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
92
+ runs-on: ubuntu-latest
93
+ environment: testpypi
94
+ permissions:
95
+ id-token: write
96
+ steps:
97
+ - uses: actions/download-artifact@v6
98
+ with:
99
+ name: dist
100
+ path: dist/
101
+
102
+ # Pinned by commit SHA, not the release/v1 branch: a job holding id-token: write should
103
+ # not follow a moving ref. dc37677b2e1c63e2034f94d8a5b11f265b73ba33 == tag v1.14.2.
104
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
105
+ with:
106
+ repository-url: https://test.pypi.org/legacy/
107
+
108
+ publish-pypi:
109
+ name: publish to PyPI
110
+ needs: build
111
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
112
+ runs-on: ubuntu-latest
113
+ environment: pypi
114
+ permissions:
115
+ id-token: write
116
+ steps:
117
+ - uses: actions/download-artifact@v6
118
+ with:
119
+ name: dist
120
+ path: dist/
121
+
122
+ # Pinned by commit SHA, not the release/v1 branch: a job holding id-token: write should
123
+ # not follow a moving ref. dc37677b2e1c63e2034f94d8a5b11f265b73ba33 == tag v1.14.2.
124
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
125
+
126
+ github-release:
127
+ name: create the GitHub Release
128
+ needs: publish-pypi
129
+ runs-on: ubuntu-latest
130
+ permissions:
131
+ contents: write
132
+ steps:
133
+ - uses: actions/checkout@v7
134
+
135
+ - uses: actions/setup-python@v7
136
+ with:
137
+ python-version: "3.12"
138
+ cache: pip
139
+ cache-dependency-path: pyproject.toml
140
+
141
+ - uses: actions/download-artifact@v6
142
+ with:
143
+ name: dist
144
+ path: dist/
145
+
146
+ - name: Extract the CHANGELOG section for this release
147
+ run: python scripts/changelog_section.py "${GITHUB_REF_NAME#v}" > notes.md
148
+
149
+ - name: Create the GitHub Release
150
+ env:
151
+ GH_TOKEN: ${{ github.token }}
152
+ run: gh release create "$GITHUB_REF_NAME" dist/* --title "pbcheck $GITHUB_REF_NAME" --notes-file notes.md
@@ -0,0 +1,171 @@
1
+ name: tests
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
+ lint:
18
+ name: ruff check
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v7
22
+
23
+ - uses: actions/setup-python@v7
24
+ with:
25
+ python-version: "3.12"
26
+ cache: pip
27
+ cache-dependency-path: pyproject.toml
28
+
29
+ - name: Install dev extra (ruff only, no scientific stack needed for lint)
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ python -m pip install -e ".[dev]"
33
+
34
+ # ruff format --check is deliberately NOT run here: this is a pre-registered scientific
35
+ # repo, and a lot of existing code predates a formatting policy. Reformatting it wholesale
36
+ # in a lint-hardening commit would pollute `git blame` on files the analysis depends on.
37
+ # `ruff check` (real lint rules: unused imports, etc.) is enforced; formatting is not yet.
38
+ - name: ruff check
39
+ run: python -m ruff check .
40
+
41
+ # pyproject.toml's version is dynamic, read from src/pbcheck/__init__.py by hatchling
42
+ # (R3); CITATION.cff keeps an independent copy for tools that read it standalone (GitHub's
43
+ # "Cite this repository", Zenodo), which is exactly the copy that can drift unnoticed.
44
+ - name: Check CITATION.cff version matches the package
45
+ run: python scripts/check_version_consistency.py
46
+
47
+ test:
48
+ name: pytest (py${{ matrix.python-version }}, ${{ matrix.os }})
49
+ runs-on: ${{ matrix.os }}
50
+ strategy:
51
+ fail-fast: false
52
+ matrix:
53
+ os: [ubuntu-latest]
54
+ python-version: ["3.12", "3.13"]
55
+ include:
56
+ # Developed on Windows; Linux is the untested platform, so it carries the matrix.
57
+ - os: windows-latest
58
+ python-version: "3.12"
59
+ # One macOS combination, on the newer interpreter, for a third-platform smoke check
60
+ # without tripling the whole matrix.
61
+ - os: macos-latest
62
+ python-version: "3.13"
63
+
64
+ steps:
65
+ - uses: actions/checkout@v7
66
+ with:
67
+ # tests/test_protocol_safety_check.py runs the checklist against main; a branch or
68
+ # pull-request checkout at the default depth 1 carries no main to diff against.
69
+ fetch-depth: 0
70
+
71
+ - uses: actions/setup-python@v7
72
+ with:
73
+ python-version: ${{ matrix.python-version }}
74
+ cache: pip
75
+ cache-dependency-path: pyproject.toml
76
+
77
+ # The [census] extra is deliberately NOT installed: Phase-0 engine validation runs
78
+ # entirely on synthetic oracles, so CI never reaches the network for data.
79
+ - name: Install package (src-layout, editable) + dev extra
80
+ run: |
81
+ python -m pip install --upgrade pip
82
+ python -m pip install -e ".[dev]"
83
+
84
+ # Not decoration: this turns the CI log into a live counterpart of docs/ENV_NOTES.md
85
+ # and shows immediately if an unpinned upper bound drifts.
86
+ - name: Report the resolved scientific stack
87
+ run: |
88
+ python -c "import numpy, scipy, pandas, anndata, scanpy, decoupler, pydeseq2, statsmodels; [print(f'{m.__name__}=={m.__version__}') for m in (numpy, scipy, pandas, anndata, scanpy, decoupler, pydeseq2, statsmodels)]"
89
+
90
+ # Report-only: no --cov-fail-under gate. This is a bleeding-edge, pre-registered research
91
+ # repo mid-Phase-0 — a coverage floor would either block legitimate work or get raised by
92
+ # weakening it, neither of which is worth it yet. The number is surfaced so it is visible
93
+ # without being enforced.
94
+ - name: Run tests (with coverage)
95
+ shell: bash
96
+ run: |
97
+ set -euo pipefail
98
+ python -m pytest -q --cov=pbcheck --cov-report=term | tee pytest-coverage.txt
99
+ {
100
+ echo "### Coverage (pbcheck) — report-only, no threshold gate"
101
+ echo
102
+ echo '```'
103
+ cat pytest-coverage.txt
104
+ echo '```'
105
+ } >> "$GITHUB_STEP_SUMMARY"
106
+
107
+ lock:
108
+ name: pytest from requirements.lock (reproducibility)
109
+ runs-on: ubuntu-latest
110
+ steps:
111
+ - uses: actions/checkout@v7
112
+ with:
113
+ # tests/test_protocol_safety_check.py runs the checklist against main; a branch or
114
+ # pull-request checkout at the default depth 1 carries no main to diff against.
115
+ fetch-depth: 0
116
+
117
+ - uses: actions/setup-python@v7
118
+ with:
119
+ python-version: "3.12"
120
+ cache: pip
121
+ cache-dependency-path: requirements.lock
122
+
123
+ # Install FROM the frozen lock, then the package itself with --no-deps so pip cannot
124
+ # reach past the lock for anything. This is the reproducibility job: it answers "does the
125
+ # exact stack that generated requirements.lock still work?" The `test` job above answers a
126
+ # different question — "does a fresh resolve of pyproject.toml's unpinned bounds still
127
+ # work?" — and is the drift detector; this job does not replace it.
128
+ - name: Install from requirements.lock
129
+ run: |
130
+ python -m pip install --upgrade pip
131
+ python -m pip install -r requirements.lock
132
+ python -m pip install -e . --no-deps
133
+
134
+ - name: Run tests
135
+ run: python -m pytest -q
136
+
137
+ build:
138
+ name: build sdist/wheel + twine check + install smoke test
139
+ runs-on: ubuntu-latest
140
+ steps:
141
+ - uses: actions/checkout@v7
142
+
143
+ - uses: actions/setup-python@v7
144
+ with:
145
+ python-version: "3.12"
146
+ cache: pip
147
+ cache-dependency-path: pyproject.toml
148
+
149
+ # build/twine are packaging tooling, not part of the [dev] test/lint extra, so they are
150
+ # installed directly here rather than pulled in by every contributor's dev install.
151
+ - name: Install packaging tools
152
+ run: |
153
+ python -m pip install --upgrade pip
154
+ python -m pip install build twine
155
+
156
+ - name: Build sdist + wheel
157
+ run: python -m build
158
+
159
+ - name: twine check
160
+ run: python -m twine check dist/*
161
+
162
+ # The real smoke test: install the built wheel (not the editable checkout) into a clean
163
+ # venv and confirm the package actually imports — catches missing package-data / MANIFEST
164
+ # issues that `pip install -e .` can silently paper over.
165
+ - name: Install the built wheel into a fresh venv and import it
166
+ run: |
167
+ python -m venv wheel-venv
168
+ wheel-venv/bin/pip install --upgrade pip
169
+ wheel-venv/bin/pip install dist/*.whl
170
+ wheel-venv/bin/python -c "import pbcheck; print(pbcheck.__version__)"
171
+ wheel-venv/bin/pbcheck --version
@@ -0,0 +1,57 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .hypothesis/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+
13
+ # Environments
14
+ .venv/
15
+ venv/
16
+ env/
17
+ .env
18
+
19
+ # Data & caches (never commit downloaded datasets or the census cache)
20
+ data/
21
+ *.h5ad
22
+ *.h5
23
+ *.loom
24
+ cellxgene_census_cache/
25
+ *.zarr/
26
+ *.tiledb/
27
+ *.mtx
28
+ *.tsv.gz
29
+
30
+ # Run outputs: ignore everything, keep the directory.
31
+ pilot/results/*
32
+ !pilot/results/.gitkeep
33
+ pilot/cache/
34
+
35
+ # Coverage & logs
36
+ .coverage
37
+ htmlcov/
38
+ *.log
39
+
40
+ # Editors
41
+ .vscode/
42
+ .idea/
43
+
44
+ # Notebooks
45
+ .ipynb_checkpoints/
46
+
47
+ # OS
48
+ .DS_Store
49
+ Thumbs.db
50
+
51
+ # Amendment 4 Part A's upper-bound reproducer keeps a resume ledger beside its artifact. The
52
+ # committed record is the dated JSON/CSV, which already carries every per-seed replicate; the
53
+ # ledger is a cache, following pilot/testsel/ committing only its summary.
54
+ pilot/upper_bound_check/replicates.jsonl
55
+
56
+ # Claude Code session scratch (worktrees are ephemeral; agents and skills ARE tracked)
57
+ .claude/worktrees/
@@ -0,0 +1,52 @@
1
+ # Mirrors CI (.github/workflows/tests.yml) on purpose: `ruff check` only, no `--fix` and no
2
+ # format hooks. This is a pre-registered scientific repo and a lot of existing code predates a
3
+ # formatting policy — see the comment on the lint job in tests.yml for why `ruff format --check`
4
+ # is deliberately absent there too. A pre-commit hook that reformatted on commit would diverge
5
+ # from what CI enforces and would pollute `git blame` on files the analysis depends on.
6
+ #
7
+ # docs/PHASE0_SPEC.md and docs/AMENDMENTS.md are the frozen protocol documents (see CLAUDE.md /
8
+ # the R3 task brief) and are excluded from the fixing hooks below so their bytes are never
9
+ # touched by a mechanical pass — end-of-file-fixer would otherwise add PHASE0_SPEC.md's missing
10
+ # trailing newline.
11
+ #
12
+ # The committed RUN ARTIFACTS under pilot/ are excluded for the same reason, one degree stronger:
13
+ # they are cited evidence, and pilot/preregistration/ is additionally pinned BY HASH in
14
+ # docs/PREREGISTRATION_STRATUM_LIST.md and by scripts/freeze_stratum_list.py, which refuses to run
15
+ # against a source whose sha256 has moved. Two of them end without a trailing newline, because
16
+ # json.dump writes none: the CI candidate manifest, and pilot/gate/synthetic_gate_2026-08-15.json.
17
+ # This was not hypothetical — running `pre-commit run --all-files` while adding the freeze rewrote
18
+ # the gate artifact, whose numbers Amendment 3's addendum, the README and CHANGELOG all quote.
19
+ # The hooks had simply never been run over it (pre-commit is configured here but not installed as
20
+ # a git hook). A mechanical pass must not be able to edit evidence, so it no longer can.
21
+ repos:
22
+ - repo: local
23
+ hooks:
24
+ - id: ruff-check
25
+ name: ruff check
26
+ entry: ruff check .
27
+ language: system
28
+ types: [python]
29
+ pass_filenames: false
30
+
31
+ - repo: https://github.com/pre-commit/pre-commit-hooks
32
+ rev: v5.0.0
33
+ hooks:
34
+ - id: end-of-file-fixer
35
+ exclude: ^(docs/(PHASE0_SPEC|AMENDMENTS)\.md$|pilot/(preregistration|gate|testsel)/)
36
+ - id: trailing-whitespace
37
+ exclude: ^(docs/(PHASE0_SPEC|AMENDMENTS)\.md$|pilot/(preregistration|gate|testsel)/)
38
+ - id: check-yaml
39
+ - id: check-added-large-files
40
+ # pilot/testsel/summary.{csv,json} are committed run artifacts (~64KB / ~22KB); the
41
+ # largest tracked *source* file in the repo is ~71KB (scripts/pb_calibration_probe.py).
42
+ # 500KB gives headroom without letting an actual accidental large-file commit through.
43
+ #
44
+ # pilot/preregistration/ is excluded rather than accommodated by raising the cap. It holds
45
+ # the §1 pre-registration evidence — the whole-Census candidate manifest of CI run
46
+ # 31910799023 (6.6 MB JSON + 4.5 MB CSV) and the frozen stratum list derived from it. Those
47
+ # are deliberately committed at full size: the pre-registration's claim is that a reader
48
+ # can re-derive every number in docs/PREREGISTRATION_STRATUM_LIST.md, and a truncated or
49
+ # externally hosted artifact cannot support it. Raising the cap instead would have
50
+ # disarmed the guard for the whole tree to admit two known files.
51
+ exclude: ^pilot/preregistration/
52
+ args: [--maxkb=500]
@@ -0,0 +1,29 @@
1
+ {
2
+ "title": "pbcheck: an auditor of pseudoreplication in single-cell RNA-seq differential expression",
3
+ "upload_type": "software",
4
+ "creators": [
5
+ {
6
+ "name": "Poliakov, Alexander"
7
+ }
8
+ ],
9
+ "description": "pbcheck estimates how much an existing single-cell RNA-seq differential-expression analysis is inflated by pseudoreplication, treating individual cells as independent replicates when the true unit of replication is the donor. v0.1.0 is the single-stratum audit tool; Phase 0, a pre-registered measurement study, continues alongside it; its frozen protocol is in docs/PHASE0_SPEC.md and every deviation is recorded in docs/AMENDMENTS.md.",
10
+ "license": "bsd-3-clause",
11
+ "version": "0.1.0",
12
+ "keywords": [
13
+ "single-cell",
14
+ "scRNA-seq",
15
+ "differential-expression",
16
+ "pseudoreplication",
17
+ "pseudobulk",
18
+ "statistics",
19
+ "reproducibility"
20
+ ],
21
+ "access_right": "open",
22
+ "related_identifiers": [
23
+ {
24
+ "identifier": "https://github.com/s4s4s4s/pbcheck",
25
+ "relation": "isSupplementTo",
26
+ "resource_type": "software"
27
+ }
28
+ ]
29
+ }