@@ -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()
0 commit comments