agentleFS
cshumcshum/vipsgen/AGENTS.md

vipsgen

AGENTS.md269 starsChanged 23 days agoMITView on GitHub

Notes for agents and contributors, short on purpose. The README is for users; this is for anyone changing the generator. - vips/, vips817/ and vips816/ are produced by CI from the matrix in .github/workflows/ci.yml. Do not regenerate them locally: the libvips you have is not the one they were generated from, so a local run produces spurious diffs. Change internal/ or the templates, push, and CI commits the result. - vipsgen -debug writes into the repo, including regenerated packages. If…

  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 31,439 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

The file

We split this AGENTS.md into the rules it gives an agent

3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Commit explicit paths. git add -A sweeps generated trees and scratch programs into the branch.

    line 14
  2. Very specific:

    When a change alters generated output, the last CI job commits it.

    line 36
  3. Distinctive:

    Call the operation twice — once with an explicit zero, once without — and compare outcomes.

    line 48

≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

Working on vipsgen

Notes for agents and contributors, short on purpose. The README is for users; this is for anyone changing the generator.

Regenerating

  • vips/, vips817/ and vips816/ are produced by CI from the matrix in .github/workflows/ci.yml. Do not regenerate them locally: the libvips you have is not the one they were generated from, so a local run produces spurious diffs. Change internal/ or the templates, push, and CI commits the result.
  • vipsgen -debug writes into the repo, including regenerated packages. If you only wanted the JSON dump, restore the tree afterwards.
  • Commit explicit paths. git add -A sweeps generated trees and scratch programs into the branch.

The options rule

  • A field is a pointer when libvips accepts its zero value and that zero differs from the option's default. nil means "not supplied". The classification is derived from GParamSpec, so it tracks the libvips you build against — the three packages are not interchangeable.
  • internal/generator/presence.go holds the rule and one measured exception list. The list cannot be derived: gifload.n and webpload.n share a range that contains zero, and both reject it. Probe with a fixture where the argument can have an effect — n looks accepted on a single-page file and rejects zero once there is a second page.
  • vips/zero_pins_test.go pins the classification; internal/generator/presence_test.go pins the policy, including that the policy reaches OptionalInputs — that slice holds copies, so flagging Arguments alone silently does nothing.

CI

  • Three legs, one per libvips version, each regenerating its own package before it tests.
  • Examples are built only on the vips (8.18) leg, so a compile error an older libvips would hit does not show there — and one the newer one hits shows nowhere else.
  • When a change alters generated output, the last CI job commits it. That job's own run can park at action_required, which is neither a pass nor a failure: the parent run already built the content it committed.
  • The legs run in ghcr.io/cshum/vipsgen-deps, built from docker/deps.Dockerfile: the packages the workflow used to install, plus libultrahdr. Its tags are append-only, so a dependency change bumps the number instead of repointing a tag a running job may be pulling.
  • Same libvips in, same bytes out — regeneration is expected to be byte-identical, and a change that alters the output should be explainable operation by operation.

Measuring a zero exception

The list in internal/generator/presence.go is measured, so re-measure before adding to it. Call the operation twice — once with an explicit zero, once without — and compare outcomes; an error in the first and not the second is attributable to the zero rather than to the input. Two traps:

  • The vips CLI swallows an empty string, so an empty --id proves nothing. Measure strings through the Go API, where vips.Ptr("") reaches libvips.
  • A single-page input can give the wrong verdict for n, which is about pages: a documented rejection only reproduces with a multi-page file.

The debug dump

vipsgen -debug writes debug_operations.json: operations with RequiredInputs, OptionalInputs and their types. Note that its GoName is lowercase (gifload, n) while the generated code is not (GifloadOptions, N), which is easy to get wrong in a script.

The templates also carry hand-written API beside the generated operations - the multi-page helpers, Pages, Loop, the metadata accessors. That is v1 API: change it only for a stated reason.

Tests

  • ./internal/... runs on every leg, but each leg tests only its own output directory (go test ./${{ matrix.output_dir }}/...). So a test under internal/ that imports v2/vips drags the 8.18-generated package into the 8.16 and 8.17 legs and fails on headers the older libvips does not have - which is why vips/zero_pins_test.go sits inside the package rather than beside the generator.
  • The consequence is that those pins run on one leg only, against the libvips the exception list was measured on. Behaviour on the other legs is not pinned.
  • The generated tests count open images, so close what you open.
  • The operation subtests are generated from the same introspection data as the operations: one per operation, one per options variant, one per default constructor. They are regenerated, so add a shape in internal/generator/operation_*.go rather than editing them.
  • A hand-written test in a *_test.go.tmpl compiles against all three trees, so it may only use symbols every version has. An operation that exists in some versions belongs in generation, where the tree without it simply has no subtest: writing NewMagickloadSource by hand broke the 8.16 and 8.17 legs, which have no such symbol.
  • A wrapper calls the libvips function directly, so a library without that operation does not link - the import path has to match. HasOperation covers the other direction, a loaded library older than the one the package was built against, where the missing symbol would be a fatal lookup rather than an error.
  • testdata/ holds inputs libvips reads but cannot write (pdf, svg, mat, analyze, openexr). Tests look for them at ../testdata/ and skip when absent, so a tree generated by the command passes without them: the fixtures ship with this repository, not the generator, and each one has a row in testdata/README.md naming where it came from.
  • A skip names the precondition and is the expected outcome for a tree or a build that lacks the thing. A subtest that asserts nothing on the way through is not a test.
  • An error path is reachable through the public API: a required number at zero or an enum outside its range makes libvips fail the call with parameter not set, which is what the <op>_error subtests use. Out-of-range option values are accepted, so they do not.
  • A dependency CI is missing is a dependency to install, not a fixture to invent: ubuntu-24.04 splits libheif into plugin packages, and a shipped HEIC there failed with Unsupported codec instead of skipping.
  • go test ./vips/ -cover measures the package, and most of what is missing is not effort: the operations left out need a real document as their fixture - slide decks, camera raw - so the ceiling without test data is far below 100%.
  • go test never sees the C, and the flags cannot ride on CGO_CFLAGS: -Werror there fails on cgo's own prolog and on runtime/cgo, which is not warning-clean. So CI compiles the generated .c files directly. That is where the wrappers broke before.

Docs

  • README and MIGRATION use plain words and state behaviour, not history. Generated field comments carry the range and the default, which is where a reader looks first.
  • Numbers in docs come from the artefact — the classification table, the probe output — not from arithmetic in prose, and not from a previous draft.

Releasing

Tags are manual and they are the release: the module path carries the major version, so v2.0.0 goes on this line and v1 keeps working at its existing paths.

Elsewhere

README for users, MIGRATION for v1 to v2, internal/introspection/README.md for the checklist to run when bumping libvips.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.