Skip to content

Stop leaking host paths into calibrated pictures, and let provenance through (#241) - #260

Merged
adulbrich merged 18 commits into
mainfrom
pipeline-header-provenance
Aug 6, 2026
Merged

Stop leaking host paths into calibrated pictures, and let provenance through (#241)#260
adulbrich merged 18 commits into
mainfrom
pipeline-header-provenance

Conversation

@adulbrich

Copy link
Copy Markdown
Collaborator

Closes #241.

Every Radiance tool appends its own command line to the header of the picture it writes. The pipeline named files by whatever string the host used to find them, and on the desktop that is an absolute path from the native file dialog, so it ended up inside the output. The reported case:

pcomb -h -f "/Users/<user>/Library/CloudStorage/GoogleDrive-<user>@oregonstate.edu/.../CF_f5d6.cal" /work/neutral_density.hdr

A university email address, in every calibrated picture, in files that go into papers as supplementary material.

What changed

Sources are staged under sanitised names. /src/<n>-<basename> for frames, /cal/<slot>-<basename> for calibration files. The basename carries the meaning (CF_f5d6.cal names the aperture); the directory is the part that leaks. Done at the single staging boundary in executeInWorker, so prepareInputs, the filter stage and the release bookkeeping are untouched: they treat a path as an opaque key into the virtual filesystem. The caller's params are copied rather than mutated, so run history and the form still show the files you picked.

Deliberately outside /work, which collectOutputs scans after every tool to gather what that tool produced.

pcomb -h is gone from the fourth correction. It was the only one of four that passed it, so that stage discarded everything the three before it accumulated: the camera, the merged frame list, the real capture date, PRIMARIES, EXPOSURE, and the crop and resize lines. A calibrated picture recorded less than an uncalibrated one. The flag traces to extra/ldr-to-hdr.sh, transcribed into Rust in 9825c4b without a comment and ported across for parity with a file that no longer exists; Table 3 step 9 of Pierson et al. (2019) does not call for it. With the paths sanitised it was holding nothing back, so the special case and its suppressHeader flag are deleted and all four corrections use one builder.

Calibration warnings name the file, not its staging path. Run transcripts are stored with the run, so that was the last place a full host path was written down.

Before and after

Before, a real calibrated output from this app:

#?RADIANCE
CAPDATE= 2026:07:28 11:41:46
/usr/local/radiance/bin/pcomb -h -f "/Users/<user>/Library/Application Support/.../calibration.cal" /Users/<user>/Documents/.../neutral_density.hdr
VIEW= -vta -vv 180 -vh 180
COMPUTED_VERTICAL_ILLUMINANCE=900.935880

After, the same stage sequence through the shipped wasm binaries:

#?RADIANCE
/work/neutral_density.hdr:
	...
			/work/resize.hdr:
				/work/nullify_exposure_value.hdr:
					CAMERA= Canon Canon EOS 5D Mark II
					created HDR image from '18-IMG_6972.JPG' ... '1-IMG_6955.JPG'
					Removed lens flare
					CAPDATE= 2017:07:13 15:45:30
					PRIMARIES= 0.6400 0.3300 ...
				EXPOSURE=1.0000e+00
			-f /cal/fisheye-fisheye_corr.cal /work/resize.hdr
		-f /cal/vignetting-vignetting_f8.cal ...
	-f /cal/neutral-NDfilter_no_transform.cal ...
-f /cal/photometric-CF_f8.cal /work/neutral_density.hdr
VIEW= -vta -vv 180 -vh 180

A correction, recorded rather than buried

The design originally claimed hdrgen also leaks host paths through its provenance line, making the merge a second leak surface. That is wrong, and was checked only after the code was written: hdrgen strips directories and writes basenames. The leak is exactly the pcomb -f cal path #241 reported. Re-pathing the merge inputs is kept because it costs nothing and makes headers identical across machines, not because it fixes anything. The design doc carries this correction in place.

Verification

pipeline.spec.ts gains two assertions on both finished pictures: every absolute path in the header must sit under /src, /cal or /work, and no Windows path may appear. The picture must carry exactly one active VIEW= line, which is the question removing -h raised: hdrgen writes its own from EXIF, and each copying stage indents the inherited header one tab deeper, so the stale one ends up deactivated. The false-colour map is exempt from the view check because falsecolor composes it with pcompos -h and it inherits no header at all.

Both were validated against real Radiance output before being committed. A header from the full stage sequence with an unsanitised cal path trips the path check four times, once per pcomb stage, and carries exactly one active VIEW=; a real 18-frame merge from the shipped wasm hdrgen with sources under /src trips neither.

Please treat the Chromium job as the merge gate. These assertions have never executed: the browser e2e does not complete on the machine this was developed on, for reasons unrelated to this change. Local gates are green (424 tests, tsc clean at the root and in e2e-web, lint clean).

Not in scope, filed separately

The false-colour map carries no provenance at all, for the same pcompos -h reason. And Table 3 step 9 is pcomb -s factor with a numeric calibration factor where the app requires a hand-written .cal.

Note

c70ad65 adds the upstream repositories for Radiance and panlib to the README's source column. Unrelated to #241 and happy to split it out if you would rather it went separately.

🤖 Generated with Claude Code

adulbrich and others added 15 commits August 5, 2026 22:32
#241 reports an absolute path, and a university email address inside it, in
every calibrated picture. Reading the code, the leak is wider than the pcomb
line it was found on: the LDR branch of prepareInputs hands hdrgen host paths
too, and only the RAW branch escapes because conversion forced it to invent
new names.

The design gives every file the pipeline names a sanitized work path that
keeps its basename, done once at the staging boundary so the orchestrator,
the filter stage and the release bookkeeping are untouched. With no host path
left in any argv, -h stops being load-bearing and comes off the fourth
correction stage, which is the consistency follow-up the tutorial-conformance
spec parked in its §9.

Also records where -h came from, since that decided whether removing it is a
change or a correction: not the tutorial, whose Table 3 step 9 is pcomb -s
factor, but extra/ldr-to-hdr.sh, transcribed into Rust in 9825c4b with no
comment and ported to TypeScript byte for byte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five tasks: a pure naming layer, the staging boundary that applies it,
collapsing the photometric stage into pcombCalArgs now that -h holds nothing
back, calibration warnings that name the file rather than its staging path,
and a manual header check, which is the only step that can prove any of it,
since nothing in the suite asserts on header content.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ctions

Task 4 edits the text of a warning inside warnIfResolutionDependent, which the
constraint as written forbade outright.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Radiance tools write their own argv into the header of the picture they
produce, so a path handed to a tool is a path published in the output. The
naming layer keeps the basename, which carries the meaning, and drops the
directory, which is what leaks. Not wired up yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
collectOutputs scans /work after every tool and files whatever it finds as
something that tool produced. Sources staged under /work/src and /work/cal
would appear there as directory entries and be collected as zero-byte outputs,
and WORK_DIR's own comment says the prefix is for intermediates. The runner
already expects sources to live elsewhere, and makeParentDirs creates whatever
depth they need, so /src and /cal cost nothing and keep the invariant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hdrgen and LibRaw rows already named theirs; these two pointed only at our
fork, so a reader could not tell where the code came from. Both confirmed
against the GitHub API rather than assumed: radiantlab/Radiance forks
LBNL-ETA/Radiance, radiantlab/panlib forks radiance-org/panlib.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… for outputs

collectOutputs scans /work after every tool and files whatever it finds as
something that tool produced. Sources staged under /work/src and /work/cal
would appear there as directory entries and be collected as zero-byte
outputs. The runner already expects sources to live elsewhere, and
makeParentDirs creates whatever depth they need, so /src and /cal cost
nothing and keep the invariant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The desktop staged every file under the absolute path the native dialog
returned, so hdrgen and pcomb were handed host paths and wrote them into the
headers of both output pictures. The browser never had the problem, because
vfs.ts already hands out synthetic paths; this gives the desktop the same
shape.

Done at the staging boundary, so prepareInputs, the filter stage and the
release bookkeeping are untouched: they treat a path as an opaque key into the
virtual filesystem, which is what makes this containable.

Closes #241 for the leak; the header flag follows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The photometric adjustment was the only one of the four corrections to pass
pcomb -h, so it threw away everything the three before it had accumulated: the
camera, the frames hdrgen merged, the capture date, and the crop and resize
lines. A picture processed with calibration files recorded less than one
processed without, which is backwards for outputs that go into papers.

The flag traces to extra/ldr-to-hdr.sh rather than to the tutorial, whose
Table 3 step 9 is pcomb -s factor. With the paths sanitized it was no longer
holding anything back, so the stage collapses into pcombCalArgs and the
special case goes.

Closes #241.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… path

Staged files are named /cal/<slot>-<file> now, which means nothing to a
user reading a status log. The run transcript is stored with the run, so this
is also the last place a full host path was being written down.

The runner's own read-failure text also embedded the staged path (e.g. "no
such file /cal/<name>"), so that detail is scrubbed too -- otherwise the
warning would still spell out the path one sentence after naming the file.

Also adds a regression test pinning all four corrections (fisheye,
vignetting, neutral density, photometric) to the same pcomb argument shape
with no -h flag, closing a coverage gap left by #241/#254: nothing previously
exercised all four cal files in one run to confirm the fourth correction's -h
flag stays gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 1 fix for task 4: the previous scrub (`String(error).replaceAll(calPath,
name)`) only worked because both ToolRunner implementations happen to embed
calPath as an exact substring, and they already disagree on phrasing ("no
such file " vs "no such file: "). A future runner that quotes, prefixes, or
normalizes the path would make the scrub a silent no-op, and no orchestrator
test exercises WasmRunner to catch it.

Instead of scrubbing, the read-failure warning now drops the underlying
error's text entirely and is built only from the label and the staged file's
basename -- values this function already controls. Nothing actionable is
lost: the correction stage that follows will fail on its own and surface the
real error if the file is genuinely unreadable.

Strengthened "names the file when it cannot be read either" to assert the
full staged path never appears in the message, not just its "/cal/" prefix,
so the invariant is pinned rather than today's wording.

Also renamed calWarning's second parameter from `path` to `name`, since every
caller now passes a basename. Naming only; signature and behaviour unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing in the suite looked inside a header, which is where #241 lived: every
Radiance tool appends its own command line to the picture it writes, so a path
handed to a tool is a path published in the output. The existing JPEG test
already downloads both pictures and reads their bytes, so the check costs no
extra run time.

Two assertions. Every absolute path in the header must sit under /src, /cal or
/work, and a Windows path is checked separately because it does not start with
a slash. And there must be exactly one active VIEW= line, which is the question
removing `pcomb -h` raised: hdrgen writes its own VIEW= from EXIF, and the
worry was that it would compete with the one the pipeline writes.

Both were validated against real Radiance output before being committed. A
header from the full stage sequence with an unsanitised cal path trips the
first four times, once per pcomb stage, and carries exactly one active VIEW=;
a header from the shipped wasm hdrgen with sources staged under /src trips
neither.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The design asserted that hdrgen writes host paths into its provenance line, so
the LDR merge was a second leak surface. Checked rather than reasoned about,
after the fact: hdrgen strips the directory and writes basenames only, and the
response function is not named in the header at all. The leak is exactly the
pcomb -f cal path #241 reported. Re-pathing the merge inputs is kept for
determinism across machines, not because it fixes anything.

Also replaces the planned manual inspection with the automated assertions in
pipeline.spec.ts, and records what they were validated against: a header from
the full stage sequence with an unsanitised cal path trips the path check four
times and carries exactly one active VIEW=, which settles the question that
removing -h raised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The new header guard asserted exactly one active VIEW= in both downloads, but
falsecolor composes its output with pcompos -h, so that file inherits no header
and carries none. The assertion would have failed on every CI run. This branch's
own plan already records the false-colour map as header-stripped, which is what
makes the mistake plain: the guard was written as though a run produced one
file.

Scoped to the picture. Both path assertions still cover both files, and the
map's missing provenance is left unpinned rather than fixed in place, since it
is filed separately.

Also sweeps four comments and a loop variable still saying "work path" for
things now staged under /src and /cal, and corrects the design's claim that
error paths are mapped back to the original: they are not, and the basename
surviving is the whole mitigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three phrases in the design itself still described staged files as living on a
work path, in the same document the fix wave edited. Raised as an out-of-scope
observation by the re-review; prose only, no behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lumilab Ready Ready Preview Aug 6, 2026 12:28pm

adulbrich and others added 3 commits August 6, 2026 14:25
…stead

Removing -h broke the pipeline outright, which CI caught and no local test
could. Radiance tools indent an inherited header with tabs, and evalglare
refuses any picture whose header has EXPOSURE= and a tab on the same line:

    pictool.c:214  if (strstr(s, EXPOSSTR) && strstr(s, "\t")) { ... exit(1) }

pcompos writes an EXPOSURE= line during the crop. With -h on the fourth
correction that line stays at column zero and evalglare is content; without it
every correction nests it one tab deeper and the glare stage exits producing
nothing. Measured against the shipped wasm binary, not inferred, which is also
why the uncalibrated path kept working and the calibrated one did not. The flag
was load-bearing for EXPOSURE, not for the stale VIEW line everyone tested.

So provenance cannot be inherited. It is re-stated instead, after evalglare has
run, through the same getinfo -a the pipeline already uses for VIEW and the
illuminance values: camera, capture date, the merged frame list, the lens-flare
note, and the calibration basenames. Entries are built as NAME= value rather
than passed through verbatim, because hdrgen's own frame line begins with an
argv[0] that WebAssembly reports as ./this.program. Nothing emitted may carry a
tab or an exposure entry, since someone will run evalglare on the output.

CAPDATE is carried as hdrgen resolved it. It names one frame's EXIF timestamp,
the first hdrgen lists, and header.c:44 makes it a standard identifier parsed
as UTC, so it has to stay a single well-formed time rather than become a range.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
setTextInputValue assigned element.value directly. React replaces the value
property with its own accessor and keeps a tracker of the last value it wrote;
on an input event it compares the two and drops the event when they match. A
direct assignment updates the DOM and the tracker together, so React concludes
nothing changed and the form never hears.

The failure is silent and convincing: the field displays the path and any
assertion on getValue() passes. All five fields were set this way, so
"generates an HDR image" has been running an unconfigured pipeline with no
calibration, no response function and a default lens mask. Reproduced against
the real app before changing anything: the DOM showed the .cal path, the run
completed, and not one correction stage executed.

That is why this suite stayed green through a change that broke calibration on
every platform. Fixing the setter is half of it; the other half is that the
test now reads the finished picture's header, which is the only evidence here
of what actually ran. It asserts the run recorded the calibration it was given,
and that no host path reached the output -- the desktop is the host #241's leak
came from, so that assertion belongs here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The premise was that -h could be removed. It could not: evalglare exits rather
than read a header with EXPOSURE= and a tab on one line, and every correction
without -h nests that line one tab deeper. The correction sits above the
original reasoning rather than replacing it, because the reasoning is why the
mistake was made and worth keeping legible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@adulbrich adulbrich mentioned this pull request Aug 6, 2026
@adulbrich
adulbrich merged commit 953a0e4 into main Aug 6, 2026
12 checks passed
@adulbrich
adulbrich deleted the pipeline-header-provenance branch August 6, 2026 18:54
adulbrich added a commit that referenced this pull request Aug 7, 2026
…through (#241) (#260)

* docs: design for sanitizing pipeline paths and restoring provenance

#241 reports an absolute path, and a university email address inside it, in
every calibrated picture. Reading the code, the leak is wider than the pcomb
line it was found on: the LDR branch of prepareInputs hands hdrgen host paths
too, and only the RAW branch escapes because conversion forced it to invent
new names.

The design gives every file the pipeline names a sanitized work path that
keeps its basename, done once at the staging boundary so the orchestrator,
the filter stage and the release bookkeeping are untouched. With no host path
left in any argv, -h stops being load-bearing and comes off the fourth
correction stage, which is the consistency follow-up the tutorial-conformance
spec parked in its §9.

Also records where -h came from, since that decided whether removing it is a
change or a correction: not the tutorial, whose Table 3 step 9 is pcomb -s
factor, but extra/ldr-to-hdr.sh, transcribed into Rust in ed8ac04 with no
comment and ported to TypeScript byte for byte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: implementation plan for the pipeline header provenance fix

Five tasks: a pure naming layer, the staging boundary that applies it,
collapsing the photometric stage into pcombCalArgs now that -h holds nothing
back, calibration warnings that name the file rather than its staging path,
and a manual header check, which is the only step that can prove any of it,
since nothing in the suite asserts on header content.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: bind the opaque-path constraint to path handling, not whole functions

Task 4 edits the text of a warning inside warnIfResolutionDependent, which the
constraint as written forbade outright.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(pipeline): name staged files without their directories

Radiance tools write their own argv into the header of the picture they
produce, so a path handed to a tool is a path published in the output. The
naming layer keeps the basename, which carries the meaning, and drops the
directory, which is what leaks. Not wired up yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: stage sources outside /work, which the runner reserves for outputs

collectOutputs scans /work after every tool and files whatever it finds as
something that tool produced. Sources staged under /work/src and /work/cal
would appear there as directory entries and be collected as zero-byte outputs,
and WORK_DIR's own comment says the prefix is for intermediates. The runner
already expects sources to live elsewhere, and makeParentDirs creates whatever
depth they need, so /src and /cal cost nothing and keep the invariant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: name the upstream for Radiance and panlib in the source column

The hdrgen and LibRaw rows already named theirs; these two pointed only at our
fork, so a reader could not tell where the code came from. Both confirmed
against the GitHub API rather than assumed: radiantlab/Radiance forks
LBNL-ETA/Radiance, radiantlab/panlib forks radiance-org/panlib.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(pipeline): stage sources outside /work, which the runner reserves for outputs

collectOutputs scans /work after every tool and files whatever it finds as
something that tool produced. Sources staged under /work/src and /work/cal
would appear there as directory entries and be collected as zero-byte
outputs. The runner already expects sources to live elsewhere, and
makeParentDirs creates whatever depth they need, so /src and /cal cost
nothing and keep the invariant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(pipeline): stage source files under names that carry no directory

The desktop staged every file under the absolute path the native dialog
returned, so hdrgen and pcomb were handed host paths and wrote them into the
headers of both output pictures. The browser never had the problem, because
vfs.ts already hands out synthetic paths; this gives the desktop the same
shape.

Done at the staging boundary, so prepareInputs, the filter stage and the
release bookkeeping are untouched: they treat a path as an opaque key into the
virtual filesystem, which is what makes this containable.

Closes #241 for the leak; the header flag follows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(pipeline): let the calibrated picture keep its provenance

The photometric adjustment was the only one of the four corrections to pass
pcomb -h, so it threw away everything the three before it had accumulated: the
camera, the frames hdrgen merged, the capture date, and the crop and resize
lines. A picture processed with calibration files recorded less than one
processed without, which is backwards for outputs that go into papers.

The flag traces to extra/ldr-to-hdr.sh rather than to the tutorial, whose
Table 3 step 9 is pcomb -s factor. With the paths sanitized it was no longer
holding anything back, so the stage collapses into pcombCalArgs and the
special case goes.

Closes #241.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(pipeline): name the calibration file in warnings, not its staging path

Staged files are named /cal/<slot>-<file> now, which means nothing to a
user reading a status log. The run transcript is stored with the run, so this
is also the last place a full host path was being written down.

The runner's own read-failure text also embedded the staged path (e.g. "no
such file /cal/<name>"), so that detail is scrubbed too -- otherwise the
warning would still spell out the path one sentence after naming the file.

Also adds a regression test pinning all four corrections (fisheye,
vignetting, neutral density, photometric) to the same pcomb argument shape
with no -h flag, closing a coverage gap left by #241/#254: nothing previously
exercised all four cal files in one run to confirm the fourth correction's -h
flag stays gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(pipeline): stop the read-failure warning depending on error wording

Round 1 fix for task 4: the previous scrub (`String(error).replaceAll(calPath,
name)`) only worked because both ToolRunner implementations happen to embed
calPath as an exact substring, and they already disagree on phrasing ("no
such file " vs "no such file: "). A future runner that quotes, prefixes, or
normalizes the path would make the scrub a silent no-op, and no orchestrator
test exercises WasmRunner to catch it.

Instead of scrubbing, the read-failure warning now drops the underlying
error's text entirely and is built only from the label and the staged file's
basename -- values this function already controls. Nothing actionable is
lost: the correction stage that follows will fail on its own and surface the
real error if the file is genuinely unreadable.

Strengthened "names the file when it cannot be read either" to assert the
full staged path never appears in the message, not just its "/cal/" prefix,
so the invariant is pinned rather than today's wording.

Also renamed calWarning's second parameter from `path` to `name`, since every
caller now passes a basename. Naming only; signature and behaviour unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(web): assert the finished pictures name no host path

Nothing in the suite looked inside a header, which is where #241 lived: every
Radiance tool appends its own command line to the picture it writes, so a path
handed to a tool is a path published in the output. The existing JPEG test
already downloads both pictures and reads their bytes, so the check costs no
extra run time.

Two assertions. Every absolute path in the header must sit under /src, /cal or
/work, and a Windows path is checked separately because it does not start with
a slash. And there must be exactly one active VIEW= line, which is the question
removing `pcomb -h` raised: hdrgen writes its own VIEW= from EXIF, and the
worry was that it would compete with the one the pipeline writes.

Both were validated against real Radiance output before being committed. A
header from the full stage sequence with an unsanitised cal path trips the
first four times, once per pcomb stage, and carries exactly one active VIEW=;
a header from the shipped wasm hdrgen with sources staged under /src trips
neither.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: record the header verification, and correct a claim that was wrong

The design asserted that hdrgen writes host paths into its provenance line, so
the LDR merge was a second leak surface. Checked rather than reasoned about,
after the fact: hdrgen strips the directory and writes basenames only, and the
response function is not named in the header at all. The leak is exactly the
pcomb -f cal path #241 reported. Re-pathing the merge inputs is kept for
determinism across machines, not because it fixes anything.

Also replaces the planned manual inspection with the automated assertions in
pipeline.spec.ts, and records what they were validated against: a header from
the full stage sequence with an unsanitised cal path trips the path check four
times and carries exactly one active VIEW=, which settles the question that
removing -h raised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(test): do not demand a VIEW= line from the false-colour map

The new header guard asserted exactly one active VIEW= in both downloads, but
falsecolor composes its output with pcompos -h, so that file inherits no header
and carries none. The assertion would have failed on every CI run. This branch's
own plan already records the false-colour map as header-stripped, which is what
makes the mistake plain: the guard was written as though a run produced one
file.

Scoped to the picture. Both path assertions still cover both files, and the
map's missing provenance is left unpinned rather than fixed in place, since it
is filed separately.

Also sweeps four comments and a loop variable still saying "work path" for
things now staged under /src and /cal, and corrects the design's claim that
error paths are mapped back to the original: they are not, and the basename
surviving is the whole mitigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: finish the work-path terminology sweep in the design

Three phrases in the design itself still described staged files as living on a
work path, in the same document the fix wave edited. Raised as an out-of-scope
observation by the re-review; prose only, no behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(pipeline): restore pcomb -h, and write provenance deliberately instead

Removing -h broke the pipeline outright, which CI caught and no local test
could. Radiance tools indent an inherited header with tabs, and evalglare
refuses any picture whose header has EXPOSURE= and a tab on the same line:

    pictool.c:214  if (strstr(s, EXPOSSTR) && strstr(s, "\t")) { ... exit(1) }

pcompos writes an EXPOSURE= line during the crop. With -h on the fourth
correction that line stays at column zero and evalglare is content; without it
every correction nests it one tab deeper and the glare stage exits producing
nothing. Measured against the shipped wasm binary, not inferred, which is also
why the uncalibrated path kept working and the calibrated one did not. The flag
was load-bearing for EXPOSURE, not for the stale VIEW line everyone tested.

So provenance cannot be inherited. It is re-stated instead, after evalglare has
run, through the same getinfo -a the pipeline already uses for VIEW and the
illuminance values: camera, capture date, the merged frame list, the lens-flare
note, and the calibration basenames. Entries are built as NAME= value rather
than passed through verbatim, because hdrgen's own frame line begins with an
argv[0] that WebAssembly reports as ./this.program. Nothing emitted may carry a
tab or an exposure entry, since someone will run evalglare on the output.

CAPDATE is carried as hdrgen resolved it. It names one frame's EXIF timestamp,
the first hdrgen lists, and header.c:44 makes it a standard identifier parsed
as UTC, so it has to stay a single well-formed time rather than become a range.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(desktop): make the suite actually fill the form, and prove it did

setTextInputValue assigned element.value directly. React replaces the value
property with its own accessor and keeps a tracker of the last value it wrote;
on an input event it compares the two and drops the event when they match. A
direct assignment updates the DOM and the tracker together, so React concludes
nothing changed and the form never hears.

The failure is silent and convincing: the field displays the path and any
assertion on getValue() passes. All five fields were set this way, so
"generates an HDR image" has been running an unconfigured pipeline with no
calibration, no response function and a default lens mask. Reproduced against
the real app before changing anything: the DOM showed the .cal path, the run
completed, and not one correction stage executed.

That is why this suite stayed green through a change that broke calibration on
every platform. Fixing the setter is half of it; the other half is that the
test now reads the finished picture's header, which is the only evidence here
of what actually ran. It asserts the run recorded the calibration it was given,
and that no host path reached the output -- the desktop is the host #241's leak
came from, so that assertion belongs here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: record that -h is load-bearing, against what this design assumed

The premise was that -h could be removed. It could not: evalglare exits rather
than read a header with EXPOSURE= and a tab on one line, and every correction
without -h nests that line one tab deeper. The correction sits above the
original reasoning rather than replacing it, because the reasoning is why the
mistake was made and worth keeping legible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Calibrated pictures lose their provenance header, and leak absolute paths

1 participant