mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-14 13:30:55 +02:00
Guard the docs seam, and close the eight knobs it had already lost
The code-to-code seams were guarded and the code-to-docs seam was not, so it drifted the way an unguarded seam does. docs/cli.md named none of --adaptive-polish, --controlnet-scale, --detect/--no-detect, --humanize, --strength, --tile-size or --tile-overlap; --no-detect is the escape hatch the CLI's own --sensitivity help recommends. docs/python-api.md named 9 of the 16 InvisibleOptions fields. The new test reads the live Click tree and the live dataclass rather than a hand-kept list. Group spellings by the Click parameter, not by string surgery: a boolean pair can be spelled --strip-metadata/--keep-metadata or --keep-standard/--remove-all, and deriving the partner from a --no- prefix splits those and demands both halves. Mutation-tested both ways -- removing a flag from cli.md and adding a field to InvisibleOptions each fail it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
326840bab4
commit
adbc6a3046
4 files changed
+133
-1
No files matched your search
@@ -92,6 +92,23 @@ status set; `TestIdentifyRealSamples::test_no_committed_fixture_reports_a_truste
|
||||
is that guard. Read a capability the stack does not have as a missing input, never as a
|
||||
negative finding.
|
||||
|
||||
## The docs are a seam, and they drift like one
|
||||
|
||||
`tests/test_docs_cover_the_public_surface.py` reads the live Click tree and the
|
||||
live `InvisibleOptions` dataclass and requires every knob to be NAMED in the
|
||||
user-facing pages. The code-to-code seams were already guarded and the
|
||||
code-to-docs seam was not, so it drifted exactly the way an unguarded seam does:
|
||||
`docs/cli.md` named none of `--adaptive-polish`, `--controlnet-scale`,
|
||||
`--detect/--no-detect`, `--humanize`, `--strength`, `--tile-size`,
|
||||
`--tile-overlap` or `--unsharp`, and `docs/python-api.md` named 9 of the 16
|
||||
option fields.
|
||||
|
||||
Group option spellings by the Click PARAMETER, never by string surgery on the
|
||||
names. A boolean pair can be spelled anything -- `--strip-metadata/--keep-metadata`,
|
||||
`--keep-standard/--remove-all` -- so deriving the partner from a `--no-` prefix
|
||||
silently splits those pairs and demands both halves. Either spelling documents
|
||||
the pair.
|
||||
|
||||
## One measurement, one gate seam
|
||||
|
||||
A detector is split into a trust-level-blind scan and a verdict that applies the
|
||||
|
||||
@@ -31,6 +31,8 @@ Optional features and installation groups are documented in [`docs/installation.
|
||||
|
||||
`maintain.sh` runs dependency freshness and security checks, Ruff, Pyright scoped to `src/`, and the parallel test suite. Full-project Pyright is not the project gate because the ML dependency graph can exhaust Node memory.
|
||||
|
||||
This gate does NOT pass `--ignore-unfixed` to uv-secure, so a transitive CVE with no released fix stops it before Ruff, Pyright and the tests ever run. That is deliberate and it is not a blocker: run those three separately and report them, per the triage in [`docs/development.md`](docs/development.md). A red gate here is not the same signal as a red gate in a sibling repository that does ignore unfixed findings.
|
||||
|
||||
Command, gate, typing, and model-test invariants auto-load from [`.claude/rules/development.md`](.claude/rules/development.md). Environment recovery, CI behavior, and fixture policy live in [`docs/development.md`](docs/development.md).
|
||||
|
||||
Before a release, read [`docs/release-and-distribution.md`](docs/release-and-distribution.md). Treat the release as complete only after PyPI, Homebrew, the Hugging Face Space, and the ComfyUI Registry are verified; conda is not published. Keep the source-distribution public allowlist.
|
||||
@@ -51,4 +53,4 @@ Topic-specific rules live in `.claude/rules/*.md` and are auto-loaded when match
|
||||
|
||||
| File | Covers |
|
||||
|---|---|
|
||||
| `development.md` | Command contracts, project gate, typing boundaries, model-adjacent tests, and the detection-path measurement rule |
|
||||
| `development.md` | Command contracts, project gate, typing boundaries, model-adjacent tests, the docs-coverage seam, and the detection-path measurement rule |
|
||||
@@ -146,6 +146,31 @@ engine's own parameter names and defaults. `force`, which decides whether the
|
||||
engine runs at all, is a parameter of `remove_all` and `remove_batch` alongside
|
||||
`backend` and `sensitivity`.
|
||||
|
||||
The complete field set, with the shipped defaults:
|
||||
|
||||
| Field | Default | Effect |
|
||||
| --- | --- | --- |
|
||||
| `strength` | `None` | Denoising strength. `None` resolves per profile and vendor cohort. |
|
||||
| `pipeline` | `"qwen-zimage"` | Profile: `qwen-zimage`, `sdxl-zimage`, `chroma-zimage`, or `auto`. |
|
||||
| `vendor` | `None` | Strength cohort, and an assertion that the watermark is present. |
|
||||
| `seed` | `None` | `None` uses the certified seed 0. Removal near the floor is seed-dependent. |
|
||||
| `hf_token` | `None` | Hugging Face token for the model download; the environment and a local `.env` are read when unset. |
|
||||
| `humanize` | `0.0` | Analog film grain. 0 is off; 2.0-6.0 is the useful band. |
|
||||
| `unsharp` | `0.0` | Unsharp-mask sharpening. 0 is off. |
|
||||
| `adaptive_polish` | `None` | `None` lets the profile decide (off for `qwen-zimage`, on for `sdxl-zimage`); `True`/`False` override it. |
|
||||
| `max_resolution` | `0` | Cap the long side before diffusion. 0 keeps native geometry and the most detail. |
|
||||
| `controlnet_conditioning_scale` | `1.0` | Canny ControlNet conditioning on the global stage. Higher stays closer to the original structure and text. |
|
||||
| `cpu_offload` | `False` | Stream model components between CPU and GPU. Lower CUDA memory, slower. |
|
||||
| `tile` | `False` | Regenerate large inputs in overlapping tiles instead of downscaling. |
|
||||
| `tile_size` | `1024` | Tile side in pixels when `tile` is set. |
|
||||
| `tile_overlap` | `128` | Tile overlap in pixels. More overlap hides seams and costs time. |
|
||||
| `text_manifest` | `None` | Path to an operator-verified text manifest; see the text-restoration section. |
|
||||
| `fidelity_anchor` | `False` | Opt into the fidelity-anchor pass. |
|
||||
|
||||
`tests/test_docs_cover_the_public_surface.py` fails when a field is added here
|
||||
and not to this table, because this seam had already drifted by eight fields
|
||||
before anything checked it.
|
||||
|
||||
If AI metadata survives the strip, `remove_all` raises `MetadataStripIncomplete`
|
||||
**before** writing anything: an AI-readable output is worse than no output.
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"""The user-facing docs must name every knob the public surface actually has.
|
||||
|
||||
The code-to-code seams are already guarded: `test_api.py` compares
|
||||
`InvisibleOptions` against the engine signature field by field, and
|
||||
`test_cli.py` drives the commands. Nothing guarded the code-to-DOCS seam, and it
|
||||
drifted exactly the way an unguarded seam does -- silently, one addition at a
|
||||
time. When this was first measured, `docs/cli.md` named none of
|
||||
`--adaptive-polish`, `--controlnet-scale`, `--detect/--no-detect`, `--humanize`,
|
||||
`--strength`, `--tile-size`, `--tile-overlap` or `--unsharp`, and
|
||||
`docs/python-api.md` named 9 of the 16 `InvisibleOptions` fields.
|
||||
|
||||
These tests read the real Click tree and the real dataclass rather than a
|
||||
hand-kept list, so a knob added tomorrow fails here until someone documents it.
|
||||
Both accept EITHER spelling of a boolean pair: `--keep-metadata` documents
|
||||
`--strip-metadata/--keep-metadata` perfectly well, and requiring the on-form
|
||||
would force noise into the guides.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from remove_ai_watermarks.api import InvisibleOptions
|
||||
from remove_ai_watermarks.cli import main
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
# The pages a user is sent to for command and API reference. A knob mentioned
|
||||
# only in a research-archive page is not documented for the purpose of this gate.
|
||||
CLI_PAGES = ("docs/cli.md", "README.md", "docs/installation.md")
|
||||
API_PAGES = ("docs/python-api.md", "README.md")
|
||||
|
||||
|
||||
def _text(pages: tuple[str, ...]) -> str:
|
||||
return "".join((REPO_ROOT / page).read_text(encoding="utf-8") for page in pages)
|
||||
|
||||
|
||||
def _cli_options() -> list[tuple[str, list[str]]]:
|
||||
"""Every ``(command, spellings)`` pair in the tree.
|
||||
|
||||
Group by the Click parameter itself, never by string surgery on the names: a
|
||||
boolean pair can be spelled anything (``--strip-metadata/--keep-metadata``,
|
||||
``--keep-standard/--remove-all``), so deriving the partner from a ``--no-``
|
||||
prefix silently splits those pairs into two groups and demands both halves.
|
||||
"""
|
||||
found: list[tuple[str, list[str]]] = []
|
||||
|
||||
def walk(command: click.Command, prefix: str = "") -> None:
|
||||
if isinstance(command, click.Group):
|
||||
for name, sub in sorted(command.commands.items()):
|
||||
walk(sub, f"{prefix}{name} ")
|
||||
return
|
||||
for param in command.params:
|
||||
if not isinstance(param, click.Option):
|
||||
continue
|
||||
names = [o for o in param.opts + param.secondary_opts if o.startswith("--")]
|
||||
if names and names != ["--help"]:
|
||||
found.append((prefix.strip() or "(root)", names))
|
||||
|
||||
walk(main)
|
||||
return found
|
||||
|
||||
|
||||
def test_every_cli_option_is_named_in_the_command_docs() -> None:
|
||||
docs = _text(CLI_PAGES)
|
||||
undocumented = [
|
||||
f"{command}: {' / '.join(names)}"
|
||||
for command, names in _cli_options()
|
||||
if not any(name in docs for name in names)
|
||||
]
|
||||
assert not undocumented, (
|
||||
"CLI options absent from " + ", ".join(CLI_PAGES) + ":\n " + "\n ".join(sorted(set(undocumented)))
|
||||
)
|
||||
|
||||
|
||||
def test_every_invisible_option_field_is_named_in_the_api_docs() -> None:
|
||||
docs = _text(API_PAGES)
|
||||
fields = [f.name for f in dataclasses.fields(InvisibleOptions)]
|
||||
assert fields, "InvisibleOptions has no fields; the guard would pass vacuously"
|
||||
undocumented = [name for name in fields if name not in docs]
|
||||
assert not undocumented, (
|
||||
"InvisibleOptions fields absent from "
|
||||
+ ", ".join(API_PAGES)
|
||||
+ f": {undocumented}. Document the field or explain in python-api.md why it is internal."
|
||||
)
|
||||
Reference in new issue
Block a user