Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add nvflare-fed-stats agent skill and dataset-target classification in agent inspect#4890

Open
chesterxgchen wants to merge 39 commits into
NVIDIA:mainfrom
chesterxgchen:fedstats-agent-skill
Open

Add nvflare-fed-stats agent skill and dataset-target classification in agent inspect#4890
chesterxgchen wants to merge 39 commits into
NVIDIA:mainfrom
chesterxgchen:fedstats-agent-skill

Conversation

@chesterxgchen

Copy link
Copy Markdown
Collaborator

Summary

Adds nvflare-fed-stats — a data-first, fully automatic agent skill for federated statistics over tabular and image datasets — plus the product-side capability it consumes: dataset-target classification in nvflare agent inspect.

The skill (skills/nvflare-fed-stats/)

  • Data-first UX: point at tabular data (CSV/TSV/parquet; pre-split per-site directories or one flat file the skill partitions deterministically) or image folders (PNG/JPEG/BMP/TIFF; DICOM/NIfTI via the format loader), and it runs end-to-end with no interaction — the same "one command in, validated result out" contract as the conversion skills. Feature names come from the header row or user-supplied names (prompt/README) — never invented; missing required inputs fail closed with a precise report.
  • Statistic selection with an honest support mapping: supported (count/sum/mean/stddev/var/histogram/quantile), noise-protected (min/max honored only through the default noise filter, disclosed as estimates), unsupported (categorical/correlations — reported, never approximated). count is always included; var/stddev auto-expand their count+sum+mean second-round prerequisites.
  • Privacy filters on by default, never weakened (StatsJob wires min-count, min/max-noise, bin-cap cleansers per client); reports state the applied values as heuristic disclosure-risk reductions, never as privacy guarantees; aggregates only — no raw rows/cell values/pixels in any output.
  • Validation ladder ending in output completeness, per-site parity (independent recompute, exact at the controller's persisted precision), and global parity (recompute over the union of partitions). Runtime facts verified against actual FedStatsRecipe simulator runs, not transcribed from docs: the feature-first output hierarchy, the precision-4 rounding, histogram leaf shapes ([low, high, count] triples), and the bin-cap boundary (20 bins first passes at 206 effective rows).
  • Lead-DS requirements baked in: per-feature missing rates with cross-site divergence flagging, skew caveats for equal-width bins, dtype-drift detection, Simpson's case-mix caveat on Global, scanner-calibration caveat for images, CT Hounsfield-rescale guidance.

nvflare agent inspect: dataset targets (nvflare/tool/agent/dataset_inspect.py)

One inspection architecture for all skills: code targets keep the AST path; data-only directories now classify as tabular_dataset/image_dataset with a metadata-only dataset block — site layout, feature names (strong header rule: every numeric-bodied column non-numeric, names unique/non-empty — masked rows like SUPPRESSED,100 never leak as names), dtype classes, per-site row/file counts, cross-site schema_agreement (names, column counts, dtypes, shard drift), parquet metadata via optional pyarrow, per-site image_formats and sampled pixel_depth — and recommend nvflare-fed-stats. Walks are bounded (data-file budget + 50k entry cap, symlink-free), every data read is accounted in a scan sub-block, and the full contract lives in the module docstring. Real code targets keep priority: only unknown targets or utility-only frameworks (numpy helpers) are overridden. The CLI gains --max-files/--max-file-bytes.

Also included

  • 18 evals with synthetic healthcare fixtures (27-feature schema, 3 non-IID pre-split sites, deliberate differential missingness), including README-injection resistance, min/max-noised disclosure, headerless fail-closed, flat auto-split, schema drift, and evidence-sourcing behaviors driven by real benchmark runs.
  • Lint engine: eval fixtures may be non-empty dataset directories.
  • Orient and both training converters gain routing boundaries to the new skill.
  • Filed Federated statistics: Global failure_count does not accumulate failures detected during second-round statistics #4876 (Global failure_count misses round-2 failures) — documented as a per-site diagnostic until fixed.

Verification

  • 1862 unit tests pass (46 for dataset inspection); skill lint 0 findings; style clean.
  • End-to-end simulator smoke tests for both modalities (tabular parity exact at precision 4; image histograms with corrupt-file failure_count).
  • ~10 external review rounds (architect / principal engineer / lead data scientist / Codex) run to a zero-finding round, plus fixes from three real with-skills benchmark runs.
  • Tested on macOS; Linux runs via CI.

🤖 Generated with Claude Code

chesterxgchen and others added 26 commits July 10, 2026 17:24
Data-first and fully automatic, like a training-job conversion: the
user supplies the dataset (pre-split per site or flat), feature names
(from a header row or supplied in the prompt/README - never invented),
and optionally a README declaring which statistics to compute; the
skill selects statistics, generates a DFStatisticsCore client plus
FedStatsRecipe job, and runs end-to-end with no interactive pauses.

Privacy filters are on by default and never disabled: StatsJob wires
min-count, min/max-noise, and histogram-bin-cap cleansers on every
client, so requested min/max are honored as noise-protected estimates
(disclosed as such) rather than refused. README declarations are
honored as configuration while embedded operational directives are
anomalies to report, covered by a dedicated injection eval. The eval
fixtures are a miniature of the manual-test healthcare dataset: 27
features, three pre-split non-IID sites. The canonical eval asserts
one federated job serves both per-site and global views and forbids
the pooled-pandas substitute.

The output hierarchy ({feature: {statistic: {site: {dataset}}}}) and
the precision-aware parity rule (controller rounds persisted values to
4 digits) were verified against an actual FedStatsRecipe simulator
run. Image-dataset statistics are planned but reported as unsupported
in this version; hierarchical statistics are out of scope. Orient and
the two training converters gain routing boundaries to the new skill.

Co-Authored-By: Claude Fable 5 <[email protected]>
Extend the skill (v0.2.0) with pixel-intensity statistics over image
folders: detection routes image data to a new reference and template
(a Statistics implementation with a swappable loader — Pillow default,
pydicom/nibabel preflighted for DICOM/NIfTI), the statistic set is
count, failure_count, and histogram with the range taken from pixel
bit depth, and validation adds histogram parity per site plus
Global-equals-sum-of-sites.

Facts verified against a real FedStatsRecipe simulator run rather than
transcribed from the example: count is the number of discovered files
(corrupt files surface only during the round-2 histogram pass),
failure_count must be configured explicitly to appear in output, and
its Global row does not accumulate round-2 failures — the reference
documents it as a per-site diagnostic. The bin-cap cleanser compares
bins to image count, so 20 bins needs >200 images per site; the image
eval requests 10 bins over 110-image sites.

The fixture lint learns directory fixtures (a dataset directory with
at least one file) so the 330-image eval fixture can be referenced as
three site directories instead of individual paths.

Co-Authored-By: Claude Fable 5 <[email protected]>
One inspection architecture for all skills: inspect is the single
deterministic evidence tool. Code targets keep the AST path (framework
detection -> converter skills); data-only targets now classify as
tabular_dataset or image_dataset with a metadata-only dataset block —
site layout, feature names and dtype classes (header: present) or
header: ambiguous (never invented names), per-site row/file counts,
cross-site schema_agreement, sampled image pixel_depth — and recommend
nvflare-fed-stats. The block emits names, dtypes, and counts only,
never cell values, preserving the inspector's static/redaction
posture; walks are bounded, sorted, and symlink-free, with
counts_approximate flagged at the file limit. Dataset classification
runs only when code classification found nothing, so training repos
that contain CSVs stay code targets.

The fed-stats skill drops its hand-rolled inspection: step 2 now
consumes the dataset block (header rule, schema precondition, bin-cap
counts), with the rules kept in references as the contract the CLI
implements. Orient inherits evidence-based routing for data targets
for free. pandas/numpy import detection was deliberately not added:
data modality is the routing signal for statistics, not script
libraries.

Co-Authored-By: Claude Fable 5 <[email protected]>
…mits

Architecture-review and follow-up findings:
- The dataset block now carries its own scan accounting (files_read,
  bytes_read) so inspect no longer under-reports the bounded data reads
  it performs; the JSON contract test pins the dataset key.
- Sharded sites aggregate row counts across all tabular files and are
  always marked row_count_approximate (per-shard headers are
  unknowable), so bin-cap decisions cannot trust an undercount from the
  first shard alone.
- Parquet sites get real metadata (names, dtype classes, exact row
  counts) via optional pyarrow; without it, schema_available: false
  routes the caller to the ambiguous-header fallback instead of
  silently supplying nothing.
- The walk's max_files limit now counts data files, with a separate
  50k entries safety cap, so non-data clutter cannot exhaust the
  budget before any data file is seen and hide the dataset entirely.
- modality: mixed is documented as intentionally unrouted, and the
  Global failure_count quirk is now tracked as NVFlare issue NVIDIA#4876
  rather than living only as a doc workaround.

Co-Authored-By: Claude Fable 5 <[email protected]>
Architect/principal review findings, all verified before fixing:
- Directory-only trees are now bounded: MAX_WALK_ENTRIES counts
  directories as well as files, so a file-free tree cannot walk
  unbounded.
- Feature names are sanitized on emission: control characters stripped,
  length capped at 120 (feature_names_truncated flags a hit), applied
  to CSV and parquet names alike — closing the metadata-only
  guarantee's blind spot where a 512KB quoted header cell became a
  JSON feature name.
- schema_agreement now compares dtype classes alongside names and
  column counts: same names with drifting dtypes reports dtypes_differ
  (not analysis-ready for numeric statistics), and the skill fails
  closed on it pre-generation.
- Parquet sharded sites sum exact row counts across every shard's
  footer metadata instead of reporting the first shard as exact;
  unreadable shards mark the total approximate.
- Degenerate files keep a stable site shape (all schema keys present
  as nulls); nan/inf tokens no longer count as numeric evidence; the
  shard-loop dead code is gone.
- The module docstring now carries the dataset-block contract table,
  the worst-case read bound (max_files x max_file_bytes), and the
  recorded rationale for keeping the data walk separate from the code
  walk (they bound different costs).

Seven new tests (mixed modality, dtype drift, dir-only bound, name
sanitization, stable shape, nan tokens, sharded parquet with pyarrow).
The parquet eval-coverage gap is recorded in the repo-only fixture
README, where the runtime-boundary lint requires such notes to live.

Co-Authored-By: Claude Fable 5 <[email protected]>
Codex review findings, all reproduced before fixing:
- Mixed modality no longer requires an exact tie: when the minority
  modality is a material share (>=10%) of the data files the dataset
  classifies as mixed and stays unrouted; below the threshold a stray
  plot.png beside CSVs does not flip the classification, and image-only
  sites under a tabular majority no longer receive parquet-shaped null
  schemas.
- The read-bound contract now tells the truth about parquet: text and
  image reads are capped at max_file_bytes each; parquet metadata reads
  parse only the footer via pyarrow, are not capped by max_file_bytes,
  and are accounted in scan at file size as the upper bound.
- Shard schema disagreement inside one site is no longer silent, for
  parquet AND sharded CSV alike: parquet shards are compared by
  (names, dtypes) signature, CSV shards by first-row field count, and a
  site mixing text and parquet formats is never presented as exact.
  Disagreement flags shard_schema_consistent: false, forces
  row_count_approximate, and surfaces in schema_agreement as a
  shards_differ mismatch so the skill fails closed on it.

Six new tests: the 2-CSV+1-PNG repro, the stray-image threshold case,
the image-only-site shape, parquet shard disagreement, CSV shard
column-count drift, and the mixed-format site.

Co-Authored-By: Claude Fable 5 <[email protected]>
Closes the architect/principal review's remaining code findings:
- nvflare agent inspect gains --max-files and --max-file-bytes so
  datasets beyond the 250-file default (our own image eval fixture is
  331 files) can get exact counts instead of permanently approximate
  ones; the help text now also discloses that data directories are
  classified by reading bounded file headers, metadata only.
- A classified dataset keeps a single recommendation: orient is no
  longer appended beside nvflare-fed-stats when the walk truncates.
- The image eval states the truncation trap it tests: with 331 fixture
  files over the default cap, the agent must verify per-site counts
  directly (or re-run inspect with raised limits) before bin-cap
  decisions, per the counts_approximate rule.
- The skill's CLI fallback is version-explicit: 2.8.x CLIs have no
  dataset block, and FedStatsRecipe is in 2.8.0, so
  min_flare_version 2.8.0 stands with the reference rules applying
  directly there.

Co-Authored-By: Claude Fable 5 <[email protected]>
…tion

The 10% minority-share rule answered no articulated use case and, as
codex showed, still routed an image-only site inside a tabular-majority
dataset as tabular_dataset with a bare schema-less site entry. File
counts are the wrong basis entirely: one CSV can be a complete dataset
while an image dataset inherently holds hundreds of files.

Modality is now decided per site against the three real shapes:
- tabular dataset with stray images (<=2 per site, e.g. exported
  plots) stays tabular;
- image dataset with companion tabular metadata (<=2 files per site,
  e.g. labels.csv beside the scans) stays image, with the companion
  flagged per site as tabular_companions and never treated as a
  statistics target;
- anything else - an image-only site among tabular sites, or
  materially both modalities - is mixed and stays unrouted.
Every site now reports tabular_files/image_files so mixed blocks are
self-explanatory.

CSV shard disagreement detection is header-aware, not width-only: a
shard whose first row is header-like (text where the site dtypes are
numeric) must carry the site's feature names, so same-width renames
(occupation -> job) flag shards_differ; a repeated identical header or
a headerless data-first shard stays consistent.

Co-Authored-By: Claude Fable 5 <[email protected]>
…apes

Review-until-clean round: a clean mixed dataset previously produced an
empty recommendation list, stranding the consumer - mixed is
definitionally ambiguous and routing ambiguity is orient's job, so
mixed now recommends nvflare-orient (single recommendation; the
converter-plus-orient companion behavior on findings is unchanged).
COMPANION_MAX_TABULAR_PER_SITE rises from 2 to 4: train/val/test label
files beside scans are the common imaging shape and must not push a
dataset to mixed. CLI walk-limit args ignore non-positive values
instead of passing them into the walk.

Co-Authored-By: Claude Fable 5 <[email protected]>
…count

Self-review of the previous round found its own regression: raising the
companion cap to 4 widened the both-consistent window, flipping a
tabular site with 3 CSV shards plus one stray plot to mixed. The
both-consistent case (tiny sites inside both tolerance windows) is now
tie-broken by total file majority - the substantive side wins, a dead
tie stays mixed - so 2 CSVs + 1 stray PNG is tabular, 1 CSV + 12 scans
is image, and 1 CSV + 1 PNG is genuinely ambiguous and routes to
orient. Also, a detected repeated shard header is no longer counted as
a data row, since the header-aware comparison already identifies it.

Co-Authored-By: Claude Fable 5 <[email protected]>
A silently-ignored invalid --max-files was the residual from the
previous round's CLI change: non-positive values now fail at parse
time with a clear message instead of falling back to defaults
unannounced, and the behavior is tested.

Co-Authored-By: Claude Fable 5 <[email protected]>
DICOM needs dedicated loaders plus domain handling (Hounsfield rescale
for CT, slice-vs-study count semantics) that this version does not
encode, so DICOM/NIfTI are reported as not yet supported rather than
half-handled: the skill names the supported formats, never improvises
a loader, and never histograms raw stored values as calibrated
intensities. The DICOM eval flips from dependency-preflight to
not-supported reporting.

Co-Authored-By: Claude Fable 5 <[email protected]>
Clarified scope: DICOM/NIfTI are supported like every other image
format - the format-specific loader (pydicom/nibabel/ITK) is import-
preflighted and a missing loader fails closed, same rule as fastdigest.
The large-file restriction applies only to repo test fixtures, which
stay small PNGs. The loader section gains the medical-format
correctness rules: CT stored values become Hounsfield Units only after
RescaleSlope/RescaleIntercept with a declared HU range, count means
slices for DICOM series and volumes for NIfTI (say which in the
report), and cross-site intensity histograms carry a scanner/protocol
calibration caveat alongside the case-mix one. The DICOM eval asserts
the rescale and count-semantics behaviors.

Co-Authored-By: Claude Fable 5 <[email protected]>
Codex findings: symlink entries consumed no walk budget (30 symlinks
under a 10-entry cap returned truncated=False), and sorted(iterdir())
materialized unbounded listings before the cap applied. Entries now
count before the symlink skip, and each directory listing is consumed
with a bounded islice take before sorting - the cap now bounds work
done, with deterministic order preserved within the budget. The image
reference names site["image_files"] (not data_files, which includes
companion labels) as the bin-cap sizing field.

Co-Authored-By: Claude Fable 5 <[email protected]>
Principal/lead-DS review round plus the flat-layout clarification:
- A flat single-source dataset is a normal input, not an edge: inspect
  reports layout flat with one '.' site and full schema, and the skill
  splits it into the requested number of seeded per-site partitions. A
  new eval (fedstats-flat-auto-split, 1000-row combined fixture) covers
  the path, which had zero eval coverage since the canonical eval went
  pre-split: site count from the request (fail closed when absent),
  deterministic seeded partitions, seed and policy stated, parity
  against the same partitions.
- Root-level data files beside site directories now classify as
  layout root_and_site_directories: an ambiguous site mapping the skill
  must resolve explicitly instead of treating '.' as a site.
- Lead-DS caveats encoded: sampled dtypes can drift from full-file
  runtime dtypes (named as the cause behind runtime-missing features);
  undeclared histogram ranges vary between runs because the min/max
  noise is re-drawn (declare a range for reproducible bins); companion
  label files earn an offered follow-up tabular run (label shift QA).

Co-Authored-By: Claude Fable 5 <[email protected]>
Codex findings, highest first:
- Header inference required only one text-over-numeric column, so a
  masked first data row like SUPPRESSED,100 leaked cell values as
  feature names. The rule now demands EVERY numeric-bodied column carry
  a non-numeric first value; the repro classifies ambiguous and emits
  no names. The residual (a first data row non-numeric in every numeric
  column is indistinguishable from a header by construction) is
  documented: masked/sentinel datasets should declare names.
- Schemas wider than the 512-column cap now set columns_truncated (CSV
  and parquet) instead of silently hiding drift past the cap.
- The row-count fallback adds the final unterminated line, so a
  201-row file without a trailing newline reports 201 exact instead of
  200 with approximate: false - a bin-cap-sensitive undercount.
- Bin-cap boundary corrected everywhere: the cleanser needs bins under
  round(10% of effective count), so 20 bins first passes at 206 rows,
  not 201; all four docs now state the real boundary. Existing fixture
  sizes (min effective 223) clear it.

Co-Authored-By: Claude Fable 5 <[email protected]>
Codex round, highest first:
- Header inference additionally requires unique, non-empty sanitized
  names: SUPPRESSED,SUPPRESSED over a numeric body no longer leaks
  masked cell values as feature names (a valid header names every
  column uniquely). Parquet metadata with duplicate/empty names is
  flagged feature_names_invalid since downstream readers mangle it, and
  the skill fails closed on the flag.
- A data root containing a helper script with only skill-less framework
  imports (numpy/sklearn utility buckets) no longer classifies as
  training_repository with no routing: dataset classification also runs
  when the detected framework has no converter skill, and a classified
  dataset wins target_type and the fed-stats recommendation. Real
  converter frameworks (pytorch/lightning) keep priority, pinned by the
  existing test.
- columns_truncated now has consequences: the skill fails closed on
  schemas wider than the cap unless the user declares a feature subset.
- Image sites report per-site image_formats so the loader is chosen
  from evidence; the reference and template say DICOM/NIfTI sites must
  extend discovery extensions and swap _load_image for the preflighted
  loader (pixel_depth stays null for non-PIL formats by design).
- The fallback header rule in the skill reference no longer describes
  the weaker pre-fix heuristic that could reintroduce the leak on old
  CLIs; it now states the full rule including uniqueness.

Co-Authored-By: Claude Fable 5 <[email protected]>
Codex round on the previous fix:
- The no-converter-skill gate was too broad: it let a dataset override
  FLARE job sources, exported jobs, and real import-only training
  frameworks (TensorFlow/XGBoost). The override now applies only to
  unknown targets or training repositories whose sole detected
  framework is a utility bucket (frameworks.UTILITY_FRAMEWORKS, i.e.
  numpy) - real code targets keep priority, pinned by two new tests
  (FLARE job beside CSVs, TensorFlow repo beside CSVs).
- The module contract no longer overstates the guarantee: it now says
  values from rows classified as data are never emitted, names come
  only from the strong header rule, and a masked first row satisfying
  every header signal (distinct non-empty tokens over all-numeric
  columns) is emitted by construction - declare names for
  masked/sentinel datasets. A test pins that exact boundary so any
  future rule change is deliberate.
- The image template's discovery now uses endswith over the extension
  tuple, so compound extensions like .nii.gz actually match when
  agents extend IMAGE_EXTENSIONS as the guidance instructs.

Co-Authored-By: Claude Fable 5 <[email protected]>
Benchmark run 800690 showed Codex skipping stats-job-validation.md and
reading NVFLARE library source (guessing the nonexistent
nvflare.recipe.recipe path along the way) to learn where the output
JSON lands - a fact the skill already documents. The image reference
(which the agent did read) now states the output location inline and
points at the validation reference for the rest, and the image eval
judges evidence sourcing: runtime facts from references, not
site-packages exploration.

Co-Authored-By: Claude Fable 5 <[email protected]>
The instruction belongs in the always-read file, not only in a
reference the agent may skip (benchmark run 800690 showed exactly that
skip): runtime facts come from the skill references and CLI outputs
BEFORE NVFLARE library source, which is a last resort that never
licenses a replacement strategy. The converters already carry their
variant of this rule; fed-stats was the one missing it.

Co-Authored-By: Claude Fable 5 <[email protected]>
Benchmark evidence: the with-skills image run probed pandas - a
tabular-only dependency the image path never touches - and the raising
import produced ModuleNotFoundError noise for the failure classifier.
Step 4 now scopes dependencies to the detected modality (tabular needs
pandas for DataFrames; images need numpy plus Pillow or the format
loader, never pandas) and prescribes non-raising preflights
(importlib.util.find_spec) so absent optional deps report cleanly
instead of stack-tracing. The image reference states pandas is not an
image-path dependency, and the image eval judges both behaviors.

Co-Authored-By: Claude Fable 5 <[email protected]>
Skill-change review findings:
- The intro still said 'point at tabular data' - the skill has carried
  the image path since v0.2; it now says tabular or image.
- Step 5 was silently tabular-only (generate from df_stats_client.py)
  while steps 3-4 branch both modalities; it now names the image branch
  first so an image-path agent following the numbered workflow is never
  pointed at the wrong template.
- Step 4's 'never pandas' for images conflicted with the image
  reference's own companion-labels follow-up (a tabular run that needs
  pandas); the rule is now scoped, not absolute.
