Files
remove-ai-watermarks/.claude/rules/development.md
T
Victor KuznetsovandClaude Opus 5 13095fb45c Verify every doc claim against the source and fix what drifted
Every code-referencing claim in the docs, the README and the rules files was
checked against src/, and each finding was re-derived independently before it
was applied. 35 held, 5 were false positives.

Two of them were code, not text. `InvisibleOptions` promises in its docstring to
mirror `InvisibleEngine`, and two defaults had silently stopped:
`max_resolution=None` reached `_target_size`'s `max_resolution > 0` and raised
`TypeError` on every library call that left the options alone, and
`cpu_offload=True` made a library run slower than the identical CLI run. Both are
fixed, and `TestInvisibleOptionsMirrorTheEngine` compares the two signatures
field by field rather than pinning the two values that happen to be known. A
companion assertion in `TestTargetSize` reads the engine's own declared default,
so a drift on the engine side -- which the mirror check alone would accept,
because both sides would still agree -- fails too.

The user-facing docs: README called `invisible` GPU-optional where it raises
without CUDA, and gave the image `metadata` command `video metadata`'s output
rule, promising the source survives a command that overwrites it. Yuanbao was
missing from the supported-mark list. `veo` was listed among the video policies
that require a run anchor, though its row sets no `anchor_iou`.
`known-limitations` called ControlNet the default profile and contradicted
itself ninety lines below. An unescaped pipe truncated the `hailuo` table row.
The `dev` extra, the CI shape, ffmpeg's role, the sdist boundary and the
strength-curve range were corrected, and `remove_all`/`remove_batch`, the pill
gate, `erase --keep-metadata` and `all`'s CUDA failure mode were documented.

Research notes that described removed modules, extras and flags in the present
tense now say so once in the page banner instead of sentence by sentence, which
covers the whole page rather than the lines that happened to be noticed, and one
fixture is referred to by role rather than by name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:38:04 -07:00

4.6 KiB

globs, description
globs description
src/**/*.py
tests/**/*.py
scripts/**/*.py
pyproject.toml
uv.lock
maintain.sh
.github/workflows/*.yml
Command contracts, project gate, typing boundaries, model-adjacent test invariants, and the detection-path measurement rule.

Development invariants

Command contracts

Every single-image command declares source with dir_okay=False; batch declares its directory with file_okay=False. Keep tests/test_cli_robustness.py::TestDirectoryInputIsRejected as the regression guard.

Exit-code and no-signal behavior is a public contract. Read the command-line section of ../../docs/module-internals.md before changing it.

Do not add an option whose only outcome is an error. Model id, step count and CFG are fixed by the profile, so none of them is a parameter of the CLI, InvisibleEngine, or WatermarkRemover -- they were accepted-then-rejected for a while, which moved the failure several frames below the caller and advertised choices the pinned stack cannot honor. If a value cannot vary, delete the knob rather than validating it.

device is the deliberate exception and stays a library parameter: None/"auto" detect, "cuda" pins without detecting, and everything else raises at construction. It is not a CLI option, because the only value a user could usefully type is the one auto-detection already finds.

The same rule applies to install hints: name the extra that actually makes the command work (qwen-zimage, not diffusion), and keep the printed command shell-quoted -- bare pkg[extra] is a glob in zsh.

Local gate

Run bash maintain.sh from the repository root. The authoritative type gate is scoped to src/; full-project Pyright can exhaust Node memory on the ML dependency graph.

Boundary modules for cv2, Torch, and Diffusers may carry narrow per-file relaxations for unknown third-party types. Keep pure-logic files strict, preserve the local piexif stub, and fix real errors before widening a pragma.

Model-adjacent tests

Do not classify an entire module as untestable because its main path downloads a model. Keep pure behavior covered without downloads, including:

  • target-size selection in test_invisible_engine.py;
  • unsharp and adaptive-polish helpers in test_humanizer.py;
  • tiling geometry and blending in test_tiling.py;
  • prompt-embedding cache keying, storage round-trip, and the cross-pipeline reuse that lets a stack load without its text encoder, in test_qwen_zimage_pipeline.py;
  • the face stack's dtype, in test_qwen_zimage_pipeline.py. A subclass that changes the pipeline dtype for its own global model must not change the inherited face stage's; sdxl-zimage shipped doing exactly that and crashed on every image with a face. When one profile inherits another's stage, guard the invariants that stage relies on, not just the code path.
  • the InvisibleOptions defaults, in test_api.py. When one signature promises to mirror another, compare them field by field rather than pinning the values you happen to know about, so the next field added on one side and not the other fails at the seam. Two of these defaults drifted in practice and neither needed a GPU to catch; the incident is recorded in docs/module-internals.md.

Use availability checks only for paths that actually load large models.

One measurement, one gate seam

A detector is split into a trust-level-blind scan and a verdict that applies the threshold, so detect and detect_both reach the same numbers by construction. Two rules follow, and both were broken in practice before they were written down:

  • A per-mark demotion goes in the _post_gate hook (or, for a whole-scan precondition like LibLibAI's size floor, in _scan) -- never in a detect override. An override is invisible to detect_both, so the RunningHub and Yuanbao anchor gates silently stopped applying on the arbiter's perception path. TestSinglePassPerception is the guard: it asserts detect_both equals two detect calls field for field.
  • Detection and the removal mask must read ONE sweep. The winning box travels on TextMarkDetection.match_box and the registry threads the detection into the mask builder; a mask path that re-runs its own sweep is how the two drift apart.

Before changing anything in the detection path, record the detectors' exact verdicts over a local sample first and diff them after. A refactor here is only correct if that record is byte-identical, and a green test suite does not establish that on its own.

Environment setup, dependency recovery, CI behavior, and fixture policy: ../../docs/development.md.