Repository navigation
Cleanup/lean package - #17
Merged
Merged
Conversation
These were an uncommitted work-in-progress that broke 5 interpretability tests: build_multi_attribute_scatters coerced every extra column with to_numpy(dtype=float), which raises on `condition` (routinely a string label). Unlike the composite-heatmap call site it was not wrapped, so the whole report build failed. - Add _resolve_color_values: numeric columns pass through, non-numeric ones are factorized with their labels kept as colorbar ticks, so categorical attributes stay plottable instead of raising. - Expose per_cluster_cell_type_enrichment params in the plugins config; the plugin was registered but unreachable by configuration. It stays out of `enabled` to preserve the conservative default. - Cover both with tests, including a string-valued condition column and the noise-cluster skip behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tree was importable only as `src.*` from the repository root, and pyproject.toml had no [project] table or build backend, so `grass-mil` could not be installed or depended on. - Move all modules to src/grass_mil/ and rewrite `from src.*` imports plus the `_target_:` strings in the config tree. - Move configs/ into the package. Hydra resolves config_path relative to the decorated function's file, so the tree now resolves identically from a checkout and from an installed wheel, with no importlib.resources shim. - Add [build-system] (hatchling) and [project], with squidpy/scanpy, plotly and playwright demoted from hard requirements to extras. - Namespace the console scripts (grass-mil-train, ...); the previous `train_command`/`eval_command` names would collide on any user's PATH. - Replace the bare rootutils.setup_root calls with _paths.setup_project_root, which tolerates a missing .project-root marker, and let paths.root_dir fall back to the Hydra runtime cwd. An installed wheel has no marker above it. - Point tests at the packaged config tree instead of CWD-relative paths. - Add tests/test_packaging.py and a CI job that pip-installs outside conda and runs the CLI from an unrelated directory, so packaging cannot rot silently. No behaviour change: the same 243 tests pass before and after, plus 9 new packaging tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two bugs that together removed cell type from every sampled subgraph. Both had to be fixed before the encoder can consume cell-type codes. 1. _apply_transform_to_each_subgraph discarded every node-length tensor as "already represented by x". That is false for categorical_index (the cell-type codes) and pos (coordinates), neither of which is recoverable from x. They are now sliced to the subgraph like any other node attribute. 2. Fixing (1) exposed the reason it had gone unnoticed: PyTorch Geometric's Data.__cat_dim__ returns -1 for any key whose name contains "index", so `categorical_index` was concatenated along the feature dimension when graphs are batched. With equal-sized graphs that silently corrupts the codes; with unequal ones it raises. Renamed to `categorical_codes`, which PyG batches along the node dimension like `pos`. Graphs written by an older precompute are migrated on load, so existing processed caches keep working without a forced re-precompute. Regression tests cover subgraph survival, correct batching, and the migration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The encoder could not express the published input scheme. It had no categorical embedding, so cell identity -- the only node input for some cohorts -- had no path into the model at all, and `gine` fed the whole edge_attr vector into the convolution rather than the edge length alone. - NodeInputEmbedding implements h0 = E[t] + Wx, summing a learned cell-type embedding with a projection of the continuous features. It handles input_dim=0, where precompute legitimately emits a zero-column feature matrix and the embedding is the entire input representation. - EncoderConfig.edge_feature_index narrows edge_attr to one scalar column for GINE message passing, matching the current NSCLC/LUAD models. No edge-type embedding: methods.tex still describes the older edge-type convention and is stale on this point. - Out-of-range categorical codes raise instead of being clamped; clamping would silently train on the wrong cell type. - Vocabulary size and column index are inferred from the processed dataset's label map and categorical slices, with explicit config winning -- the same contract as encoder.input_dim. - New gine_length encoder preset (LayerNorm, max pooling) is the production arm; existing configs are unaffected because categorical_embedding defaults to null and reproduces the previous input projection exactly. Covered by unit tests (sum-not-concat, zero-width x, reserved unassigned row, out-of-range rejection, and a bit-identical assertion that non-selected edge columns cannot influence the output) and an end-to-end fast_dev_run on a cell-type-only cohort. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bag pooling squeezed the attention head to a single channel and broadcast it
across every class logit, so the published per-class form could not be
expressed. The attribution suite rests on the identity
L_c = sum_i A[i,c] * l[i,c]
which needs one attention channel and one logit per class; a shared channel
cannot express it.
- Softmax down the instance axis per class column. Attention width must be 1
(shared, previous behaviour) or one per class; any other width now raises via
a shared contract instead of silently broadcasting into the wrong shape.
- Fix the shape ripples together, since they failed in opposite ways:
inference/aggregation.py flattened attention silently, while
interpretability_export.py hard-rejected anything wider than one column.
- Export attention_c{j} and the raw attention_logit_c{j} alongside the
canonical `attention` column. The raw logits are comparable across bags,
which the within-bag normalised scores are not.
- validate_attention_width runs at build time too, so a mismatched config
fails before the first batch rather than part-way through it.
- The width contract lives in a new leaf module (grass_mil.contracts) because
models and inference already import each other through the supervised module.
BREAKING: task=finetune_mil is now categorical/CE with graph_head.output_dim=2
and attention.n_classes=2. A single-logit BCE head cannot express the
attribution identity. finetune_mean is unchanged.
Also changed: AggregatedPredictionPayload.bag_attention rows are now
[n_instances, n_channels] rather than [n_instances].
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two sampling controls the clean repo lacked. Both default to off, so existing configs are unaffected. Interior seeds (ported from nsclc_tuning_study/boundary_sampling.py): ego-graphs rooted at the edge of a section are truncated by the section boundary rather than by biology, so they are not comparable to interior ones. The mask excludes only the n_hops graph-hop rim around the convex hull; internal holes and low-cellularity gaps are deliberately not boundary. The mask definition is kept identical to the legacy one so models stay comparable, but the traversal is expressed as sparse frontier expansion instead of a Python BFS. Interior restriction is applied to the candidate pool *before* class weighting, so class frequencies are computed over eligible nodes only. Not ported: the Voronoi-bbox boundary mode (it exists to bridge networkx graphs to relabelled tensors, which is unnecessary here since `pos` is available), the distant-edge fallback, merged_allowed_seed_indices (it serves a merge-all-regions pretraining path this repo does not have), and the NSCLC_BOUNDARY_MODE environment variable, which becomes plain config. Ego radius: r_k = radius_per_hop * depth + radius_offset, applied as a post-hoc subgraph transform through the existing hook. Distances are in datamodule coordinate units, not pixels -- the published 75/55 are pixel values at the NSCLC imaging scale, so the defaults are documented as needing conversion per cohort rather than being silently correct. Kept from the legacy implementation: the empty-interior fallback to all nodes, the largest-interior-component filter, and the atomic np.save/os.replace cache write (a half-written cache file must not be visible to another worker). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The SSL views were uniform elementwise masking, not the production augmentation. - drop_importance: nodes and edges are dropped with probability min((1 - importance) * mu, p_lambda), where importance is the mean-max normalised log-degree, so hubs are preserved and sparse periphery is masked. Dropped nodes have features zeroed and their cell type set to the reserved "unassigned" code rather than being left as a spurious real type. - feature_noise resolves its columns BY NAME against the datamodule's feature names. The legacy default noised a hardcoded index, which pointed at cell size in one cohort and at an unrelated column in another, feeding the encoder pure noise. Two bug classes carried across deliberately: - Edge dropping is symmetric. The legacy finetune transform left force_undirected=False while pretraining used True, so the two regimes augmented differently and message passing became direction-dependent. Both the new augmentations and the existing augment_graph now force it, with a test asserting is_undirected() over several seeds. - Every write clones first. The legacy code wrote through a view, so its unassigned-token fixup silently reverted; a test now asserts the source graph is untouched. Naming corrected without changing behaviour: the legacy `keep_prob` was used as `rand > p`, so it is the drop probability. Deviations recorded in the docstrings: methods.tex describes a min-max degree normalisation, but the final run code uses mean-max and the code is authoritative. Not ported: RewireEdgesAll, AddEdgesByCellType, ShufflePositions, Apoptosis, Mitosis, PhenotypeShift, DropFeaturesGRACE, and the O(E) Python remove_directed_edges (replaced by a vectorised mutual-edge mask). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Survival was fully supported by the module and losses but had no task preset, so it could only be reached by hand-overriding four keys, and a mis-shaped config failed deep inside the Cox loss with an opaque shape error. - New task=finetune_survival, documenting that target_columns order is load-bearing ([time, event]) and that attention stays single-channel because a scalar log-hazard has no per-class decomposition. - validate_task_config now rejects survival configs with the wrong number of target columns or a non-Cox loss. - _survival_concordance_index was an O(n^2) Python double loop with two .item() calls per pair, i.e. a device synchronisation per pair. Replaced with a chunked tensor implementation that bounds peak memory at chunk_size * n booleans. No new dependency: lifelines/sksurv would contradict the lean packaging. A 4000-subject cohort now scores in ~0.05s. - Tests compare against a reference double loop over several seeds, check that chunk size cannot change the result, and cover the no-events, all-ties, perfect and reversed orderings. The synthetic datamodule gained a survival mode. Its follow-up times are spread and its events mixed deliberately: with every event at the largest time the partial likelihood is constant and carries no gradient, which is exactly how the first version of this test failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The interpretability suite computed an attention lift but no margins, pooled over all bags with no region-equal weighting and no uncertainty. The method's attribution claim rests on the identity L_c = sum_i A[i,c] * l[i,c], which was never actually evaluated. - instance_margin_contributions gives M[i,c] = A[i,c] * (l[i,c] - beta_c), with the head bias removed so the margin reflects the instance-driven part of the decision rather than the head's prior. - cluster_attribution_summary reports signed margin share, |M|-weighted margin, margin sign consistency, abundance-corrected attention lift and prevalence, each with a 200-resample percentile bootstrap interval over regions. Regions are resampled, not instances: region sizes vary by orders of magnitude and pooling over instances lets one large region dominate. - identity_residual = max_r |sum_i M[i,c] - L_c| is reported so the additive assumption is checked rather than trusted, and is covered by a test that deliberately breaks it. One correction to the manuscript's description: the signed margin share is NOT bounded to [-1, 1]. It is a fraction of the bag margin, so a cluster can exceed 1 when another cluster pushes the opposite way. The genuine invariant is that a full partition sums to 1, which is what the test asserts. Regions whose margin is within margin_eps of zero are excluded rather than clipped, since the ratio is meaningless there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three spatial/geometry analyses the report generator relies on had no equivalent here. - Moran's I over a binary connectivity matrix with a permutation null, BH-FDR adjustment, and the differential-vs-reference z statistic (dI / sqrt(var_k + var_ref)) used to contrast clusters against the noise cluster. - Ripley's centred cross-L via cKDTree pair counts, keeping the self-pair correction (a point is not its own neighbour: the count loses n_a and the denominator is n_a(n_a - 1)) and the region-equal aggregation. - Chance-corrected agreement (ARI/AMI/NMI) plus cluster Jaccard overlap, so "structure the encoder resolves beyond the composition baseline" is a measurement rather than an assertion. Implemented natively over scipy.sparse rather than through squidpy: the package stays installable without scanpy/numba, and tier2.neighborhood already sets that precedent. Two things stated rather than papered over: - SCOPE: the legacy computes Moran's I over cells inside pooled ego-graphs. Here the spatial table is root-instance level, so this measures autocorrelation over the instance graph. Defensible and arguably cleaner, but not numerically the same statistic. - A latent legacy bug is fixed in the port: the Ripley radius grid was derived from whichever region happened to be iterated first, so results depended on ordering. The grid is now explicit config, defaulting to the median extent, with a test asserting order-independence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Attention heads expose head_spaces(), giving a_pre_tanh (the argument of the tanh gate) as a first-class output. The legacy report reached into attention_net.attention_a[0] directly. Pre-tanh matters because saturation collapses exactly the extremes that distinguish strongly-attended instances. With the space exported as table columns, multi-space analysis needs no new pipeline code: re-run the report with a different embedding_prefixes. - New plugins: margin_attribution (the exact additive identity with bootstrap intervals), morans_i (+ differential vs the noise cluster), ripley, and cluster_agreement. - All four are configurable and stay out of the default `enabled` list; a new plugins=full preset turns everything on. margin_attribution is added alongside the existing attention_attribution rather than replacing it. Consolidating them would change the payload keys that build_composite_cluster_heatmap consumes, so that cut is deferred rather than bundled into an already-large change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The goal is region cross-entropy as the sole objective, but the module still carried the machinery for two auxiliary terms that the reported models never used, plus config surface implying they were part of the method. Removed: - task.node_aux (instance-pull) and task.entropy_reg, with build_mil_aux_targets, compute_aux_node_loss, compute_entropy_regularization and gather_instance_logits. - task.loss_weights, which is vacuous once those are gone: _compute_losses now returns the region loss directly instead of composing a weighted sum. - NodePredictionHead, build_node_head and configs/model/heads/node.yaml. It was only ever consumed by node_aux, and was never wired into SupervisedModule at all -- the "node" auxiliary actually used instance-level subgraph logits from the graph head. - GraphPooling's `attention` and `set2set` modes. Instance selection is the MIL head's job, pooling across ego-graphs; duplicating it inside each ego-graph adds parameters without a matching claim. GlobalAttention is also deprecated upstream. supervised_module.py drops from 1318 to ~1100 lines. The advanced-MIL test arm now exercises region accumulation alone, which is retained: it is a throughput mechanism rather than an objective, and removing it would change optimizer-step semantics for streamed bags. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- New docs/method_fidelity.md maps each part of the method to its implementation and to the test that holds it in place, so a reader can check what is actually enforced before trusting a number. It also records the four places where methods.tex and the run code disagree (edge length vs edge type, mean-max vs min-max degree normalisation, the signed-share bound, and the Moran's I scope), and lists the analyses methods.tex claims that no code implements. - Rewrote docs/legacy_gap_audit.md. It previously asserted that a list of legacy risks had been resolved by rebuilding from concepts, which was misleading: it audited engineering hygiene while several load-bearing pieces of the published method were missing entirely. It is now a status record, including the silently-wrong-answer bugs found while closing those gaps. - Contracts doc gains the encoder, attention and attribution contracts, including the identity sum_i A[i,c]*l[i,c] == L_c as an invariant that bag pooling changes must preserve. - README states the scope decision (the method, not a reproduction harness) instead of listing benchmark packs as a "known gap", and documents the survival preset. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The margin was computed as contribution_c1 - contribution_c0 for ANY class count, so with three or more classes every class beyond the first two was silently ignored. - instance_ovr_margins gives m[i,c] = M[i,c] - mean over the other classes. With C=2 this is exactly M[i,1] - M[i,0], so binary results are unchanged; with C=1 (regression, or Cox on the log-hazard scale) the contribution is already the margin. - Cluster summaries are now indexed by (cluster_label, class_index). Binary heads summarise the positive class only, since the class-0 margin is its exact negation; multi-class heads summarise every class. Selectable via focus_classes. - Each class's summary uses that class's own attention channel, so the attention lift reported for class c reflects A[:, c] rather than a pooled weight. Covered by a test where one class attends a single instance and another is uniform. The contributions were already the bias-free attention-weighted logits M[i,c] = A[i,c] * (l[i,c] - beta_c); that is unchanged, and the additive identity is now asserted for a 4-class head as well. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Interpretability output is read by biologists, but it spoke the algorithm's language. - "Cluster" becomes "niche" throughout the interpretability layer: the `niche_label` column, `NicheSummary`, `niche_attribution_summary`, `niche_profiles`/`niche_agreement`/`per_niche_cell_type_enrichment` plugins, figure titles and axis labels. The *algorithm* is still clustering, so `run_clustering`, `ClusteringResult` and the sklearn parameters (`n_clusters`, `min_cluster_size`) keep their names -- those are the method, niches are the result. - Label -1 is named **Background** rather than "noise", including the `skip_background` parameter and the "excluding Background" figure variants. Density-based clustering puts transitional and sparsely populated tissue there; it is a meaningful category, not a failure. - Niches are ordered by a dendrogram over their composition profiles, so compositionally similar niches are adjacent in every table and heatmap instead of appearing in arbitrary label order. Correlation distance is the default, so equal makeup at different densities reads as similar. Background is pinned last rather than joining the tree, since a catch-all would distort it. Ordering flows from the composition summary into enrichment, counts and the report figures, and is disableable. Figures name niches "Niche 3", keeping the numeric id so a reader can join a figure back to the table without a lookup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Identifiers that embedded "cluster" inside a longer name were missed by the word-boundary pass: the cluster-transfer helpers, AttributionResult.per_cluster, and the instance_with_clusters.csv artifact. All now use niche vocabulary. The clustering algorithm's own names are deliberately left alone (run_clustering, ClusteringResult, n_clusters, min_cluster_size, and the internal PCA feeding the clustering step). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Attention was allowed to emit a single channel broadcast across every class logit. That cannot express a niche which pushes toward one class and away from the other, and it makes the per-class attribution identity vacuous. A binary head now needs two attention channels, a scalar head (regression, Cox log-hazard) one; anything else raises with the required width named. Also merges grass_mil/interpretability/contracts.py into grass_mil/contracts.py. Two modules with the same name, one holding a single validator and the other the interpretability payload dataclasses, was a needless ambiguity; the single module stays dependency-light so any layer can import it without a cycle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
drop_edges accepted a torch.Generator and never used it: it delegated to torch_geometric's dropout_edge, which takes no generator and draws from the global RNG. The module advertised purity while BGRL additionally passed a literal None, so pretraining views were not reproducible under a fixed seed and the existing determinism test passed only because it exercised a different function. - drop_edges now takes the draw itself, so the generator actually governs it. - BGRL seeds one generator from task.augmentation_seed and advances it across views, so the two views of a batch differ while the run stays reproducible and independent of unrelated global RNG consumption. - Deleted ssl_runtime.augment_graph. It duplicated drop_edges/drop_features with different semantics -- elementwise feature masking rather than dropping whole columns -- so the same-looking config behaved differently depending on which path ran. The column-wise version is kept, matching the legacy DropFeatures it was ported from. The uniform preset now goes through build_augmentation like every other mode. Tests assert that the generator drives the draw, that the global RNG cannot influence it, and that BGRL's two views repeat across runs while differing from each other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The config told users to read logit_bias from model_summary.json, but nothing ever wrote that file, so bias-free margins were unreachable in practice: the value could only be transcribed by hand. read_graph_head_bias pulls the output bias straight from a training checkpoint -- the highest-numbered net.<i>.bias under the head prefix, which is the final Linear of the head MLP -- and margin_attribution takes logit_bias_checkpoint. An explicit logit_bias still overrides it. An encoder-only checkpoint raises rather than silently attributing with the bias left in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Niche ordering was re-derived inside the composition summary and reached only that one table, so the attribution and spatial-statistics outputs in the same report still came out in arbitrary label order. Niches are now renumbered once, immediately after clustering: Niche 1 is first in dendrogram order, Niche 2 second, and so on. The id carries the ordering, so every downstream table, figure and export gets composition order by simply sorting on it, and no ordering state has to be threaded through the pipeline. Background keeps its own label and takes no position in the tree. The mapping is recorded in pipeline_summary.json. Consequently niche_composition_summary loses its order_by_composition flag, and sort_by_niche_order is deleted -- it existed only to re-apply an ordering that the ids now carry, and its axis/level parameters were never exercised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Attention weights on their own say what the model looked at, not which way that pushed the decision, so reporting them alongside margins invited reading attention as evidence. Interpretation is now expressed solely through the weighted logit margins M[i,c] = A[i,c] * (l[i,c] - beta_c). Removed: the attention_attribution plugin, niche_attention_summary and niche_survival_attention_summary, the attention-derived NicheSummary fields (weighted_scores, mean_scores, attention_present, attention_lift_present), and the attention_lift column from margin attribution. The composite report figure drops its predictions and attention-lift panels, keeping composition enrichment beside niche abundance. Attention itself is unchanged -- it still drives bag pooling and still appears in the instance table. What is gone is the reporting of attention as an interpretation in its own right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- resolve_encoder_cfg now calls resolve_categorical_embedding_config instead of reimplementing the same inference inline. The shared helper had been reachable only from tests, so the tested implementation was the dead one. - Renamed the region-accumulation locals: total_losses, step_total_loss, mean_total_loss and _region_total_loss_buffer all held the region loss alone once the auxiliary terms were removed, so the names implied a composition that no longer exists. The rename immediately exposed a test that had been setting a stale attribute name. - Dropped the `slow` marker. The six marked tests run in ~13s, so `-k "not slow"` bought nothing and left `make test` and `make test-full` as two commands that ran different subsets for no reason. - EncoderConfig.categorical_embedding is typed Dict[str, Any] like its neighbours instead of a bare dict. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The docs still described removed features, and the hyperparameter reference handed users copy-pasteable commands that now fail: task.node_aux.*, task.entropy_reg.*, task.loss_weights.*, set2set/attention pooling, configs/model/heads/node.yaml, and the attention_attribution plugin. requirements.txt is deleted. It duplicated pyproject.toml's dependency list and was what two docs told users to install, which bypassed the extras and would drift silently. Install instructions now point at the package with its extras. Also records the survival preset, the per-class attention requirement, and the two things deliberately retained: region accumulation (the final NSCLC runs use hyperbatch_size 16) and the clustering algorithm's own parameter names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The macOS job failed building torch-sparse from source. The compile error is in PyTorch's own header -- c10/util/strong_type.h specializes std::is_arithmetic, which Xcode 26's libc++ marks _LIBCPP_NO_SPECIALIZATIONS and rejects outright -- so on that runner *any* pip-built torch C++ extension fails, not just this one. torch-sparse was the only PyG extension routed through pip; pytorch-scatter was already coming from the pyg conda channel and installed cleanly as a prebuilt osx-arm64 binary. The pyg channel publishes pytorch-sparse 0.6.18 py312_torch_2.4.0_cpu for osx-arm64 too, so it now comes from there as well. Nothing is dropped: macOS keeps full coverage, including the shadow_custom sampler tests that need torch-sparse. Also clears the warnings the run surfaced: actions/checkout and setup-python bumped for the Node 20 runner deprecation, codecov-action to v5, and setup-miniconda's renamed auto-activate input. setup-miniconda@v3 still targets Node 20 upstream; nothing to do on our side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.