Skip to content

Commit e00a40b

Browse files
committed
Parity apparatus efficiency: shard R cache, close automation loop, dedupe CI, pin dev env
Cache sharding (schema v2): tests/_r_cache.json (15.7MB monolith, opaque hash keys) becomes tests/_r_cache/ with one JSON shard per R function label and keys of the form "<label>|<sha256>". Regeneration diffs now name the function they touch, git delta-compresses the shards, and a live-R run rewrites only the shard it changed. All 2314 entries were migrated by replaying tests/parity against the old monolith (the sha256 payload is unchanged, so legacy keys are the digest part of new keys); zero orphans, full suite green cache-only. regenerate_r_cache.py, run_live_r_parity_for_changed_api.py, the sync manifest/scripts, and docs follow the new layout. Automation loop: the regen workflow now records r_commit/r_version in sync/nns_source.json itself and posts a per-function changed-entry table in the PR body, so a cache PR is self-consistent and reviewable without forensic scripts. CI dedupe: native-backend-ci ran the identical Python 3.11-3.14 matrix twice per PR push (push + pull_request events). push is now main-only, with a concurrency group cancelling superseded runs. Dev env pinning: pre-commit hooks run ruff/mypy via `python -m` in the project environment, so local results match CI instead of a foreign mypy reporting spurious numpy import errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AxBV5HmXHY5nCUvW9SwDvb
1 parent 1bf7c1e commit e00a40b

73 files changed

Lines changed: 502829 additions & 502214 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/inspect-r-api-update.yml

