Repository navigation
Ratify RULE G and RULE N; the CLI on cw; RULE G's compile half - #4
Merged
Merged
Conversation
**Both owner questions ratified 2026-09-02.** RULE G: `burns` stays separate and
gains a `looks`-backed ffmpeg backend — so `looks` owns the COMPILE side
(keyframes to a filter fragment) and `burns` samples its BurnsPath and runs the
argv. The dependency points burns -> looks, legal because looks is stdlib-only;
the reverse stays forbidden. RULE N: yes, `looks` owns normalisation, exactly as
already shipped in `Look.target`.
**The CLI is `cw`** ($PP/i/cw), which was the right answer and which I had
missed: MIT, `dependencies = []`, and — verified in a fresh interpreter —
`import cw` pulls in NOTHING beyond stdlib; the argcomplete/i2 imports an AST
walk finds are all inside function bodies. It is the fleet's own argh
replacement, so `looks` neither imports the LGPL package nor hand-rolls
argparse. Dispatched under `cw.MODERN` rather than its argh-compatible default.
Eight commands, and `check` is the headline:
$ looks check scale eq lut3d
eq
1 of 3 need a GPL ffmpeg build. LGPL-clean substitutions: curves / lutyuv /
colorlevels / colorbalance / exposure for eq; ...
$ echo $?
1
Non-zero on purpose, so a build script can gate on it — a refusal that exits 0
is a warning wearing a refusal's clothes. A typo exits non-zero too, with a
message rather than a traceback: an unrecognised filter name is a refusal, not
a missing answer.
**`cw` could not read the annotations this package is written with.** argh
recognises `list` and nothing else, so `filters: Sequence[str]` bound ONE token
and reported the rest as unrecognised — quietly, since the parser builds and
--help looks right. Contributed the fix upstream as i2mint/cw#36: `modern_decode`
now reads Sequence / Iterable / Collection / Set / tuple / set / frozenset,
subscripted or bare, while `argh_decode` stays argh's if-chain untouched. `str`
and `bytes` are excluded BY CONSTRUCTION (matching on `typing.get_origin`, which
is None for both) rather than by a special case — both are registered Sequences,
so an issubclass test would have read every string parameter as variadic.
**Two of the package's own guards caught me while doing this**, which is the
best evidence they work:
- `test_every_runtime_extra_has_a_ledger_row` refused the new `[cli]` extra:
"an extra is a conveyance decision, not a convenience list." So `cw` has a
ledger row now, with both observations dated — the metadata read AND the
fresh-interpreter run that shows the empty dependency list is accurate rather
than optimistic, which is not the usual case.
- The pin over §5.7's table refused a silently-grown ledger, so the row is in
the decisions document too.
The zero-dependency test needed rework rather than an exemption. Forbidding any
third-party NAME in the source was too strong the moment an optional extra
arrived. It is now two tests that together say something stronger: importing
`looks` must pull in nothing third-party (asked of the interpreter, not the
AST), and any third-party name in the source must be a DECLARED extra imported
lazily. Adding a dependency therefore forces a pyproject edit, which the ledger
coverage test then forces into the ledger.
Also: `__version__` is now resolved on first access (PEP 562). Reading it
eagerly cost **50 ms of import time** — `importlib.metadata` pulls in
`email.parser` — for a value most callers never look at. 54.5 ms eager, 38.6 ms
lazy, with the honest-version property intact. Pinned by a test that asserts
`email.parser` is absent after `import looks`.
Claude-Session: https://claude.ai/code/session_01CnXLihXKG9jfvygHvgKVpX
…t tomllib
CI went red on Python 3.10: `tomllib` is 3.11+, and the new zero-dependency
scan imports it. The obvious fix — skip on 3.10 — is what the OTHER guard
already did, and finding that is what makes this a real fix rather than a
one-line patch:
test_every_runtime_extra_has_a_ledger_row:
except ImportError: # Python 3.10
pytest.skip("tomllib needs Python 3.11+")
So the guard that refuses an undeclared conveyance — the one that caught `cw`
yesterday — has been reporting **skipped** on half of CI since it was written.
A skip reads as a pass. Two guards, two different failures, one cause: a
licence guard cannot read the declaration on an interpreter this package
claims to support.
`looks/tests/_pyproject.py` reads the file on every version. It is a small
reader for the shapes this one file uses — table headers, strings, arrays of
strings — and it is deliberately NOT a TOML parser. A hand-rolled reader is
normally a liability: it agrees with the real parser until the day it quietly
does not, and everything built on it goes on reporting green.
What removes that liability is `test_pyproject_reader.py`. On every
interpreter that HAS `tomllib` — which is one of CI's two legs, every run —
the reader is run against it over the REAL pyproject, comparing every table
and every key, in both directions (a missed table would make a guard read an
empty declaration and pass vacuously; an invented one would make it read
fiction). The fallback is never trusted, only checked, against the same bytes
the 3.10 leg will read.
Three shapes in the live file break a naive reader, and each has a test:
`publish_marker = "[skip ci]"` is a bracket that must not open an array;
`"looks[cli]"` is one that must not either; and a comment two lines above an
array says "CI's publish job", whose apostrophe would open a string that never
closes. Comments are stripped BEFORE strings are masked, which is the ordering
that handles all three.
`importlib.metadata` was the tempting alternative and is wrong here: measured
on this machine it reports `Provides-Extra: ['dev', 'docs']` — no `cli` — because
an editable install's dist-info is a snapshot of install time and nothing
refreshes it. A licence guard reading that would have said the extra does not
exist.
Verified on a real 3.10 interpreter, not by reasoning about it: both guards now
RUN there (8 passed, 3 skipped — the 3 being the cross-checks, which belong to
the leg that has a parser). Full suite 503 passed / 80 skipped on 3.10 and 580
passed / 3 skipped on 3.12, same 583 collected on both; every one of the 77
extra skips accounted for (60 geometry cross-checks against `mixing`, 13 CLI
tests wanting `cw`, 3 meta-checks, 1 imageio-ffmpeg).
Claude-Session: https://claude.ai/code/session_01CnXLihXKG9jfvygHvgKVpX
…easurement RULE G was ratified this session: `burns` keeps authored geometry, `looks` owns compiling it. This is the `looks` half — keyframes in, an ffmpeg fragment out. It does not ease, does not sample, does not decide where a camera goes, and never imports `burns`; a window is anything with .x/.y/.w/.h in burns' normalised convention, so the structural type is the seam. **Which filter is not a matter of taste, and the fleet's recorded answer was wrong.** `muvid._crop_filter`'s docstring — propagated by two of this package's own research notes into a warning aimed at the `burns` owner — says *"Not zoompan: its expression vocabulary has no t at all, and it duplicates frames on video input."* Measured on ffmpeg 8.1 today, both premises are true and the conclusion does not follow: - `t` really is undefined in zoompan. But `in_time` and `it` are not, and `ffmpeg -h filter=zoompan` prints no variable list at all — which is very likely how the recorded fact came to be half-complete. - It really does duplicate frames. At the DEFAULT d: 1800 out for 20 in. At `d=1` it is exactly 20 for 20. Meanwhile the filter the notes recommended instead cannot do the job at all. `crop`'s w/h do not merely evaluate once — `t` is not in scope and the filter REFUSES TO CONFIGURE (`Error when evaluating the expression`), reproduced in three invocation forms. That is stronger than this repo's own earlier note, which recorded it as "evaluated once ... produced 60 frames of unchanged size"; nothing renders. So a Ken Burns path, which is a zoom by definition, has exactly one compilable form. muvid's own code is fine — it compiles a *pan*, the case where crop is right and simpler. What was wrong was generalising from "wrong for my case" to "wrong filter". **Three more zoompan traps, none of them in the notes, each a silent wrong answer rather than an error — which is why each is now a refusal:** - its x/y are in ORIGINAL input pixels, not zoomed ones. The two readings score 60.5 dB and 6.3 dB against a crop+scale reference for the same window. The first implementation here had it wrong, and it configured cleanly. - its `fps` silently retimes. At fps=25 on a 10 fps source the frame count is unchanged and a 2.0 s clip becomes 0.8 s — so a frame-count check, the obvious check, passes. Required here, never defaulted. - its `zoom` is clamped at 10. z=10 scores 54.4 dB against the reference and z=12 scores 13.2 — not a worse rendering of the window asked for, a correct rendering of a different one. Windows below 1/10 of the frame are refused. Also refused: a window that leaves the frame (ffmpeg would clamp to a different framing, silently), keyframes that do not advance in time, a window that is not the source's shape (zoompan's zoom is one scalar, so honouring one axis would distort the other), and a zoom with no delivery size or rate. The ramp is a sum of clamped terms rather than nested `if()`: exact at every knot, linear between, and flat outside the path at both ends — so a clip that outlives its path parks on the last window instead of drifting off it. A segment that does not move emits no term, so a still axis is a constant that ffmpeg reads once rather than an expression it evaluates per frame. **The tests ask ffmpeg, not the strings.** String equality would pass for a fragment ffmpeg refuses and equally for one that renders the wrong picture — both of which happened here. So each fragment is built a second way by hand and the two are compared with ffmpeg's own `psnr`: a static window is bit-identical to the crop it names, a zoom is >45 dB from its crop+scale equivalent, and the two filters' clocks agree frame for frame on the same pan. The threshold is not a tuning knob — correct pairings score 54-60 dB and the nearest wrong ones score 6-13. Full transcript: docs/research/00f_motion_filters_evidence.md. The warning to hand the burns owner is withdrawn and replaced: the fast path is viable on video, on four specific terms, and all four are compiled away so an adapter remembers none of them. 628 tests on 3.12, 545 on 3.10. Claude-Session: https://claude.ai/code/session_01CnXLihXKG9jfvygHvgKVpX
cw, not arghcw; RULE G's compile half
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
628 tests on 3.12, 545 on 3.10. Both owner questions ratified, the CLI lands on
cw, and RULE G's compile half is built.Ratified 2026-09-02
burnsstays separate and gains alooks-backed ffmpeg backend.looksowns the compile side;burnssamples itsBurnsPathand runs the argv. Dependency pointsburns → looks, legal becauselooksis stdlib-only; the reverse stays forbidden.looksowns normalisation, exactly as already shipped inLook.target.looks.motion— RULE G's compile halfKeyframes in, an ffmpeg fragment out. No easing, no sampling, no camera decisions, and no
burnsimport — a window is anything with.x/.y/.w/.hin burns' normalised convention, so the structural type is the seam.A recorded fleet fact, overturned by measurement
muvid._crop_filter's docstring — propagated by two of this package's own research notes into a warning aimed at theburnsowner — says "Not zoompan: its expression vocabulary has notat all, and it duplicates frames on video input."Both premises are true. The conclusion does not follow:
tin_time/itboth work.ffmpeg -h filter=zoompanlists no variables at all, which is likely how the fact came to be half-completed(1800 out for 20 in), exactly 1:1 atd=1Meanwhile the filter the notes recommended instead cannot do the job:
tis not in scope forcrop'sw/hand the filter refuses to configure — reproduced in three invocation forms. That is stronger than this repo's own earlier note, which recorded it as "evaluated once … produced 60 frames of unchanged size". Nothing renders. A Ken Burns path is a zoom by definition, so it has exactly one compilable form.muvid's own code is fine — it compiles a pan, the case where
cropis right and simpler. Filed as reeleehq/muvid#68; theburnsadapter is reeleehq/burns#12.Three more traps, each a silent wrong answer
Which is why each is a refusal rather than a doc note:
x/yare in original input pixels, not zoomed — the two readings score 60.5 dB and 6.3 dB against acrop+scalereference. The first implementation here had it wrong, and it configured cleanly.fpssilently retimes —fps=25on a 10 fps source keeps all 20 frames and turns a 2.0 s clip into 0.8 s, so a frame-count check passes. Required, never defaulted.zoomis clamped at 10 — z=10 scores 54.4 dB, z=12 scores 13.2: not a worse rendering of the window asked for, a correct rendering of a different one.The tests ask ffmpeg, not the strings
String equality passes for a fragment ffmpeg refuses and for one that renders the wrong picture — both happened here. So each fragment is built a second way by hand and compared with ffmpeg's own
psnr: a static window is bit-identical to the crop it names, a zoom is >45 dB from itscrop+scaleequivalent, and the two filters' clocks agree frame for frame. The threshold isn't a knob — correct pairings score 54–60 dB, nearest wrong ones 6–13.Full transcript:
docs/research/00f_motion_filters_evidence.md.The CLI is
cwMIT,
dependencies = [], and — verified in a fresh interpreter —import cwpulls in nothing beyond stdlib. Solooksneither imports the LGPL package nor hand-rollsargparse.cwcould not readSequence[str](argh recogniseslistand nothing else), solooks check scale eq lut3dbound one token — quietly, since the parser builds and--helplooks right. Fixed upstream in i2mint/cw#36:modern_decodenow reads thecollections.abcinterfaces, withstr/bytesexcluded by construction (matching ontyping.get_origin, which isNonefor both) rather than by special case — they are registeredSequences, so anissubclasstest would have made every string parameter variadic.Two of this package's own guards caught me
test_every_runtime_extra_has_a_ledger_rowrefused the new[cli]extra: "an extra is a conveyance decision, not a convenience list."cwhas a ledger row now, with both observations dated.Reading the declaration on both legs
CI went red on 3.10:
tomllibis 3.11+. The obvious fix — skip — is what the other guard already did, and finding that is what made this a real fix: the guard that refuses an undeclared conveyance had been reporting "skipped" on half of CI since it was written. A skip reads as a pass.looks/tests/_pyproject.pyreads the file on every version. It is deliberately not a TOML parser — a hand-rolled reader is normally a liability, agreeing with the real one until the day it quietly doesn't. What removes that istest_pyproject_reader.py: on every interpreter that hastomllib(one of CI's two legs, every run) the reader is checked against it over the real file, every table and every key, in both directions. Never trusted, only checked — against the same bytes the 3.10 leg reads.importlib.metadatawas the tempting alternative and is wrong: measured, it reportsProvides-Extra: ['dev', 'docs']— nocli— because an editable install's dist-info is a snapshot of install time. A licence guard reading that would have said the extra does not exist.Verified on a real 3.10 interpreter: both guards now run there.
Also
__version__resolves on first access (PEP 562). Eager resolution cost 50 ms of import time —importlib.metadatapulls inemail.parser— for a value most callers never read. 54.5 ms → 38.6 ms, pinned by a test assertingemail.parseris absent afterimport looks.https://claude.ai/code/session_01CnXLihXKG9jfvygHvgKVpX