- Restored the meaning my earlier trim destroyed in step 3's
  reference-loading clause.
Accepted duplications (min/max noised x3, fail-closed inputs x3, bin
cap x4 docs) re-verified consistent; find_spec-vs-broken-install noted
as covered by the simulator rung.

Co-Authored-By: Claude Fable 5 <[email protected]>
Run 321039: the agent-authored parity checker guessed the stats JSON
schema twice (a 'bins' key that does not exist; a scalar count treated
as a nested object) before self-correcting. The references documented
the hierarchy but never the leaf shapes - facts we verified in the
original smoke test and failed to write down. The validation reference
now states them (scalars are plain numbers; histogram leaves are
[low, high, count] triples; quantiles are percentile->value mappings)
plus the rule that closes the class: probe the actual output JSON
before writing any checker and derive shapes from the file, never from
guessed keys. Both parity evals judge the behavior.

Co-Authored-By: Claude Fable 5 <[email protected]>
The manual-test datasets are offline assets, not part of this PR; the
fixture README no longer references their local folder name.

Co-Authored-By: Claude Fable 5 <[email protected]>
@codecov-commenter

codecov-commenter commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.77108% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.90%. Comparing base (d2aba6b) to head (d35a0d8).

Files with missing lines Patch % Lines
nvflare/tool/agent/dataset_inspect.py 92.78% 22 Missing ⚠️
nvflare/tool/agent/agent_cli.py 85.71% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4890      +/-   ##
==========================================
+ Coverage   60.77%   60.90%   +0.13%     
==========================================
  Files         977      978       +1     
  Lines       93211    93539     +328     
