Three gaps found while measuring the record path against the file path, each one a signal the library could not see: WebP stores `XMP ` after the pixels, so on any WebP above the scan window a fixed read stops short of the label. `_riff_late_metadata` steps over the coded image to reach it, the RIFF analogue of the existing PNG and ISOBMFF readers. Three corpus files hid an IPTC "Made with AI" tag and a C2PA `trainedAlgorithmicMedia` there. The decoder-backed fallback now covers only what it is actually for -- metadata the raw bytes do not spell, such as a compressed PNG `zTXt` packet. A C2PA reader failure returned the same `None` as a file with no manifest, so a verdict could fall back to the raw byte scan with no trace anywhere. Failures now log at warning and only genuine ones do: a file without credentials never reaches that branch, and an unsupported container is demoted to debug through the reader's own `C2paError.NotSupported`. The first corpus run with it found a truncated PNG. `scan_dataset.py` never registered the pillow-heif opener it declares as a dependency, so every HEIC was scanned as unreadable -- no EXIF, and a pixel layer that was 397 of 406 features NaN instead of 136. `_riff_late_metadata` caps its total like `isobmff.scan_c2pa_region` does. Clamping each chunk to the bytes remaining is not enough on its own: one chunk can declare a length spanning most of the file, and this runs on the memoized verdict path over images from arbitrary sources. Also lands `identify_metadata_record` and `ProvenanceReport.to_dict()`, the one-call entry point and the versioned JSON contract for the record path. Record-vs-file equality holds over 3,478 corpus images, and the eight files these fixes recovered still report AI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.1 KiB
globs, description
| globs | description | |||||||
|---|---|---|---|---|---|---|---|---|
|
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.
From a worktree, uv run imports the package from the MAIN checkout -- that is where the editable install points. A script measuring a worktree's edit must insert that worktree's src at sys.path[0] and assert module.__file__ resolves inside it, or it silently compares unmodified code against itself.
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-zimageshipped 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
InvisibleOptionsdefaults, intest_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 indocs/module-internals.md. Keep the comparison free of an exception table: a field that needs one is a field that belongs elsewhere, which is whatforceturned out to be.
A defaults comparison is not a forwarding test, and the two fail differently. Pin the
VALUE at the seam, not just the name -- _run_invisible passed the whole suite with
controlnet_conditioning_scale hardcoded, because nothing asserted the caller's value
arrived. test_every_field_arrives_at_the_engine_with_the_caller_s_value drives the
real seam with every field set off its default, so one test covers the whole bag
instead of one assertion per knob.
Count the seams before believing a knob is covered. Each of force and
controlnet_conditioning_scale reaches the engine through TWO paths -- remove_all
versus remove_batch(mode="all") for the first, _run_invisible versus _batch_engine
for the second -- and in both cases guarding one path left the other free to hardcode a
constant with a green suite. The mode-parametrized guards in
TestRemoveBatchLibrary::test_force_reaches_the_scrub_gate_in_every_scrubbing_mode and
TestBatchCommand::test_batch_controlnet_scale_flows_to_the_cached_engine exist because
that is what actually happened.
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_gatehook (or, for a whole-scan precondition like LibLibAI's size floor, in_scan) -- never in adetectoverride. An override is invisible todetect_both, so the RunningHub and Yuanbao anchor gates silently stopped applying on the arbiter's perception path.TestSinglePassPerceptionis the guard: it assertsdetect_bothequals twodetectcalls field for field. - Detection and the removal mask must read ONE sweep. The winning box travels on
TextMarkDetection.match_boxand the registry threads the detection into the mask builder; a mask path that re-runs its own sweep is how the two drift apart.
The C2PA manifest-store JSON is NOT stable across reads: the reader regenerates manifest
URNs and instance ids. Compare the derived c2pa_info, never the raw store.
A third seam reaches the same verdict: collect_metadata_record ->
evidence_from_metadata_record -> identify_from_evidence, the path a caller uses when
collection and verdict run on different machines. Its contract is equality with
identify(path, check_visible=False, check_invisible=False) on the same image, and it
can break from EITHER side -- a region the collector stops walking, or a placement the
file path learns to read and the record does not. tests/test_metadata_record.py pins
it over the tracked fixtures; a separate local evaluation corpus catches placements the
fixtures do not cover. Change either side and re-run both.
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. A change that is meant to FIX detection is the exception that proves the rule: the diff must then be exactly the files you intended to change, named in advance.
Environment setup, dependency recovery, CI behavior, and fixture policy: ../../docs/development.md.