Two owner-reported bugs, both verified as gaps that never closed (the old
code on master had them too).
Cancel: the node never polled ComfyUI's interrupt flag, so the Cancel
button had no effect. A new src/keep_interrupt.py wraps
comfy.model_management.throw_exception_if_processing_interrupted() (lazy
import, a no-op outside ComfyUI); it is called at every long-loop
boundary - detection, crop, restore chunks, paste frames, and the paste
module's group/chunk loops. InterruptProcessingException is a
BaseException, so it escapes the pipeline untouched.
Multi-face: the pipeline collapsed to one face per frame - the detector
call passed only_keep_largest=True (the vendored selector reduces to the
largest face) and the processor then took all_landmarks_5[0]. With
only_center_face=False the new branches keep every detected face:
detection returns all faces, the crops/restore/paste flatten to the
ragged per-frame face count, the device canvas composites several ROIs
per row, and the vendored fallback hands the helper all of a frame's
faces. The near-black tail follows the real vendored reference (one
convert per image after a forward face loop). only_center_face=True and
the aligned path keep their bodies byte-for-byte.
44 new pins: cancel (the exception escapes the sequence and the node, no
work after it, unchanged output without a cancel), multi-face image and
video (all faces restored and pasted, faces appearing/disappearing,
None-affine and black-face drops, per-row near-black tail against the
vendored composite), and the paste core (face-to-frame map, row
compositing, chunk boundaries that never split a frame's faces).
Verified on the CPU device only - no model loads, no GPU runs. Gate: 505
passed, 6 skipped, 7 deselected, 0 failed (461 baseline + 44 new pins).
The audit artifacts and the plan moved to the local .dev/docs tree, so the
references in the code, the tools and the tests follow them there. The
license file names the tool that produces the record instead of a path.
The headless tier runs with the ComfyUI mocks switched off
(KEEP_NO_COMFY_MOCKS=1), but test_the_mock_exposes_no_symbol_the_real_api_lacks
read the mock out of sys.modules - where it is not installed in that
configuration - and died with KeyError: 'comfy_api.latest'.
The mock construction is now a module-level function (build_comfy_api_mocks)
that _install_comfy_mocks uses, so the test can build the mock itself when
the mocks are not installed. The comparison against the real ComfyUI API now
runs in the headless tier as well.
The five review findings of 2026-10-23 for phase P5:
1. The P5.5 fallback branch (a checkpoint without flownet.* keys) died with
a bare RuntimeError at the strict load: the arch always owns the flownet
(keep_arch.py:882), so the state dict of the net always carries the
flownet.* keys - and the GMFlow weights the arch had just loaded were
discarded. The flownet tensors of the built net (the trained ones - the
arch was built with flownet_path) are now merged into the checkpoint
state dict before load_state_dict(..., strict=True). The load stays
strict (an unexpected key still raises) and the trained flow weights
survive. The stub arches of the tests behave like nn.Module now - a
no-op recorder had hidden the defect.
2. P5.4's acceptance is no longer unmet: the GPU acceptance run was
executed (real weights, real ComfyUI modules, RTX 4070 Ti SUPER) and is
green (5 passed, exit code 0; the run log is in the audit doc section 5).
KEEP_MODEL_DIR now really decides where the weights come from - it is the
models root of the run (folder_paths.models_dir), the harness fixture
no longer isolates the gpu_acceptance tier, and the run needs
KEEP_NO_COMFY_MOCKS=1 so the real ComfyUI modules are in charge.
3. The "the flow weights are not random" assertion of the acceptance test
could not distinguish random weights from trained ones. It now compares
the flownet tensors with the tensors of their source (the KEEP checkpoint
or the GMFlow file) bit for bit.
4. The superseded download helper load_file_from_url_comfy is gone
(src/keep_paths.py only resolves paths; the download lives in
src/keep_download.py since P5.2). The stale docstrings, the stale
comment in the vendored facelib and the two tests that called the dead
helper are fixed.
5. The risk list mentioned a pending deletion of assets/keep-preview.png
that does not exist - the asset is committed and present (93279 bytes).
The rule in the implementation workflow now states the truth.
Checks: the gate (pytest -q -m "unit or mock_integration") is green (234
passed, 1 skipped, 1 xfailed); the opt-in acceptance run is green (5 passed);
the acceptance tier still skips cleanly without its environment variables.
Seven review findings of the P1 phase, each fixed at its root:
1. (high) The CI install list omitted PyYAML, but the vendored tree imports
yaml eagerly (modules/deps/basicsr/utils/options.py:5), so the CI job
failed on its first run. pyyaml is now installed and asserted by
tests/test_ci_workflow.py.
2. (medium) tests/test_ci_workflow.py skipped the whole module without
PyYAML. The module never skips now: a minimal indentation parser is the
fallback, and it is cross-checked against the PyYAML parse.
3. (medium) Guard (e)'s self-test asserted that the tree still contains
breakage (it would break exactly at P2.1). It now proves the guard
instead; P2.1's work list is the scanner output and
test_guard_e_flip_owner_covers_every_current_finding keeps plan and tree
in sync.
4. (medium) The mock-fidelity tripwire skipped the comfy_api.latest
symbols. Every mocked symbol is now checked against the real source
(re-export / definition / instance attribute), including io.ComfyNode,
ComfyExtension, ComfyAPI and ComfyAPISync, plus the reverse check that
the mock invents nothing.
5. (low) The phase review listed committed work as outstanding and
misstated the ruff ignore list; both are corrected.
6. (low) The reported whole-suite count was impossible; the phase review
now carries the measured numbers.
7. (low) The root conftest installed the mocks unconditionally. The
KEEP_NO_COMFY_MOCKS=1 opt-out lets the headless tier import the real
comfy/comfy_api modules in-process.
tools/ci_env_sim.py runs the gate in an environment without the packages
the CI install list omits.
- conftest.py: comfy_api.latest (io/ui as siblings, ComfyExtension,
ComfyAPI/ComfyAPISync with an instance-only progress recorder),
comfy.model_management, comfy.model_patcher, comfy.utils and folder_paths
are installed before collection; the package alias ComfyUI_KEEP keeps
__path__ + submodule_search_locations and provides the logger the
modules/* code imports
- fixtures: deterministic sample_image and an autouse per-test
models_dir/folder-registry reset
- tests/test_conftest_mocks.py: dummy V3 node end-to-end through the mock
io, folder_paths isolation, mock-fidelity tripwire against the real
ComfyUI source (skips cleanly without COMFYUI_ROOT)
All 7 P0 review findings were verified by execution and fixed.
F1 (medium) - the compat scanner modelled only the basicsr/archs eager scan.
The vendored basicsr also scans data/, losses/ and models/ eagerly
(basicsr/__init__.py:3-11 imports all of them), so the closure missed 32
files that the repaired package really imports (incl. models/keep_model.py and
models/keep_gan_model.py, both with 'import pdb' at line 4). The scanner now
models all four scans plus 'from <package> import <submodule>': closure
100 -> 132 files, findings 8 -> 10, and 0 runtime-loaded vendored files are
missing from the closure (verified on a temp copy of modules/deps).
The audit doc's opposite claim is corrected and P2.4 in the plan now has to
rewrite basicsr/__init__.py and prove 'import basicsr' still succeeds after
pruning (otherwise ModuleNotFoundError is swallowed into DummyArchRegistry).
F2 (medium) - the Asian manifest entry is now decided (P0.4 section 4.1):
url 404, sha256 null, sha256_prefix '4765ebe0', unverified true, available
false, required FALSE (a required entry would break the default KEEP model
through P5.3's fail-fast rule). Plan P5.0/P5.1/P5.3 and the weights scanner
were aligned.
F3 (low) - the plan's section 8 decision 1, the B3 baseline row, Goal 5, the
risk table and P5.5 carried the superseded GMFlow premise; they now match the
P0.4 decision (the shipped checkpoint carries the flownet weights; the GMFlow
file is defensive and resolved only when the checkpoint has no flownet.* keys).
F4 (low) - the P0.1 fixture asserted rc == 0 before checking the probe's fatal
report, so the documented skip was unreachable. It now handles the probe's
exit code 2 + fatal report and skips with a reason.
F5 (low) - the baseline-import doc claimed all eight stub symbols fire in the
ffmpeg-blocked run; it is 7 of 8 (loader_face_restore_helper_is_dummy does not
fire - the real facelib imports fine).
F6 (low) - the scanner's exit-code contract (0 = no findings, 1 = findings) is
documented in the tool docstring and in the audit doc.
F7 (low) - the root conftest registered '__init__' and the invalid directory
basename; it now registers the plan's alias 'ComfyUI_KEEP' and provides
execute_package_init() so the V3 entrypoint is executed rather than bypassed.
P1.2 is specified to replace that bootstrap.
Gates: pytest tests/ -q -> 37 passed, 1 xfailed; scan_compat.py --quiet ->
closure 132, 10 findings, exit 1; baseline_probe.py -> import ok / model build
fails (unchanged baseline); scan_weights.py -> 0 of 8 present.