Lines changed: 113 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -185,24 +185,120 @@ jobs:
185185
import os
186186
from pathlib import Path
187187
188-
path = Path('tests/_r_cache.json')
189-
if not path.exists():
190-
raise SystemExit('tests/_r_cache.json was not generated')
191-
payload = json.loads(path.read_text(encoding='utf-8'))
192-
actual = payload.get('nns_version')
188+
cache_dir = Path('tests/_r_cache')
189+
shards = sorted(cache_dir.glob('*.json')) if cache_dir.is_dir() else []
190+
if not shards:
191+
raise SystemExit('tests/_r_cache/ contains no shard files')
193192
expected = os.environ['EXPECTED_VERSION']
194-
if actual != expected:
195-
raise SystemExit(f'cache version {actual!r} != dispatched version {expected!r}')
196-
entries = payload.get('entries')
197-
if not isinstance(entries, dict) or not entries:
198-
raise SystemExit('regenerated cache contains no entries')
199-
print(f'Validated {len(entries)} cache entries for NNS {actual}.')
193+
total = 0
194+
for shard_path in shards:
195+
shard = json.loads(shard_path.read_text(encoding='utf-8'))
196+
actual = shard.get('nns_version')
197+
if actual != expected:
198+
raise SystemExit(
199+
f'{shard_path.name}: cache version {actual!r} '
200+
f'!= dispatched version {expected!r}'
201+
)
202+
entries = shard.get('entries')
203+
if not isinstance(entries, dict) or not entries:
204+
raise SystemExit(f'{shard_path.name} contains no entries')
205+
total += len(entries)
206+
print(
207+
f'Validated {total} cache entries across {len(shards)} '
208+
f'shards for NNS {expected}.'
209+
)
200210
PY
201211
202212
- name: Remove transient cache files
203213
if: always()
204214
shell: bash
205-
run: rm -f tests/_r_cache.json.bak tests/_r_cache.lock
215+
run: rm -rf tests/_r_cache.bak && rm -f tests/_r_cache.lock
216+
217+
- name: Record cache provenance in sync manifest
218+
if: always()
219+
shell: bash
220+
env:
221+
R_COMMIT: ${{ steps.package.outputs.r_commit }}
222+
R_VERSION: ${{ steps.package.outputs.r_version }}
223+
run: |
224+
python - <<'PY'
225+
import json
226+
import os
227+
from pathlib import Path
228+
229+
path = Path('sync/nns_source.json')
230+
manifest = json.loads(path.read_text(encoding='utf-8'))
231+
manifest['r_commit'] = os.environ['R_COMMIT']
232+
manifest['r_version'] = os.environ['R_VERSION']
233+
path.write_text(json.dumps(manifest, indent=2) + '\n', encoding='utf-8')
234+
print(f"Manifest now records {manifest['r_repo']}@{manifest['r_commit']} "
235+
f"(NNS {manifest['r_version']}).")
236+
PY
237+
238+
- name: Summarize cache changes by function
239+
id: cachediff
240+
if: always()
241+
shell: bash
242+
run: |
243+
python - <<'PY'
244+
import json
245+
import subprocess
246+
from pathlib import Path
247+
248+
def committed(name: str) -> dict:
249+
proc = subprocess.run(
250+
['git', 'show', f'HEAD:tests/_r_cache/{name}'],
251+
capture_output=True, text=True,
252+
)
253+
if proc.returncode != 0:
254+
return {}
255+
return json.loads(proc.stdout).get('entries', {})
256+
257+
cache_dir = Path('tests/_r_cache')
258+
lines = ['### Cache changes by function', '']
259+
if not cache_dir.is_dir():
260+
lines.append('_No cache directory was generated; see the regeneration log._')
261+
else:
262+
proc = subprocess.run(
263+
['git', 'ls-tree', '--name-only', 'HEAD', 'tests/_r_cache/'],
264+
capture_output=True, text=True,
265+
)
266+
old_names = {Path(p).name for p in proc.stdout.split() if p.endswith('.json')}
267+
new_names = {p.name for p in cache_dir.glob('*.json')}
268+
rows = []
269+
for name in sorted(old_names | new_names):
270+
before = committed(name) if name in old_names else {}
271+
after = (
272+
json.loads((cache_dir / name).read_text()).get('entries', {})
273+
if name in new_names else {}
274+
)
275+
added = len(set(after) - set(before))
276+
removed = len(set(before) - set(after))
277+
changed = sum(
278+
1 for k in set(before) & set(after) if before[k] != after[k]
279+
)
280+
if added or removed or changed:
281+
label = name[: -len('.json')]
282+
rows.append(
283+
f'| `{label}` | {len(before)} | {len(after)} '
284+
f'| {added} | {removed} | {changed} |'
285+
)
286+
if rows:
287+
lines += [
288+
'| function | before | after | added | removed | changed |',
289+
'| --- | --- | --- | --- | --- | --- |',
290+
*rows,
291+
]
292+
else:
293+
lines.append('_No cache entries changed._')
294+
Path('cache-diff.md').write_text('\n'.join(lines) + '\n', encoding='utf-8')
295+
print('\n'.join(lines))
296+
PY
297+
{
298+
echo 'summary<<CACHE_DIFF_EOF'
299+
cat cache-diff.md
300+
echo 'CACHE_DIFF_EOF'
301+
} >> "$GITHUB_OUTPUT"
206302
207303
- name: Verify committed-cache mode
208304
id: verify
@@ -246,10 +342,13 @@ jobs:
246342
- Live regeneration result: `${{ steps.regenerate.outcome }}`
247343
- Cache-only verification result: `${{ steps.verify.outcome }}`
248344
249-
The exact R source package was installed and the committed parity cache was regenerated. Any remaining Python/R parity mismatches are retained in the workflow diagnostics and should be repaired against this R-authored baseline.
345+
The exact R source package was installed and the committed parity cache was regenerated. `sync/nns_source.json` records this R commit as the behavioral-truth provenance. Any remaining Python/R parity mismatches are retained in the workflow diagnostics and should be repaired against this R-authored baseline.
346+
347+
${{ steps.cachediff.outputs.summary }}
250348
add-paths: |
251349
tests/_r.py
252-
tests/_r_cache.json
350+
tests/_r_cache/**
351+
sync/nns_source.json
253352
254353
- name: Report parity status
255354
if: always()

.github/workflows/native-backend-ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
name: Native backend CI
22

3+
# pull_request covers every branch with an open PR; push is limited to main so
4+
# a PR-branch push does not run the identical matrix twice (once per event).
35
on:
46
pull_request:
57
push:
8+
branches: [main]
69

710
permissions:
811
contents: read
912

13+
# A superseded push to the same ref cancels the in-flight run.
14+
concurrency:
15+
group: native-backend-ci-${{ github.event.pull_request.number || github.ref }}
16+
cancel-in-progress: true
17+
1018
jobs:
1119
native-backend:
1220
name: Python ${{ matrix.python-version }}

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ __pycache__/
1616

1717
# R cache lock and fresh-regeneration backup
1818
tests/_r_cache.lock
19-
tests/_r_cache.json.bak
19+
tests/_r_cache.bak/
2020

2121
# Vendored R NNS local-install build artifacts
2222
tools/NNS/src/*.o

.pre-commit-config.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Local hooks run `python -m ...` with language: system so they use the
2+
# project environment's interpreter — the same one that has numpy and the
3+
# package installed. A mypy/ruff binary from a different environment produces
4+
# spurious import-not-found noise (numpy stubs missing) that masks real errors
5+
# CI would catch; running through the project env keeps local == CI.
6+
repos:
7+
- repo: local
8+
hooks:
9+
- id: ruff
10+
name: ruff check (project env)
11+
entry: python -m ruff check --force-exclude
12+
language: system
13+
types_or: [python, pyi]
14+
require_serial: true
15+
- id: mypy
16+
name: mypy (project env)
17+
entry: python -m mypy
18+
language: system
19+
types_or: [python, pyi]
20+
pass_filenames: false
21+
require_serial: true

docs/install.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ uv run ruff check .
5050
uv run mypy
5151
```
5252

53+
Optionally install the pre-commit hooks, which run `ruff` and `mypy` through
54+
the project environment's interpreter so local results match CI (a `mypy`
55+
binary from another environment lacks the project's dependencies and reports
56+
spurious import errors):
57+
58+
```bash
59+
uv run pre-commit install
60+
```
61+
5362
The default parity suite is cache-backed and does not require `Rscript`.
5463
`Rscript` and the R `NNS` package are needed only when regenerating parity caches
5564
or running live R comparison scripts.

docs/parity.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ and remains the native C++ foundation for accelerated partial-moment routines.
99
Full package parity is **not** claimed. Parity is bounded by the committed tests
1010
and cache:
1111

12-
- `tests/_r_cache.json` — cache-only R result fixtures (2,406 keyed entries,
13-
schema version `1`, `nns_version == "13.0"`),
12+
- `tests/_r_cache/` — cache-only R result fixtures, sharded into one JSON
13+
file per R function label (keys are `<label>|<sha256>`, schema version
14+
`2`, every shard stamped with the generating `nns_version`),
1415
- `tests/parity/` — public behavior parity checks,
1516
- `tests/invariants/` — Python-native contracts and invariants, and
1617
- `tests/fixtures/original_tests_expected.json` — adopted original R tests
@@ -85,9 +86,10 @@ If full regeneration is slow or unstable, regenerate deterministic chunks one
8586
file at a time, for example
8687
`python scripts/regenerate_r_cache.py -- -n 0 tests/parity/test_core.py`, then
8788
continue through the remaining parity files. The committed result must remain a
88-
single valid `tests/_r_cache.json` with `nns_version == "13.0"`,
89-
`schema_version == 1`, and non-empty `entries`; `scripts/regenerate_r_cache.py`
90-
enforces those guardrails after the pytest run.
89+
valid `tests/_r_cache/` shard directory: every shard carries the same
90+
`nns_version`, `schema_version == 2`, a `label` matching its filename, and
91+
non-empty `entries`; `scripts/regenerate_r_cache.py` enforces those
92+
guardrails after the pytest run.
9193

9294
Validate the regenerated cache offline:
9395

@@ -130,7 +132,7 @@ NNS R API change
130132
### Fix rules (applied by the maintainer; later by the agent)
131133

132134
- Edit **`src/nns/**` only**. Never edit `extern/NNS-core/**`, `tools/NNS/**`,
133-
or `tests/_r_cache.json` to make a check pass.
135+
or `tests/_r_cache/**` to make a check pass.
134136
- Classify the root cause and act accordingly:
135137
- **Python port bug** → fix in `src/nns/**`.
136138
- **R changed behavior** → do not chase a cache value; cache regeneration is a

docs/plot_parity_policy.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ colors and which element they sit on*, never rendered images.
3636

3737
- Numeric return values (scalars, vectors, matrices, nested result dicts) from
3838
every ported function, against committed R fixtures and the committed R cache
39-
(`tests/_r_cache.json`).
39+
(`tests/_r_cache/`).
4040
- Structural contracts (result keys, shapes, dtypes, finiteness) via the
4141
invariant suite.
4242

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dependencies = [
4242
dev = [
4343
"hypothesis",
4444
"mypy",
45+
"pre-commit",
4546
"pytest",
4647
"pytest-benchmark",
4748
"pytest-cov",

0 commit comments

Comments
 (0)