==========================================
+ Hits        56649    56974     +325     
- Misses      36562    36565       +3     
Flag Coverage Δ
unit-tests 60.90% <92.77%> (+0.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The eval image dataset is now created at eval time by
generate_images.py (seeded, deterministic - same 110 PNGs per site
plus the corrupt file), which the eval prompt explicitly asks the
agent to run first. No binary image files remain in the repo; the
fixture lint is satisfied by the script, and data generation stays a
user-requested effect consistent with the skill's authorization rules.

Co-Authored-By: Claude Fable 5 <[email protected]>
@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds federated statistics support for dataset-focused agent workflows. The main changes are:

  • New nvflare-fed-stats skill with tabular and image statistics templates.
  • Dataset classification in nvflare agent inspect for tabular and image inputs.
  • New inspect scan-limit flags for bounded dataset reads.
  • Eval fixtures, references, and routing updates for the new skill.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
skills/nvflare-fed-stats/assets/image_stats_client.py Adds image statistics handling with upfront unreadable-file detection, deduplicated failures, and all-failed histogram handling.
nvflare/tool/agent/dataset_inspect.py Adds bounded dataset inspection for tabular and image targets with schema, count, format, and scan metadata.
dev_tools/agent/skills/checks/lints.py Allows skill eval fixtures to reference non-empty dataset directories.

Reviews (12): Last reviewed commit: "Document what recipe.execute returns so ..." | Re-trigger Greptile

Comment thread skills/nvflare-fed-stats/assets/image_stats_client.py Outdated
chesterxgchen and others added 2 commits July 10, 2026 18:09
Greptile P2 on the PR: failure_images accumulated across histogram()
calls, so a retried pass counted the same corrupt file twice, inflating
failure_count (a pattern inherited from the official image_stats
example). Failed paths are now a deduplicated set, making
failure_count retry-safe.

Co-Authored-By: Claude Fable 5 <[email protected]>
Skill .py files are loaded into LLM context at runtime, so every header
line is a recurring per-invocation token cost. The 2-line SPDX header
carries the same license terms as the 13-line boilerplate at about a
third of the tokens. The license checker now requires the SPDX form
under skills/ and the full boilerplate everywhere else, and skills/ is
added to the checked folders (it was previously not scanned at all).

Co-Authored-By: Claude Fable 5 <[email protected]>
Comment thread skills/nvflare-fed-stats/assets/image_stats_client.py
Greptile P1 on the PR: failure_count is collected with the first-round
statistics, but the template only discovered unreadable files during
the round-2 histogram pass - so the server consumed 0 and the privacy
cleansers treated corrupt files as readable. initialize() now runs a
cheap header verification (PIL verify, no full decode; adaptation
point notes pydicom's stop_before_pixels for DICOM) so failure_count
is correct from the first collection. Verified against a simulator
run: the Global failure_count row now reports 1 for the corrupt-file
fixture where it previously reported 0 - this also neutralizes the
user-visible effect of NVFlare issue NVIDIA#4876 for header-detectable
corruption; the reference keeps the scoped caveat for decode-time
failures only.

Co-Authored-By: Claude Fable 5 <[email protected]>
Comment thread skills/nvflare-fed-stats/assets/image_stats_client.py Outdated
chesterxgchen and others added 3 commits July 10, 2026 18:54
Greptile P1 follow-up: Image.verify() is header-only, so a file with a
valid header but broken pixel data passed round-1 verification and
failed only during the round-2 histogram - after the server had
consumed failure_count for count-minus-failures privacy decisions.
initialize() now verifies each file by calling _load_image itself, so
verification cannot disagree with the histogram's read path by
construction; adapted clients that swap the loader inherit consistent
verification for free. The trade-off (each image decodes twice) is
documented with the relaxation note for very large datasets. Simulator
re-verified: Global failure_count still 1 for the corrupt fixture.

Co-Authored-By: Claude Fable 5 <[email protected]>
…ata reading

The dependency step sat after data examination and only guarded "recipe
construction or simulation", so an exploratory raising import (e.g. pandas
on tabular data) could hit ModuleNotFoundError before the install ever ran —
exactly the ordering error the conversion skills' wording was designed to
prevent. Align fedstats with that ordering: install right after the static
inspect step, guarded against any import-level preflight or exploratory
data reading.

Co-Authored-By: Claude Fable 5 <[email protected]>
Benchmark run 986631 (claude, tabular healthcare): the agent's parity
checker followed this reference's exact-at-precision rule for Global
values and failed on two of 23 features (stddev off by exactly 1 ulp:
0.4958 vs 0.4957, 1.6611 vs 1.6612), then correctly diagnosed the
cause with a full-precision comparison and loosened its tolerance. The
product rounds each site's second-round variance term to the persisted
precision before the server sums them, so global stddev/var carry up
to site-count ulps of double-rounding error versus a full-precision
recompute. The reference was the bug: 'exact at persisted precision'
is the per-site contract, not the Global one. Global tolerances are
now defined (count exact; sum/mean 1 ulp; stddev/var site-count ulps)
with the mechanism explained, so checkers stop false-failing.

Co-Authored-By: Claude Fable 5 <[email protected]>
Comment thread skills/nvflare-fed-stats/assets/image_stats_client.py Outdated
chesterxgchen and others added 6 commits July 10, 2026 20:11
Greptile P1: a site where every image is unreadable raised out of
histogram(), turning a representable data-quality condition (round-1
already reports count == failure_count) into a task exception that
aborted the federated job for the healthy sites. The histogram now
returns proper bin edges with zero counts for such a site; the report
surfaces the condition through failure_count. Verified: an all-corrupt
site yields 10 zero-count bins with failure_count=3 and no exception.

Co-Authored-By: Claude Fable 5 <[email protected]>
Second benchmark occurrence of the same noise class (codex guessed
nvflare.recipe.recipe; claude guessed
nvflare.app_common.executors.statistics_task_handler): agents doing
last-resort source reading invent module paths and generate exit-1
ImportError noise. The references-before-source rule now adds: locate
modules by grepping the installed tree, never by guessing import
paths.

Co-Authored-By: Claude Fable 5 <[email protected]>
Run 913878's source dive answered a real gap: the references said
'numeric features only' and the template said 'return the DataFrame',
but nothing said WHO drops the text columns. The agent went to the
executor source to find out (the product filters to numeric dtypes
client-side). Now stated explicitly in the mapping reference and the
tabular template: return the full DataFrame, never drop columns in
load_data; the skill's job is to know and name the exclusions.

Co-Authored-By: Claude Fable 5 <[email protected]>
Per the product contract: fed stats accepts categorical input and
filters non-numeric features automatically. The requirement bullet
that read 'must compute statistics only for numeric features' could be
taken as an instruction to filter up front; it now states the product
does the filtering and the agent's duty is naming the exclusions and
reporting missing rates.

Co-Authored-By: Claude Fable 5 <[email protected]>
Even reworded as 'the product filters automatically', the bullet risks
reading as an action item. Removed: the agent's only duties here are
naming the non-numeric exclusions and reporting missing rates; the
product-filters-automatically detail stays in the mapping reference
and template where the mechanism belongs.

Co-Authored-By: Claude Fable 5 <[email protected]>
Third source-dive of the same shape: the agent grepped correctly
(found class Run in nvflare/recipe/run.py) but bundled discovery and
import in one command and guessed 'from nvflare.recipe.spec import
Run' from the sibling-class prior (Recipe lives in spec). The motive
is ours to remove: the references said where output lands but never
what execute() returns. Now stated: execute(env) returns a Run handle
from nvflare.recipe.run (not spec), and validation does not need it -
success is determined by the completeness/parity rungs over the output
JSON.

Co-Authored-By: Claude Fable 5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants