## What
Makes the local **pre-push** git hook enforce the same branch-coverage
gate as CI, so contributors catch coverage regressions before they push
(not just in CI). Two files change: the hook (`.pre-commit-config.yaml`)
and a doc note (`CONTRIBUTING.md`).
## The hook (`.pre-commit-config.yaml`, `run-tests` pre-push hook)
Mirrors `.github/workflows/test.yml`:
1. Runs the full suite with **real torch** + branch coverage: `pytest
--cov=NRS --cov-branch --cov-report=xml`.
2. Then the diff gate: `diff-cover coverage.xml
--compare-branch=origin/main --branch-coverage --fail-under=90` —
**blocks the push if changed code drops below 90% branch coverage**.
3. **Graceful skip** with a warning if `uv` isn't installed (unchanged
behavior); **fails loudly** if `origin/main` can't be resolved (nothing
to diff against).
4. Cleans up `coverage.xml`/`.coverage` on both success and failure
paths, while preserving the exit code so a failing gate still blocks.
All existing hooks are untouched: `prevent-push-to-main`,
`prevent-commit-to-main`, ruff, and the standard hooks. Only the
`run-tests` entry/name changed. Coverage stays on the **pre-push** stage
only — commits remain fast.
### Uses `uvx`, not `uv run` (important)
The invocations use `uvx --with torch --with pytest-cov --with
diff-cover ...`. An earlier `uv run` version was verified to **break the
push**: `uv run` syncs the project env and mutates `uv.lock` mid-hook,
which conflicts with pre-commit's stash/restore and aborts the push
(`Stashed changes conflicted with hook auto-fixes... Rolling back`).
`uvx` runs in an ephemeral env and never touches `uv.lock` — matching
how the project runs tests locally everywhere else.
## Docs (`CONTRIBUTING.md`)
Added a short note right after the `pre-commit install --hook-type
pre-push` instruction explaining the gate, the `uv` requirement (skips
with a warning otherwise), and that `origin/main` must be fetched for
the diff.
## Validation
- Ran the hook's exact command: **45 tests pass**, diff-cover passes;
`git status` after confirms `uv.lock` is **not** mutated (the
stash-conflict path can't recur).
- `uvx pre-commit run --hook-stage pre-push` completes with no
stash-conflict rollback.
- **This very PR's push exercised the hook live** — it ran the gate and
pushed cleanly.
- Only `.pre-commit-config.yaml` and `CONTRIBUTING.md` changed;
`pyproject.toml`/`uv.lock`/tests/`NRS/` untouched.
## Out-of-scope observation
The `ruff-format` hook has no `stages:` restriction, so a manual
`--all-files` dry-run reformats files broadly. Pre-existing; not
addressed here.
## What
Groundwork folded ahead of the held v1.0.0 release (publish-neutral —
**`pyproject.toml` is untouched**, so this does not trigger the registry
auto-publish):
1. **`NRS.__version__`** — added `__version__ = "0.7.4"` (mirrors the
current `pyproject.toml` version) in `NRS/nodes_NRS.py`, re-exported
from a new `NRS/__init__.py` so `NRS.__version__` is importable.
Commented to bump both together at release.
2. **Patch-time log line** — `patch()` now emits, right after
prediction-type detection:
`logging.info(f"NRS v{__version__}: prediction type detected ->
{pred_type.name}")`
So a user's console shows which NRS version ran and which prediction
space it selected (the key diagnostic for the flow-family
reclassification).
3. **GitHub bug-report issue form** —
`.github/ISSUE_TEMPLATE/bug_report.yml` + `config.yml`. Captures the NRS
version + detected prediction type (from the new log line),
model/sampler family, skew/stretch/squash values, expected vs actual,
repro, and console log.
- **Platform-agnostic:** NRS ships for ComfyUI *and* the A1111 family,
so the form has a **Platform / UI** dropdown — `ComfyUI, AUTOMATIC1111,
Forge, reForge, Forge Neo, Stability Matrix, Other` — with a free-text
follow-up and a generic "Platform version" field (no ComfyUI-only
wording).
## Tests
`tests/test_version.py`: asserts `NRS.__version__` is a valid `X.Y.Z`
string, matches `pyproject.toml` (catches future drift), and (via
`caplog`) that `patch()` emits the exact version + pred-type log line.
## Notes
- Adding `NRS/__init__.py` turned `NRS` from an implicit namespace
package into a real package, which newly triggered ruff's `N999` on the
pre-existing mixed-case filename `nodes_NRS.py`. Since `pyproject.toml`
is off-limits here, it's suppressed with an in-file `# ruff: noqa: N999`
pragma (with an explanatory comment) — consistent with the existing
per-file `N802/N804` carve-out for that same file. Renaming the file is
out of scope (breaks imports).
## Validation
- `ruff check .` → clean.
- `pytest --cov=NRS --cov-branch` → **45 passed** (42 + 3 new).
- diff-cover branch gate (mirrors CI) → **100%** on changed code.
- `pyproject.toml` / `uv.lock` untouched; issue-form YAML validated with
`yaml.safe_load`.
## What
Adds the project's first **CI test workflow** —
`.github/workflows/test.yml` (job "CI"). Until now the only workflow was
`publish.yml`; tests never ran in CI. This gates every push to
`main`/`master` and every PR on (a) the full suite passing and (b)
**>90% by-branch coverage on changed code**.
## The workflow
- **Triggers:** `push` to `main`/`master`, and all `pull_request`s.
- **Env:** Python 3.12 (reliable CPU torch wheels),
`actions/checkout@v5` with `fetch-depth: 0` (diff-cover needs
base-branch history), `actions/setup-python@v5` with pip cache.
- **Real torch:** installs the CPU wheel from
`download.pytorch.org/whl/cpu`, so `tests/test_pack_split.py` (which
swaps the mock torch for real torch) actually executes rather than
skipping — its branch coverage counts.
- **Tests + coverage:** `pytest --cov=NRS --cov-branch --cov-report=xml
--cov-report=term-missing`. Any test failure fails the job.
- **Branch-coverage gate on the diff:** `diff-cover coverage.xml
--compare-branch=<base> --branch-coverage --fail-under=90`. Base is
`origin/${{ github.base_ref }}` for PRs (falling back to `origin/main`).
The `--branch-coverage` flag is required — diff-cover otherwise scores
only line coverage even when fed branch data in the Cobertura XML. Gate
is on **changed** code, not total project coverage, so a no-Python PR
passes trivially.
## Deliberately NOT touched
`pyproject.toml` is untouched (test deps live in the workflow), so this
change cannot trigger `publish.yml` or the held v1.0.0 version bump.
## Local validation
- `pytest --cov=NRS --cov-branch ...` → **42 passed**; total NRS branch
coverage ~79% (informational; the gate is diff-only, not total).
- `diff-cover coverage.xml --compare-branch=origin/main
--branch-coverage --fail-under=90` → passes (this branch changes no
Python).
- YAML parses cleanly; `coverage.xml`/`.coverage` already gitignored —
no artifacts committed.
## Note
Making this check **required** is a branch-protection repo setting you
control (GitHub → Settings → Branches).
## What
Adds `PredictionType.FLOW` — an **identity operation space** — and
reclassifies the flow-matching family (`flux`, `chroma`, `flow`, `wan`,
`const`) off the VP ε-prediction path onto it. Flow models (H3, Flux,
Chroma, WAN) were resolving to `PredictionType.EPS`, so NRS ran the ε→v
/ v→ε σ-algebra on them using the wrong sigma convention. NRS's
projection/rejection geometry and norm ratios are invariant to a flow
model's per-sample velocity scale, so flow models are now operated
**natively with no conversion** (identity in, identity out).
## Changes (`NRS/nodes_NRS.py`)
- New enum member `PredictionType.FLOW`.
- `_convert_to_v_space` / `_finalize_from_v_space`: the `V` identity
branch is widened to `V | FLOW` (no ε↔v algebra, `x_div=None`,
cond/uncond/x_final passed through). **EPS** (SDXL/SD1.5), `X0`
(`NotImplementedError`), and the UNKNOWN fallback are untouched.
- `_RAW_TO_ENUM`: `flux`/`chroma`/`flow`/`wan`/`const` → `FLOW`
(comments updated). `eps`/`epsilon`/`v`/`v_prediction`/`x0`/`sample`
unchanged.
- `_get_pred_type` enhanced-detection fallback (the **second** detection
site): the CONST sampling-class branch and the `model.model.model_type`
flow/flux branch now return `FLOW` instead of `EPS` (log strings
updated). The `v_prediction`→V and `eps`→EPS branches are unchanged.
Changing only the dict would have silently left class-name /
model_type-detected models (WAN, H3) on the VP path.
## H3 specifically
MiniMax H3 needs **no special case**. It exposes `BaseModel.model_type`
as the `ModelType.FLOW` **Enum**; `_canon`'s Enum branch reduces it to
`"flow"`, an exact `_RAW_TO_ENUM` key → resolves to `FLOW` at the dict
direct-hit site. There is no `"h3"` raw type. A regression test pins
this exact Enum→canon→FLOW path.
## Tests (`tests/test_pred_type.py`)
- Flipped the flow-family pins (`flow`/`const`/`flux`/`chroma`/`wan`)
from EPS to **FLOW** — the PR-1 safety net doing its job.
- Fallback-site coverage: stubs whose only flow signal is a CONST-like
`model_sampling` class name, or a `model.model.model_type` string merely
*containing* `"flow"`/`"flux"` (not an exact dict key) — so the
enhanced-detection branch is genuinely exercised, not the dict.
- H3-representative test: an actual `enum.Enum` member
(`ModelType.FLOW`-shaped) resolves to FLOW via `_canon`'s Enum branch.
- FLOW identity round-trip through both conversion helpers; V-path
identity regression guard; EPS-path regression guard asserting the ε→v
algebra still runs.
- `uvx ruff check .` → All checks passed!
- `uvx --with torch pytest -q` → **42 passed** (up from 33)
## Rollout
Breaking for **all flow-family outputs** (Flux/Chroma/WAN/H3 produce
different images/video — retune skew/stretch). Baseline preserved at tag
`pre-flow`. This is the last breaking change before the **v1.0.0**
release.
## What
Fixes NRS on MiniMax H3 (and any sampler that packs multiple streams).
ComfyUI hands the cfg hook a **flat packed latent** `[B,1,N]` whose
`dim=1` is a singleton, so every NRS reduction runs over a size-1 axis
and the projection geometry collapses — Skew becomes a silent no-op,
Stretch degenerates to elementwise CFG, Squash to a Stretch-reverter
(proven to float precision in Phase 0).
This PR unpacks the flat pack into its real channels-first per-stream
tensors, runs the **unchanged** NRS geometry per stream, then repacks.
## Changes (`NRS/nodes_NRS.py`)
- Guarded `import comfy.utils as _comfy_utils` (module still imports
with no ComfyUI present) + `import math`.
- Module-level pure-Python `_unpack_latents`/`_pack_latents` fallbacks
matching ComfyUI's pack contract (each stream reshaped to `(B,1,-1)` and
concatenated on the last dim; unpack slices `prod(shape[1:])` per
stream).
- Extracted `_apply_guidance(...)` — the per-stream geometry pipeline
(sigma reshape → `_convert_to_v_space` → dot/proj/stretch/skew/squash →
`_finalize_from_v_space`), **byte-for-byte identical** to the prior
inline math.
- Rewired `nrs()`: read `args["model"].latent_shapes`; when present with
`len>1`, unpack cond/uncond/input per stream (comfy-preferred,
pure-Python fallback), map `_apply_guidance`, repack; otherwise the
single-stream path is the **exact prior behavior** (regression no-op for
SDXL etc.).
- Permanent **degeneracy tripwire**: warns once per `patch()` if any
routed stream still has a singleton reduction axis — the silent-failure
class that cost the original Skew investigation days.
## Why this is safe
- Single-stream models are a verified byte-for-byte no-op (two
regression tests compare against a manually-computed `_apply_guidance`).
- Not H3 fingerprinting: any multi-stream packed sampler (LTXV AV
variants) benefits; older ComfyUI without the API falls through cleanly.
- Verified against the real 18MB H3 capture (not committed):
unpack→repack is bit-exact for cond/uncond/x_orig; streams recover as
`[1,24,72,38,22]` (video) / `[1,32,2,405]` (audio); per-stream guidance
produces large live deviations where the flat pack was near-degenerate —
Skew is alive again.
## Tests
New `tests/test_pack_split.py` (9 tests, real-torch via a module-scoped
isolation harness that restores the mock afterward): round-trip
pack/unpack, single- & multi-stream fallback regression, tripwire
fires/doesn't-fire, per-stream reduced shapes, and
non-degenerate-vs-degenerate rejection.
- `uvx ruff check .` → All checks passed!
- `uvx --with torch pytest -q` → **33 passed** (24 existing + 9 new)
Breaking for H3 outputs only. Baseline preserved at tag `pre-flow`.
## Summary
Behavior-preserving cleanup ahead of the H3 flow-fix work (see
`.claude/plans/nrs_h3_fix_plan.2.md`, fact 8 / Phase 3 / Sequencing
PR-1). No output change — pure refactor plus a regression net.
### Changes to `NRS/nodes_NRS.py`
- **Removed the broken name-mangling guard.** `hasattr(self,
"__pred_type")` checked the literal name while assignment created
`_NRS__pred_type`, so the guard never fired. `patch()` now computes
`pred_type = self._get_pred_type(model)` unconditionally and passes it
as a closure local / explicit parameter into `nrs()`,
`_convert_to_v_space`, and `_finalize_from_v_space`. This preserves the
(correct) always-redetect behavior and removes a latent cross-model
aliasing bug. The `hasattr` string was **not** "repaired" — doing so
would introduce a stale-cache bug since ComfyUI reuses node instances
across queue runs.
- **Amputated dead operation-space arms.** `self.__OPERATION_SPACE` was
hardcoded to `PredictionType.V`, making both `match` blocks and the
`_convert_to_eps_space`/`_finalize_from_eps_space` helpers unreachable.
Removed them; the `nrs()` hook now calls the V-space
conversion/finalization directly. The inner V-vs-EPS conversion math
inside the V-space helpers is untouched — that is the real per-model
algebra, not dead code. FLOW is added in a later PR.
### Tests
- New `tests/test_pred_type.py`: parametrized coverage of all 11
`_RAW_TO_ENUM` entries plus `_get_pred_type` walks over stub models
(direct-attribute path and enhanced-detection fallback). These pin
**current** behavior (flow-family → EPS) as a regression net for the
FLOW reclassification PR.
## Verification
- `24/24` tests pass (21 new + 3 pre-existing smoke) from a clean
checkout.
- Greps for `self.__pred_type`, `_NRS__pred_type`,
`self.__OPERATION_SPACE` all empty.
- Diff: `NRS/nodes_NRS.py` +16/-87, `tests/test_pred_type.py` +111 —
behavior-preserving, well under 500 LoC.
## Summary
- Add version increment check before publishing to Comfy registry
- Compare current version in pyproject.toml against HEAD~1
- Skip publishing successfully if version is unchanged (prevents
unnecessary publish attempts)
- Fail workflow if version is downgraded (prevents registry conflicts)
- Use Python with semantic versioning for robust comparison (handles
0.7.10 > 0.7.9 correctly)
## Changes
- Modified `.github/workflows/publish.yml`:
- Added `fetch-depth: 2` to checkout step to access HEAD~1
- Added new version check step with Python script
- Made publish step conditional on version increment
## Test Plan
- [x] All pre-commit hooks pass
- [x] All tests pass
- [ ] Test unchanged version: Modify pyproject.toml metadata (not
version), workflow should skip publish
- [ ] Test version increment: Bump version, workflow should publish
- [ ] Test version downgrade: Downgrade version, workflow should fail
- [ ] Test invalid version: Malformed version, workflow should fail
The workflow can be tested using manual `workflow_dispatch` trigger
after merge.
## Summary
Establishes a complete development infrastructure for the project with
automated quality checks, testing, and contributor guidelines.
- Add pre-commit hooks for automated linting and quality checks
- Configure ruff for code linting and formatting (120 char line length,
Python 3.10+)
- Set up pytest with mocked torch/gradio dependencies for CI/CD
compatibility
- Create comprehensive CONTRIBUTING.md with setup instructions and
workflow guidance
- Add uv.lock for reproducible dependency resolution
- Replace trivial tests with functional interface tests
## Key Components
### Git Hooks (.pre-commit-config.yaml)
- Pre-commit: ruff linting/formatting, trailing whitespace, YAML
validation, prevents direct commits to main
- Pre-push: runs pytest test suite, prevents direct pushes to main
### Testing (tests/)
- Functional tests for ComfyUI node and WebUI script interfaces
- Mock torch, gradio, and WebUI modules via conftest.py (no heavy
dependencies in dev env)
- Tests verify API contracts without requiring full ComfyUI/WebUI
runtime
### Documentation (CONTRIBUTING.md)
- Development setup with uv package manager
- Git workflow and branch protection guidelines
- Linting, testing, and pre-commit hook usage
- Code style guidelines and commit message conventions
## Test Plan
- [x] Pre-commit hooks run on commit (linting, formatting, checks)
- [x] Pre-push hooks run pytest successfully
- [x] All tests pass in isolated venv
- [x] CONTRIBUTING.md instructions verified
- [x] No IDE diagnostics or issues
- Update actions/checkout from @v4 to @v5 for Node.js 24 support
- Update Comfy-Org/publish-node-action from @v1 to @main per official docs
- Add FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable
## Summary
Fixes issue #20 where NRS would fail with `RuntimeError: "Could not
determine prediction type for this model"` when using certain samplers
like WanKSamplerAdvanced and RES4LYF ClownsharKsampler.
## Changes Made
- **Enhanced prediction type mapping**: Added support for FLOW models
(`"flow"`, `"wan"`, `"const"` → `PredictionType.EPS`)
- **Improved model introspection**: Added `model_sampling` class
inspection and `model.model.model_type` enum detection
- **Graceful fallback**: Replaced RuntimeErrors with safe EPS defaults
when prediction type cannot be determined
- **Better debugging**: Added warning logs when using fallback
prediction types
- **Documentation**: Updated README with sampler compatibility
information
- **Version bump**: 0.7.3 → 0.7.4
## Technical Details
The issue occurred because WAN and similar samplers use **FLOW model
types** (ModelType.FLOW) that implement the CONST prediction class,
which are fundamentally different from traditional EPS/V-prediction
models. The original code only checked for string attributes and failed
to recognize these newer model architectures.
This fix follows the established pattern from commits fc38b5c (flux
support) and ba145c4 (chroma support) while adding robust fallback
mechanisms.
## Testing
- ✅ Manual integration testing planned with WanKSamplerAdvanced
- ✅ Manual integration testing planned with RES4LYF ClownsharKsampler
- ✅ Backwards compatibility maintained for existing samplers
- ✅ Enhanced logging for debugging unrecognized models
## Risk Assessment
**Low Risk**: Only enhances existing detection logic without changing
core mathematical operations. Adds fallback instead of removing
functionality.
Closes#20
---------
Signed-off-by: Reithan <bo122081@hotmail.com>
## Summary
Addresses documentation gaps identified in issue #25 by improving setup
instructions and user experience across all supported platforms.
## Changes Made
### 📚 README Enhancements
- **ComfyUI Setup Section**: Added collapsible setup instructions with:
- Clear workflow explanation (Model → NRS Node → KSampler)
- Prominent note that CFG setting on KSampler is ignored
- Pro tip for validating NRS is working
- Reference to the workflow image from issue #25
- **A1111/Forge/reForge Setup Section**: Added setup instructions
explaining:
- Extension installation and enabling process
- CFG Scale is ignored when NRS is active
- Reference to Beginner How-To for parameter guidance
- **StabilityMatrix Integration**: Added section highlighting native
support with link to https://lykos.ai/
### 🖥️ ComfyUI Node UX Improvements
- **Node Description**: Added clear explanation that NRS replaces CFG
and KSampler CFG will be ignored
- **Parameter Tooltips**: Added helpful guidance directly in the
interface:
- **Skew**: Explains direction steering, suggests starting with CFG/2
- **Stretch**: Explains positive intensification, suggests normal CFG
value
- **Squash**: Explains effect softening, recommends keeping low
initially
## Problem Solved
This addresses the confusion reported in issue #25 where users struggled
with:
- How to configure CFG values in ComfyUI workflows
- Understanding the relationship between NRS and CFG
- Lack of example workflows and clear setup guidance
## Test Plan
- [x] Verify README renders correctly with collapsible sections
- [ ] Test ComfyUI node shows tooltips when hovering over parameters
- [ ] Confirm node description appears in ComfyUI interface
- [x] Validate links work correctly (StabilityMatrix)
Fixes#25
Updates the default parameter values for the Negative Rejection Steering
implementation to improve usability and user experience.
## Changes
- **Skew**: 4.0 → 2.00
- **Stretch**: 2.0 → 5.00
- **Squash**: 0.0 → 0.75
These new defaults (2/5/0.75) provide a better starting point for users,
as documented in the updated README.
## Files Modified
- `NRS/nodes_NRS.py`: Updated default values in INPUT_TYPES
- `scripts/negative_rejection_steering_script.py`: Updated default
initialization values
- `README.md`: Updated beginner how-to guide to reference the new
defaults
---------
Signed-off-by: Reithan <bo122081@hotmail.com>
Hey! My name is Robin and I'm from [comfy-org](https://comfy.org/)! We
would love to have you join the Comfy Registry, a public collection of
custom nodes which lets authors publish nodes by version and automate
testing against existing workflows.
The registry is already integrated with ComfyUI-Manager, and we want it
to be the default place users install nodes from eventually. We do a
security-scan of every node to improve safety. Feel free to read up more
on the registry
[here](https://docs.comfy.org/registry/overview#introduction)
Action Required:
- [ ] Go to the [registry](https://registry.comfy.org). Login and create
a publisher id (everything after the `@` sign on your registry profile).
- [ ] Add the publisher id into the pyproject.toml file.
- [ ] Merge the separate Github Actions PR, then merge this PR.
If you want to publish the node manually, [install the
cli](https://docs.comfy.org/comfy-cli/getting-started#install-cli) by
running `pip install comfy-cli`, then run `comfy node publish`
Otherwise, if you have any questions, please message me on discord at
robinken or join our [server](https://discord.com/invite/comfyorg)!
---------
Co-authored-by: snomiao <snomiao+comfy-pr@gmail.com>
Co-authored-by: Reithan <bo122081@hotmail.com>
This PR adds a Github Action (publish-node-action) that will publish an
updated version of your custom node to the
[registry](https://registry.comfy.org/) whenever the `pyproject.toml`
file changes. The pyproject.toml defines the custom node version you
want to publish (added in another PR). Make sure you update the version
number in `pyproject.toml` when you make a change that should be
published to everyone!
Action Required:
- [ ] Make sure the trigger branch (`master` or `main`) in
`publish.yaml` matches the branch you want to use as the publishing
branch. It will only trigger when the pyproject.toml gets updated on
that branch.
- [ ] Create an api key on the Registry for publishing from Github.
[Instructions](https://docs.comfy.org/registry/publishing#create-an-api-key-for-publishing).
- [ ] Add it to your Github Repository Secrets as
`REGISTRY_ACCESS_TOKEN`.
Please message me on Discord at robinken or join our
[server](https://discord.com/invite/comfyorg) server if you have any
questions!
---------
Co-authored-by: snomiao <snomiao+comfy-pr@gmail.com>
Co-authored-by: Reithan <bo122081@hotmail.com>