Repository navigation
Foundation: enforce the Images API's 280 px minimum, hash image bytes in replay keys, bound tiles (#306) - #321
Conversation
… in replay keys, bound tiles (#306) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Opus reviewer (independent review 1 of 2): Claude Opus 5.5 (
SummaryThe code is correct and well tested. I checked the limits against the live upstream page and they match exactly. The boundary tests cover both axes, and a mutation run kills every enforced rule. S1, S3, S5 and S7 are all handled. I am asking for one small docs change, because this PR freezes the image replay-key contract and leaves a promise in BlockingB1.
|
| Mutation | Result |
|---|---|
M1 min_side back to 256 |
killed |
| M2 key hashes the base64 text again (S3 reverted) | killed |
| M3 height minimum unchecked | killed |
| M4 width minimum unchecked | killed |
| M10 height maximum unchecked | killed |
M8 max_side 1569 |
killed |
| M5 tile cap removed | killed |
M6 tile cap made exclusive (>=) |
killed |
M7 box_iou returns 0.0 instead of raising |
killed |
| M9 area check removed | survived (unreachable, see suggestion 2) |
Checks run (fresh venv, Python 3.14.3, pip install -e ".[dev]", Windows, no TYPESAFE_API_KEY)
ruff check .: All checks passed.ruff format --check .: 395 files already formatted.- Full
pytest: 4951 passed, 58 skipped, 50 deselected, 31 xfailed, 2 failed, in 542.66 s. The two failures aretests/test_notebook_ci_sandbox.py::test_kills_every_non_zombie_process_and_returns_once_none_remainand::test_a_process_that_will_not_die_fails_closed, the known Windows-onlysignal.SIGKILLcases (Foundation: two notebook-CI sandbox tests use signal.SIGKILL and fail on Windows #302). This PR does not touch that code. Nothing else failed, includingtest_hygiene.py::test_the_repository_itself_is_clean, which the builder had only re-run partially. python tools/render_catalog.py --check: exit 0.python tools/check_hygiene.py: Hygiene check passed, 604 files scanned.python -m jev_cookbook.fixtures validate --all: all 50 recipes and_templatevalid (mode replay).- Notebooks: no recipe, template or tool imports
jev_cookbook.vision. Thebackends.pychange only affects the non-empty-assetsbranch. Text-only keys are pinned bytest_text_only_replay_keys_are_pinned_to_the_published_envelope, and each recipe's byte-identical-responses test passed in the full run. So no notebook can be affected, and I did not re-execute them locally. In CI, everyNotebook (...)job passed on this head, executed offline in a scratch copy and checked for freshness. gh pr checks 321: 60 of 60 pass, covering lint, Tests py3.10 and py3.14, catalog, hygiene, fixtures, scope and every notebook.
Other things I checked and found correct
- The extra
sha256field that_check_assetsnow returns is consumed only byreplay_key.live.py:696-704reads onlydata_base64, so nothing new reaches the SDK, a recording or the ledger. A caller cannot inject its ownsha256, becausevalidate_image_assetrequires exactly{type, format, data_base64}. - The key digest is the same one
image_referenceembeds (tested). The non-canonical twin helper really produces different base64 for identical bytes, the replay hit works across the two spellings, and reverting S3 is caught (M2). box_iouraises when the union is 0. One tiny box against a normal box is still defined (0.0), andlocalization_metricspropagates theValueError. Raising is a defensible choice: returning 0.0 would score two identical boxes as disjoint.generate_tileschecks the tile count before any crop. The test patchesImage.cropto fail if it is reached, and covers the 1568² overlap-511 case. The default of 256 still allows any sensible grid: the docs example(320, 280)with overlap 32 on 1568² makes 42 tiles.max_tilesrejects0,-1,True,2.5and"9".- The
box_confidencedocstring is corrected, and its test asserts that a reported 0.0 and 1.0 come back unchanged and that absence staysNone. - No test was deleted or loosened. Changing the 256-pixel fixtures to 280 keeps every assertion, and the rejection tests are stricter because they now match the message.
Acceptance criteria (#306)
-
vision.pyenforces 280-1568 per side.docs/vision.mdand thevalidate_image_assetdocstring say 280, and the example sizes, includingtile_size=(320, 280)and "256 × 256 does not", were rechecked. - Boundary cases (280, 280) and (1568, 1568) are accepted, and (279, 400), (400, 279) and (256, 307) are rejected, plus both axes at the maximum.
- A docs-drift test pins the limit numbers in one constant table (
IMAGE_LIMITS), with a comment citing the upstream page and the 2026-10-20 fetch date. -
box_iouraisesValueErrorwhen the union is 0, with a parametrized 1e-200 test throughbox_iouandlocalization_metrics. - Decision recorded on S3: partly met. The key now hashes the decoded bytes, the
QQ==/QR==test is present and the text-key pin is green. However,docs/backends.md:294-295still saysvis bumped on any rule change and was not updated to record that the image member was redefined atv: 1(B1). -
generate_tilestakesmax_tiles=(default 256) and has tests. - The
box_confidencedocstring is corrected.
OPUS-REVIEW: CHANGES-NEEDED 424790b
|
Opus reviewer (independent review 2 of 2). Model: Claude Opus 5.5 (
Blocking findingsNone. Did the three contracts work for the recipe builders who will use them?I went through each one as the builder of a Box recipe (77-84) and of the tiled-search recipe (83).
The only configuration refused is one that would cost more than 400 API calls per image. That is a reasonable thing to stop by default, and the error message says what to change. The tile count is computed from start positions before any crop, and the test proves it by patching Replay key hashes decoded bytes (S3): right contract, applied consistently. Every place a key is built goes through Mutation evidence. I applied each mutation to the source, ran
Suggestions (not blocking)
Checks run (fresh venv, Python 3.14, Windows, no
|
…ge policy (#307) (#322) Closes #307 ## What this adds The image-file contract for multimodal recipes, a checker for it, and the fixes to the scope, freshness and attribute handling of binary files. Contract in `docs/fixtures.md` (Image fixtures); notebook rules in `docs/notebook-ci.md` (staleness check rules 2-5). - `tools/check_recipe_assets.py` (stdlib only), second step of the `Hygiene (secrets and notebook outputs)` job and a pre-commit hook: location (`recipes/<slug>/assets/` only), PNG/baseline JPEG with lowercase suffix and matching bytes, 280-1568 px / 78,400-2,458,624 area, 1 MiB per file, 8 MiB per folder, no metadata, no symlinks. Parsing is allowlist-based for both formats (see fix round 1). - `tools/check_recipe_scope.py`: an image suffix added/modified/renamed anywhere but `recipes/<slug>/assets/` is rejected (deletion allowed). - `tools/check_notebook_fresh.py`: `image/jpeg` is normalised like `image/png` (Pillow displays every image as both); output cap 2 MiB; embedded images are metadata-checked in both files. - `.gitattributes` `*.jpeg binary`; `docs/recipe-template.md` `assets/` row; `CONTRIBUTING.md`, `docs/development.md`, `.pre-commit-config.yaml` updated. ## Fix round 1 (both Opus reviews on 367a1e3 were CHANGES-NEEDED) Review 1: - **B1** metadata smuggling: JPEG is now a marker allowlist (SOI, one exactly-16-byte JFIF APP0 with no thumbnail, whole DQT/DHT tables only, DRI of length 4, one SOF0 8-bit with 1 or 3 components, SOS with consistent length, RSTn, EOI); everything else is `jpeg-metadata` or `jpeg-structure`. PNG: exact length for gAMA/cHRM/sRGB/pHYs/sBIT/bKGD/tRNS, PLTE only in palette images, IHDR validated, IDAT consecutive and one zlib stream with nothing after it. One negative test per carrier from the review (text after JFIF, second APP0, thumbnail, DNL, JPG0, 0xFF02, DRI, PLTE in RGB, pHYs/bKGD with text, IDAT trailing text, ...). - **S5** CRC test now corrupts byte 20 and asserts `bad CRC in chunk IHDR` (mutation of the CRC check is killed; I checked). - **B2** notebook outputs: every committed `image/png` and `image/jpeg` output (and every `data:image` URI in an output) is checked with the asset rules in both files; matplotlib's short `tEXt Software` is the only text allowed. Test: EXIF JPEG committed vs clean fresh JPEG now fails. Markdown/raw cells may not embed an image (attachment or data URI); `image/gif` etc. outputs are problems. All notebooks on main still pass (tested). - **B3** `.pre-commit-config.yaml` hook, `docs/development.md` Hygiene row and section, `CONTRIBUTING.md` `assets/` entry; each pinned by a test. - **B4** `sniff` now also detects BMP, ICO, HEIF/AVIF, JPEG 2000 and SVG by content; docs list exactly the detected set and state that base64/data: images inside text files are not detected. - **S2** documented: 8/16-bit and interlaced PNGs accepted; JPEG precision 8, 1 or 3 components enforced. **S3** noted the cap runs in the Notebook job. **S1** test pins the checker limits to the documented 280-1568 and, if `jev_cookbook.vision.IMAGE_LIMITS` exists (PR #321), to it; `vision.py` untouched. Review 2: - **Blocking 1** the "twice its file" claim is replaced in `docs/notebook-ci.md` and the `tools/check_notebook_fresh.py` comment with the measured 8.6x to 22x factors; both recommend `IPython.display.Image(filename=path)` and `Image.thumbnail((640, 640))`; `docs/fixtures.md` carries the same guidance. - **Suggestions** taken: `savefig(metadata={"Software": None})` documented; `file-too-large` message and doc suggest JPEG for photographic content; ICC warning corrected to PNG only; CMYK/Adobe note. Not taken: #321 ordering (vision.md still says 256 until #321 merges). ## Verification (fresh venv, Windows, after merging origin/main 7fa806e with a signed merge commit) - `ruff check .` pass; `ruff format --check .` 397 files formatted. - `pytest`: the full run before the last test-only fix had 4 failures: the 2 known Windows `SIGKILL` ones (#302), `test_a_scripted_scaffold_runs_from_the_scaffold_to_a_byte_identical_notebook` (kernel start timeout under load; passes on rerun), and `test_style_notebook_display.py::test_apply_style_does_nothing_outside_ipython` (caused by a new test of mine creating a global IPython shell; fixed to use `DisplayFormatter()`; rerun of the three affected files: 143 passed). The final head was not re-run in full; it differs from the full run only by that test fix. - `render_catalog.py --check` exit 0; `check_hygiene.py` 606 files pass; `check_recipe_assets.py` 461 files pass; `notebook_ci.py fixtures` 51 folders, 0 failed. Two independent Opus reviews requested by the issue; this is a new head, so earlier approvals do not apply. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Fix round 2 (head 171240f; re-review 1 CHANGES-NEEDED, re-review 2 APPROVE on 3456211) - **R1 (blocking)**: the text-cell rule now matches `data:\s*image/` case-insensitively with any parameters, base64 or not; cell metadata and notebook metadata (values, nested values and keys such as `image/jpeg`) are scanned too; the output data-URI regex is case-insensitive and accepts parameters. One test per spelling (upper-case scheme, mixed-case type, one and two parameters, whitespace, non-base64 SVG, markdown syntax; markdown and raw cells) and per metadata placement. - **S7 (cheap PNG hardening)**: PNG chunks the spec allows once (gAMA, cHRM, sRGB, sBIT, pHYs, bKGD, tRNS) are rejected when repeated; the inflated IDAT length must equal the length implied by width, height, depth, colour type and interlace (including the Adam7 passes; the test computes the expected size independently). Tests include text appended inside the zlib stream. - **S8**: an embedded PNG output may hold at most one `tEXt` chunk, and it must match matplotlib's own wording (`Software\0Matplotlib version<N>, https://matplotlib.org/`). - **S6**: tests for a second exact JFIF APP0, a late APP0, and the marker allowlist (unknown markers with a valid table body are refused with the allowlist message). I confirmed by mutation that removing either guard now fails a test. - **Docs**: `docs/fixtures.md` says the rules remove metadata ordinary tools write and are not a detector for deliberately hidden bytes; the output-cap failure message recommends `IPython.display.Image(filename=path)`. - Not added, per the scope decision: table-content and pre-EOI-data rules. - **Verification on this head (origin/main had not moved; no merge needed)**: ruff check and format clean; `render_catalog --check` 0; hygiene 606 files; `check_recipe_assets` 461 files; fixtures 51/0; full `pytest`: 2 failed, 5284 passed, 58 skipped, 50 deselected, 31 xfailed. The 2 failures are the known Windows `SIGKILL` tests (#302). --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…an image replay key (#306) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Opus reviewer (independent review 1 of 2, re-review): Claude Opus 5.5 (
Is my earlier review resolved?B1 (blocking) is resolved. Suggestions:
Other new text in The merge, and the asset checker against
|
| Mutation | Result |
|---|---|
M1 min_side 256 |
killed (15 failures) |
| M2 hash the base64 text | killed (4, now including the literal pin) |
| M3 / M4 height or width minimum unchecked | killed (4 / 6) |
| M10 height maximum unchecked | killed (1) |
M8 max_side 1569 |
killed (14) |
| M5 tile cap removed | killed (2) |
| M6 tile cap exclusive | killed (1) |
M7 box_iou returns 0.0 |
killed (3) |
M11 max_area 2,458,625 |
killed (2) |
| M9 area check removed | survived (the check cannot be reached: min_area = min_side² and max_area = max_side²) |
I also re-checked the upstream page directly: docs.typesafe.ai/images.md, fetched on 2026-10-20, gives 280 to 1568 px per side, 78,400 to 2,458,624 px in all, and 16 MiB per file, with both ends of each range included. These match.
Acceptance criteria (#306)
-
vision.pyenforces 280 to 1568 per side, and the docs and the docstring say 280. The worked examples were rechecked. - The boundary cases are tested on both axes: (280, 280) and (1568, 1568) are accepted; (279, 400), (400, 279) and (256, 307) are rejected; the maxima are covered too.
- A docs-drift test pins
IMAGE_LIMITS, and its comment cites the upstream page and the fetch date. The asset checker is now cross-pinned to the same values. -
box_iouraisesValueErrorwhen the union is zero. A parametrized 1e-200 case coversbox_iouandlocalization_metrics. - The S3 decision is recorded. Keys now hash the decoded bytes. The
QQ==/QR==test, the literal image-key pin and the text-key pin all pass, and the key-version rule is documented truthfully withvkept at 1 (B1). -
generate_tilestakesmax_tiles=(default 256), with tests. - The
box_confidencedocstring is corrected.
OPUS-REVIEW: APPROVE f486636
|
Opus reviewer (independent review 2 of 2, re-review). Model: Claude Opus 5.5 (
Blocking findingsNone. My earlier notes
Since 424790bThe literal image key pin (
The asset checker's limits match
The rename case is still caught elsewhere: Docs after the merge. Suggestions (not blocking)
Checks run (fresh venv
|
) Closes #311 ## What this adds - `vision.gold_box(label)`: decodes and validates the gold Box label `{"present": true, "box": {top, bottom, left, right}}` / `{"present": false}` (strict keys, boolean `present`, no `box` on an absent label, edges in [0, 1], positive size, and a gold area that does not underflow to zero, so `box_iou` can never hit #321's zero-union error against a gold box). - `vision.localization_report(gold, outcomes, *, iou_threshold=0.5)` and `LocalizationReport`: takes `("located", box)` / `("absent", None)` / `("review", None)` per example; a present table and an absent table count every example exactly once; mean IoU over located-present and over all present (missed and review count 0), hit rate at the threshold, false positives, correct absents; `str(report)` and `report.denominators` name every denominator. A box returned for an absent target is a false positive, never an IoU; a review is never an absent prediction. - Fixture validator: a top-level label object with a `present` key declares Box labels; each must pass `gold_box`, and a non-Box label among them is an error. `schema.json` description points to the format. - `localization_metrics` kept; its docstring and `docs/vision.md` point to the report. - Docs: `docs/fixtures.md` ("Box labels"), `docs/vision.md` (gold labels, the report, the worked example), `docs/evaluation.md` (Box localization table of denominators). Design note for the reviewers: a Box label nested under another key (rank 98 mixed types, `{"target": {"present": ..., "box": ...}, "rubric": 3}`) is not seen by the validator; such recipes call `gold_box(label["target"])` themselves. A tile choice is not stored beside a gold box: the box is stored once, full-image, and Python derives tiles from it (`docs/vision.md`, "Tiled and hierarchical recipes"; `docs/fixtures.md`, "Box labels"). "Declared" is by the reserved top-level `present` key because no per-recipe declaration mechanism exists. Raise it if you prefer an explicit marker. ## Recipe checklist Foundation work; section skipped. ## How it was verified Merged head 23182da (signed merge of origin/main c316712 into 865dfcc), Linux, fresh Python 3.10 venv: - `ruff check .`: All checks passed. `ruff format --check .`: 397 files already formatted. - `pytest`: 5409 passed, 49 skipped, 50 deselected, 31 xfailed. (865dfcc also passed on Python 3.14: 5408 passed.) - `python tools/render_catalog.py --check`: exit 0. `python tools/check_hygiene.py`: passed, 606 files. `python tools/check_recipe_assets.py`: passed. `python tools/notebook_ci.py fixtures`: 51 recipe folders, 0 failed. - No notebook touches `vision.localization_*`, so no notebook execution is affected. - New tests (`tests/test_vision.py`, `tests/test_fixtures.py`) use hand-computed values on dyadic rectangles: mixed, all-absent, all-review, a reviewed present target changing the denominator, a box returned for an absent target, a present target called absent, a degenerate (underflowing) gold box, threshold inclusivity, malformed inputs; the vision.md worked example is executed and its printed text compared. Two docs-structure tests pin the Box-labels examples and the tiled-recipe statements. --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…309) (#323) Closes #309 ## What this adds - `src/jev_cookbook/frames.py` (new, pure Python, no new dependency): `sample_frames`, `frame_timestamps`, `select_frames` (copies carry `frame_index`), `frame_intervals`, `relative_frames`, `window_timestamps`, `frame_at`, `frame_windows`, `transition_time`, `earliest_transition`. - `evaluation.transition_metrics` + `TransitionMetrics`: exact and within-tolerance accuracy, review count with stated denominators (resolved and all), label-matched earliest-transition error in seconds (plus separately named any-label fields), per-example `outcomes` and `timing_errors_seconds`, `max_sampling_interval_seconds` / `tolerance_below_sampling_interval`. - `docs/vision.md` (sampling rules, tie-breaking, no video decoding, elapsed-seconds convention, index spaces, recording the selection in `fields`) and `docs/evaluation.md` (the transition-time convention, tolerance versus sampling gap, review labels, multi-transition sequences, result type row). - Tests: `tests/test_frames.py` (new), additions to `tests/test_evaluation.py`; `tests/test_docs_examples.py` bumped for the one new python block in `docs/evaluation.md` (8 to 9 blocks, empty preamble). Deliberate deviation from the issue text: the helpers live in `jev_cookbook.frames`, not `jev_cookbook.vision` (#321 edits `vision.py`; it is untouched). `jev_cookbook.frames` is the single import path; no re-export is planned. ## Review round 1 resolutions (head 8ff6588) - Timing error counted wrong-label predictions (both reviews): the headline `n_timed` and means now cover only label-matched examples with times on both sides; the all-labels view is `n_timed_any_label` and `*_any_label_*`. Tests and docs updated (hand-computed: n_timed 2, MAE 0.2). - Tie rule with frames closer than the slack (review 1 B2): the nearest search now considers the neighbouring frames and takes the earliest within the slack of the closest. Tests include `[0, 1, 1+5e-10, 2]` giving `[0, 1, 3]`. - No transition-time convention (review 2 B1): one convention, the `timestamp_seconds` of the first frame in which the new state is visible, for gold and predictions. `frames.transition_time` and `frames.earliest_transition` implement it; docs say tolerance should be at least the largest sampled gap; `max_sampling_interval_seconds` is recorded and `tolerance_below_sampling_interval` flags the opposite. A hand-computed end-to-end test pins it. - Suggestions taken: JSON two-element lists accepted; `uncertain_labels` takes several review labels; `ValueError` (not `OverflowError`) for huge times, one shared validator; per-example outcomes; `frame_at` and `frame_windows`; `frame_index` on selected copies; docs on position-based `count`, elapsed seconds, multi-transition sequences, and mapping the model's uncertain option and Python's gate to the review labels. Merged `origin/main` (7fa806e) with a signed merge commit; no conflicts (#318's `docs/evaluation.md` edits merged cleanly alongside this section). ## Review round 2 resolutions (head 2cef5c7) - `earliest_transition` accepts `None` for an undetermined pair (and NumPy booleans). An undetermined pair before the first `True`, or with no `True`, returns `{"review": True, "first_undetermined_index": i, ...}` with no time, never a later transition. Confirmed results now carry `"review": False`. Docs say to score it with an `uncertain_labels` label. Tested. - `frame_windows` appends a final window aligned to `end` when the last stride-spaced window stops short. Every frame lies in some window when `stride_seconds <= width_seconds`; with a larger stride the windows are deliberately sparse and frames between them are in no window (documented with an example, tested). Tested with frames every 0.5 s to 8 s, width 3, stride 2 (windows start 0, 2, 4, 5) and with a small case. - `docs/vision.md` states the actual tie guarantee (up to two frames within the slack around a target); "can only be late" now says a correct detection; docs say to drop `frame_index` too; `select_frames` validates a pre-existing `frame_index`; `uncertain_labels=None` (or a non-collection) raises `ValueError`. ## Recipe checklist Skipped (foundation work). ## How it was verified Final head 42dc36a (signed merge of origin/main 2601074 into a9c7922, after two fix rounds), Linux, fresh Python 3.14 venv: - `ruff check .`: All checks passed. `ruff format --check .`: 399 files already formatted. - `pytest`: 5547 passed, 49 skipped, 50 deselected, 31 xfailed, 0 failed. - `python tools/check_hygiene.py`: passed, 608 files. `python tools/check_recipe_assets.py`: passed, 461 files. `python tools/render_catalog.py --check`: exit 0. `python tools/notebook_ci.py fixtures`: 51 recipe folders, 0 failed. - No notebook is affected (no recipe imports the new code). `tests/test_docs_examples.py` executes the `docs/evaluation.md` blocks and the `docs/vision.md` frame walkthrough. No live call, no key; nothing here claims anything about Jev. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Closes #306
What this adds
Fixes
vision.pyaccepting 256-279 px image sides that the Images API rejects, and closes #297's carried-over review items S1, S3, S5 and S7. Foundation work; no recipe or notebook changes. This defines contracts every image fixture binds to (the size limits and the image replay-key envelope), so it needs two independent reviews.Upstream limits, quoted from https://docs.typesafe.ai/images.md (Limits table, fetched 2026-10-20 via
llms.txt-> Images):Behaviour changes (each tested)
vision.py). Area bounds and 16 MiB are unchanged and now live with the sides in one table,vision.IMAGE_LIMITS, with a comment citing the upstream page and fetch date. Tests: 279x400 and 400x279 rejected (viaimage_asset,validate_image_assetandreplay_key), 280x280, 280x1568, 1568x280, 1568x1568 accepted, 256x307 now rejected. Existing tests that used 256-px fixtures were moved to 280 (320x256 -> 320x280); no assertion was loosened.test_image_limits_match_the_upstream_table_and_the_docspinsIMAGE_LIMITSto a hand-copied upstream table and checksdocs/vision.mdand thevalidate_image_assetdocstring say 280-1568.box_iouraisesValueError("union underflows to zero") when the union is 0, rather thanZeroDivisionError. I chose raising over returning 0.0 because two identical 1e-200 boxes would otherwise score 0 (disjoint) silently. Parametrized test throughbox_iouandlocalization_metrics; one underflowing box against a normal box still returns 0.0.payload["assets"][ref]["sha256"]is now the SHA-256 of the decoded file bytes (the digest already insideimage_reference), not of the base64 text.QQ==/QR==-style non-canonical base64 of identical bytes now yields one key (tested with a real PNG, plus a replay hit across the two spellings). The envelope is otherwise unchanged ("v": 1,type, canonicalformat,sha256). Confirmed withgit grepandgit ls-filesthat no image fixture exists onmain(the only.pngfiles are the banner and the notebook-style docs images), so no recorded key changes;test_text_only_replay_keys_are_pinned_to_the_published_envelopeis untouched and green. A test rebuilds the documented envelope independently and compares the key. Documented indocs/backends.md(and cross-referenced indocs/vision.md).generate_tiles(..., max_tiles=256)(DEFAULT_MAX_TILES). The tile count is computed from the start positions before any crop; above the cap it raisesValueError. Tested including the overlap-511 case on 1568x1568 (~1.1M tiles) withcroppatched to fail if reached, inclusive boundary, raised cap, and invalidmax_tiles.box_confidencedocstring anddocs/vision.mdnow say a reported 0.0/1.0 is returned as reported and only absence is never coerced. Docstring test.Rechecked docs examples
docs/vision.md: "384 x 320 fits" stays true; "a 256 x 256 fixture does not" stays true (now also 279 x 400 / 400 x 279 named);tile_size=(320, 280)is valid because both sides are >= 280 (a note says tiles/sources under 280 must be scaled or padded beforeimage_asset). Other docs mention no 256 limit.Recipe checklist
Skipped: foundation work.
How it was verified
Fresh venv,
pip install -e ".[dev]", Windows:ruff check .-> All checks passedruff format --check .-> 395 files already formattedpython tools/render_catalog.py --check-> passes (no output)python tools/check_hygiene.py-> "Hygiene check passed: 604 file(s) scanned." (an earlier run caught a 64-char alphabet literal in my new test; replaced bystringconstants)python -m jev_cookbook.fixtures validate --all-> every recipe validpytest(full): 4950 passed, 58 skipped, 31 xfailed; the only failures were the two known Windows-onlytests/test_notebook_ci_sandbox.pysignal.SIGKILLtests (Foundation: two notebook-CI sandbox tests use signal.SIGKILL and fail on Windows #302) andtest_hygiene.py::test_the_repository_itself_is_clean, which failed on the alphabet literal above and passes after the fix (re-run: 494 passed on the affected files).jev_cookbook.vision(git grep), and text-only replay keys are pinned unchanged, so no notebook output can change; I did not re-execute notebooks.No live call, no API key.