showcase · tier 1 · content
Image FX batch
glob lists the folder, for_each styles every photo through the same deterministic ops chain: no model, pure pixels.
- 3tasksthe plan falls out of the bindings
- 3wavesparallel by construction
- 1verbs exercisedinvoke
- T1the tiercontent
The whole file
3 tasks · 3 wavesThe plan falls out of the bindings, nothing is scheduled by hand.
image-fx-batch.nika.yamlsource
nika: v1workflow: id: image-fx-batch description: "Folder of PNG files → deterministic art (grayscale · dither · pixelate) · offline batch"const: # Two sample photos ship with the repo. Point this at your own folder and # move `permits.fs.read` with it — a const is baked in, never caller-supplied, # so the two lines stay in step by editing, not by magic. photos: "examples/fixtures/photos"permits: exec: false tools: ["nika:glob", "nika:image_fx", "nika:jq"] fs: # ONE entry that has to cover TWO different reads, and this is the trap: # · `nika:glob` gates its WALK ROOT — the directory itself # · `nika:image_fx` gates each MATCHED FILE underneath it # Measured against 0.106.0: granting only `…/photos` passes `check` with # zero hints and then dies at run — NIKA-SEC-004 on the first photo. # Granting only `…/photos/*.png` is worse: `check` calls the entry drift # (it matches no literal the body names) and the glob is refused at run. # `**` covers the directory AND every descendant, so one entry is honest # here; `["…/photos", "…/photos/*.png"]` is the two-entry equivalent. read: ["examples/fixtures/photos/**"] # The renders land FLAT in one directory, so `*` is enough: it matches a # single path segment and never crosses `/`, which means a photo whose # name contained a slash could not steer a write into a subtree. write: ["out/darkroom/*"]tasks: # The folder is the work list — no hand-maintained array to drift. shots: invoke: tool: "nika:glob" args: { pattern: "${{ const.photos }}/*.png" } # Derive {src, out} pairs · jq does the string work, natively. jobs: with: shots: ${{ tasks.shots.output }} invoke: tool: "nika:jq" args: input: "${{ with.shots }}" expression: 'map({ src: ., out: ("out/darkroom/" + (split("/") | last)) })' # One deterministic darkroom pass per photo · the ops run IN ORDER. stylize: with: jobs: ${{ tasks.jobs.output }} for_each: ${{ with.jobs }} max_parallel: 4 invoke: tool: "nika:image_fx" args: input: "${{ item.src }}" out: "${{ item.out }}" seed: 0 # explicit · the render is a function of (bytes · ops · seed) ops: - grayscale: {} - dither: { mode: bayer4 } - pixelate: { block: 4 }outputs: rendered: # an empty folder skips the fan-out (its output is null) — the guard keeps # the typed contract honest on the very first exploratory run value: "${{ tasks.stylize.output != null ? tasks.stylize.output : [] }}" description: "The stylized artifact receipts · one per input photo · path + sha256"nika inspect · engine 0.106.1 · vendored graph, never re-derived
2 declared edges · 3 waves · the plan falls out of the bindings