Bases on next. (The original description said this targeted PR #169's branch and had to be merged after it; #169 has since landed as f7f141a and this branch was rebased onto next carrying only its own guard commit.)
The gap
Deleting the __BUILD_DEBUG__ define from build.js leaves make check green while every emitted bundle ships with the debug branch live. tests/ loads src/shared/constants.js outside a bundle and takes the jest fallback, so the property being protected exists only in the emitted output and has to be asserted there.
The check
build.js passes metafile: true and writes dist/constants-bundles.txt: every emitted JS output whose input set includes src/shared/constants.js. Nothing hardcoded, and the manifest is cleared at the start of each build so a stale one can never be verified against.
src/shared/constants.js exports BUILD_DEBUG_MARKER, derived from DEBUG itself, which the bundler constant-folds to autistmask-build-debug=off or =on. If DEBUG is not known at build time the ternary survives and BOTH literals appear — which is the primary failure signal, not a gap.
script/verify-build cross-checks the two independent facts. Missing manifest, empty manifest, missing file, both markers, neither marker, wrong marker, a marker-carrying bundle absent from the manifest, and zero bundles inspected are each a hard failure. There is no path to exit 0 without positively identifying the expected marker.
Wired into make build and make build-debug (the latter asserting the inverse), so script/cibuild covers it. Deliberately not in make check, which would otherwise depend on dist/ existing.
Verification
make check, make build, make build-debug and script/cibuild green on the consolidated tree, with the four bundles verified off. Falsification re-run after the rebase: with the define deleted, make build exits 2 with "carries both debug markers" while make check still exits 0 — the guard is the only thing that catches it.
Diagnostic wording that overstates the consequence is tracked separately in #180.
Closes #170.
Bases on `next`. (The original description said this targeted PR #169's branch and had to be merged after it; #169 has since landed as `f7f141a` and this branch was rebased onto `next` carrying only its own guard commit.)
## The gap
Deleting the `__BUILD_DEBUG__` define from `build.js` leaves `make check` green while every emitted bundle ships with the debug branch live. `tests/` loads `src/shared/constants.js` outside a bundle and takes the jest fallback, so the property being protected exists only in the emitted output and has to be asserted there.
## The check
- `build.js` passes `metafile: true` and writes `dist/constants-bundles.txt`: every emitted JS output whose input set includes `src/shared/constants.js`. Nothing hardcoded, and the manifest is cleared at the start of each build so a stale one can never be verified against.
- `src/shared/constants.js` exports `BUILD_DEBUG_MARKER`, derived from `DEBUG` itself, which the bundler constant-folds to `autistmask-build-debug=off` or `=on`. If `DEBUG` is not known at build time the ternary survives and BOTH literals appear — which is the primary failure signal, not a gap.
- `script/verify-build` cross-checks the two independent facts. Missing manifest, empty manifest, missing file, both markers, neither marker, wrong marker, a marker-carrying bundle absent from the manifest, and zero bundles inspected are each a hard failure. There is no path to exit 0 without positively identifying the expected marker.
- Wired into `make build` and `make build-debug` (the latter asserting the inverse), so `script/cibuild` covers it. Deliberately not in `make check`, which would otherwise depend on `dist/` existing.
## Verification
`make check`, `make build`, `make build-debug` and `script/cibuild` green on the consolidated tree, with the four bundles verified `off`. Falsification re-run after the rebase: with the define deleted, `make build` exits 2 with "carries both debug markers" while `make check` still exits 0 — the guard is the only thing that catches it.
Diagnostic wording that overstates the consequence is tracked separately in https://git.eeqj.de/sneak/AutistMask/issues/180.
Base is PR #169's branch, not main. Deliberate, and the one place I went
against the issue's notes. main has no __BUILD_DEBUG__ define, so a guard
branched there could not be shown to trigger — there would be no define to
delete. Merge after #169; retarget to main then. Full reasoning at the top
of the PR body.
What to look at
The interesting decision is that the check reads two facts from two
independent sources, rather than inferring one from the other:
which bundles to inspect comes from esbuild's metafile, written by build.js to dist/constants-bundles.txt;
each bundle's DEBUG state comes from BUILD_DEBUG_MARKER, a new
constant in src/shared/constants.js derived from DEBUG and folded by the
bundler to one of two string literals.
Collapsing these into one signal is the silent-pass failure: a marker-less
bundle would then be indistinguishable from content/index.js, which
legitimately has no marker, and the check reads green forever. Worth
attacking that reasoning if you think it is wrong.
The marker replaces the DEBUG:!1 substring match the issue mentioned,
because a string literal is stable across esbuild versions and !1 is not.
Its useful property is that when DEBUG is not resolvable at build time the
fold does not happen and both literals land in the bundle — which is
exactly the shape of the regression being guarded, so the ambiguity is the
detector rather than a hole in it.
Where it runs
Build path only. make build and make build-debug both invoke script/verify-build, and Dockerfile:17 runs a bare make build, so CI
covers it. Not in script/check, on purpose: that would make check
depend on dist/ existing and pull a build into its time budget, and the
"skip if dist/ is absent" workaround is precisely the silently-green
behaviour the issue forbids. Per the steer, the build path wins and I am
saying so out loud.
Verification
make check green: 5 suites, 57 tests, prettier clean.
make build green, 4 bundles verified off; make clean && make build green
from an empty dist/.
make build-debug and AUTISTMASK_DEBUG=1 make build green, 4 bundles
verified on.
script/cibuild (real docker build ., so make check then make build)
green end to end.
Both required failure modes, actually triggered
Full transcripts are in the PR body. In short:
(a) define deleted — make check exits 0 on that same tree, and make build exits 2 with carries both debug markers, so the build-time DEBUG value was never resolved and the debug branch is still live. Define
restored before committing; it is at build.js:95.
(b) unrecognizable bundle — a bundle overwritten with content carrying
neither marker fails with carries no debug marker, so its DEBUG state cannot be determined [...] Refusing to report success.
I also triggered four further guards: debug artifacts against a release
verification, empty manifest, missing manifest, and a manifest gone
stale/short (caught by the cross-check that no unlisted bundle may carry a
marker — this is what stops the check trusting whatever the manifest happens
to say).
Things a reviewer might reasonably push on
build.js grew a local bundle() helper, which is why the four esbuild
call sites collapse in the diff. Bundler options are unchanged apart from metafile: true. The motive is that a future entry point cannot silently
skip metafile collection.
The two new jest tests pin source-level invariants only. They cannot see a
compiled bundle and are not offered as a substitute for the artifact check.
The marker adds a short string to every shipped bundle that contains constants.js.
## Summary for review
**Base is PR #169's branch, not `main`.** Deliberate, and the one place I went
against the issue's notes. `main` has no `__BUILD_DEBUG__` define, so a guard
branched there could not be shown to trigger — there would be no define to
delete. Merge after #169; retarget to `main` then. Full reasoning at the top
of the PR body.
### What to look at
The interesting decision is that the check reads **two facts from two
independent sources**, rather than inferring one from the other:
- **which bundles to inspect** comes from esbuild's metafile, written by
`build.js` to `dist/constants-bundles.txt`;
- **each bundle's DEBUG state** comes from `BUILD_DEBUG_MARKER`, a new
constant in `src/shared/constants.js` derived from `DEBUG` and folded by the
bundler to one of two string literals.
Collapsing these into one signal is the silent-pass failure: a marker-less
bundle would then be indistinguishable from `content/index.js`, which
legitimately has no marker, and the check reads green forever. Worth
attacking that reasoning if you think it is wrong.
The marker replaces the `DEBUG:!1` substring match the issue mentioned,
because a string literal is stable across esbuild versions and `!1` is not.
Its useful property is that when `DEBUG` is *not* resolvable at build time the
fold does not happen and **both** literals land in the bundle — which is
exactly the shape of the regression being guarded, so the ambiguity is the
detector rather than a hole in it.
### Where it runs
Build path only. `make build` and `make build-debug` both invoke
`script/verify-build`, and `Dockerfile:17` runs a bare `make build`, so CI
covers it. **Not** in `script/check`, on purpose: that would make `check`
depend on `dist/` existing and pull a build into its time budget, and the
"skip if `dist/` is absent" workaround is precisely the silently-green
behaviour the issue forbids. Per the steer, the build path wins and I am
saying so out loud.
### Verification
- `make check` green: 5 suites, 57 tests, prettier clean.
- `make build` green, 4 bundles verified off; `make clean && make build` green
from an empty `dist/`.
- `make build-debug` and `AUTISTMASK_DEBUG=1 make build` green, 4 bundles
verified on.
- `script/cibuild` (real `docker build .`, so `make check` then `make build`)
green end to end.
### Both required failure modes, actually triggered
Full transcripts are in the PR body. In short:
- **(a) define deleted** — `make check` exits `0` on that same tree, and
`make build` exits `2` with `carries both debug markers, so the build-time
DEBUG value was never resolved and the debug branch is still live`. Define
restored before committing; it is at `build.js:95`.
- **(b) unrecognizable bundle** — a bundle overwritten with content carrying
neither marker fails with `carries no debug marker, so its DEBUG state
cannot be determined [...] Refusing to report success`.
I also triggered four further guards: debug artifacts against a release
verification, empty manifest, missing manifest, and a manifest gone
stale/short (caught by the cross-check that no unlisted bundle may carry a
marker — this is what stops the check trusting whatever the manifest happens
to say).
### Things a reviewer might reasonably push on
- `build.js` grew a local `bundle()` helper, which is why the four esbuild
call sites collapse in the diff. Bundler options are unchanged apart from
`metafile: true`. The motive is that a future entry point cannot silently
skip metafile collection.
- The two new jest tests pin source-level invariants only. They cannot see a
compiled bundle and are not offered as a substitute for the artifact check.
- The marker adds a short string to every shipped bundle that contains
`constants.js`.
Reviewed at head b5f3da2 against base acb5885 (PR #169). Independent
checkout; nothing modified or committed (working tree verified clean at the
end of every experiment below).
1. Independent reproduction of the two required failure modes
I did not take the author's transcripts on trust. Both were reproduced from
scratch in an isolated worktree.
(a) Deleting the __BUILD_DEBUG__ define fails the build, while make check stays green
Removed __BUILD_DEBUG__: JSON.stringify(debugBuild), from the define map at build.js:95, then make clean && make build:
verify-build: FAIL: dist/chrome/src/popup/index.js carries no debug marker, so its DEBUG state cannot be
determined. ... Refusing to report success.
exit=1
Confirmed.
2. Additional failure modes the author did not demonstrate
Eight further attacks constructed against a known-good release dist/. All
hard-fail; none produced a silent pass.
Attack
Result
Manifest lists a file that does not exist
FAIL (lists ..., which does not exist), exit 1
Bundle unreadable (chmod 000)
FAIL (exit 1) — see finding N2 on the message
Bundle truncated mid-marker
FAIL, exit 1
Stale leftover debug artifact at an unlisted path (index-old.js)
FAIL via the unlisted-bundle cross-check, exit 1
Listed bundle replaced by a symlink to the debug build
FAIL (mode mismatch), exit 1 — -f and grep both follow the link
BUILD_DEBUG_MARKER deleted from src/shared/constants.js, then rebuilt
make build FAILS (no marker); make test also fails 2 tests
AUTISTMASK_DEBUG=true make build (non-literal 1)
release build, verified off — matches build.js:48 exactly
script/verify-build invoked from an unrelated cwd (/tmp)
correct, exit 0 — repo root resolution works
I also re-ran the author's own guards (empty manifest, missing manifest, debug
artifacts against a release verification) and all reproduced exactly as
claimed.
Attempt to construct a passing-but-wrong scenario
I could not find one. The two-source design closes it: the manifest comes from
esbuild's metafile and is independent of the marker, so a bundle losing its
marker is still listed and still demanded to carry one; and the check_unlisted_bundles cross-check means an under-generated or stale manifest
cannot buy silence, because any unlisted dist/**/*.js carrying a marker is
itself a failure. Renaming or removing constants.js empties the manifest,
which the -s test rejects. main() never reaches its success echo without count > 0 and every counted bundle matching expected. has_marker failing
for any reason (unreadable, absent, truncated) resolves to "neither", which is
a hard failure, not an absence of evidence. Neither loop runs in a subshell, so fail's exit 1 is always effective — verified empirically, not just by
reading.
The author's claim that there is no path to exit 0 without positively
identifying the expected marker in at least one bundle holds up.
3. Two-source design
Sound, and the reasoning is correct. Collapsing the two facts would make a
marker-less bundle indistinguishable from dist/chrome/src/content/index.js,
which legitimately carries no marker (confirmed: 8 emitted .js files, 4 in
the manifest, and check_unlisted_bundles passes on a good build, i.e. the
other four genuinely have no marker). Manifest staleness is handled from three
directions: pre-cleared at build.js:117 before anything is emitted, written
last at build.js:196 so a build that dies leaves none, and cross-checked
against the emitted tree at script/verify-build:75-90. A manifest left over
from a debug build cannot survive a release build, since the release build
deletes it and rewrites it.
4. BUILD_DEBUG_MARKER risk
No new risk found. It is derived from DEBUG on one line
(src/shared/constants.js:21-23), read nowhere at runtime, and its only effect
on the artifact is a 25-byte string literal. It cannot alter behaviour because
nothing branches on it. It cannot become the thing that goes wrong silently: I
deleted it and both the artifact check and the jest suite failed. DEBUG
itself and the typeof guard at constants.js:8 are byte-identical to #169.
5. Scope, conventions, non-weakening
script/verify-build is #!/bin/sh, set -eu, repo root via pwd -P, main "$@" at the bottom — matches the other twelve. /bin/sh on
this host is dash and the script executes under it, so POSIX compliance is
demonstrated, not asserted. No local, no [[, no arrays, no +=.
build.js diff: the bundle() helper preserves bundle, format, platform, target, minify and define verbatim; the only added option is metafile: true. Justified, and it does stop a future entry point skipping
metafile collection.
Nothing from #169 is weakened: isDebugBuild(), the define, and the constants.js DEBUG line are unchanged.
make build-debug succeeds and asserts the inverse (verified).
Tests are not vacuous — verified by mutation: breaking the marker in constants.js fails 2 of the 2 new tests.
6. Base branch
The deviation from note 6 was the right call and is disclosed in the PR body,
the PR comment, and TODO.md. main has no __BUILD_DEBUG__ to delete, so
demonstration (a) — the whole point of the issue — would have been unrunnable.
Merge ordering is unmistakable. Verified: b5f3da2 sits directly on acb5885
(linear), and git merge-tree against current main reports zero
conflicts for both this branch and #169's, so the retarget is clean.
Merge order: #169 to main first, then retarget #178 to main and merge.
7. Policy
No Claude/Anthropic references anywhere in the diff, commit message, or PR
body. No attribution trailers.
Commit b5f3da2 subject ends with (closes #170); the PR title does too, so
the squash subject carries it either way.
RULES.md unmodified. TODO.md is a single four-line insert at the top of
Completed Steps; stale Status/Next Step untouched, per note 6.
README Entrypoints documents script/verify-build, plus a Debug Builds note.
make check green (5 suites, 57 tests, prettier clean), so make fmt is
clean.
No scope creep. The two out-of-scope items (#152, #166) are correctly left
alone.
8. Verification runs (make targets and script/ only)
make check — green
make build — green, 4 bundles verified off
make build-debug — green, 4 bundles verified on
make verify-build standalone — green on a release dist, fails on a debug one
make clean && make build — green from an empty dist/
script/cibuild (real docker build .) — green; verify-build observed
running inside the image build, so the CI property holds
CI status on b5f3da2: success (check / check (push), 40s)
Mergeable: yes, no conflicts
Non-blocking findings
N1. script/verify-build:55-57 — the both-markers error message overstates
the defect, and the same overstatement is in the commit body and PR body.
The message says the missing define means "the debug branch is still live". I
checked the actual emitted output. With the define removed, the bundle contains:
var cB=typeof __BUILD_DEBUG__<"u"?__BUILD_DEBUG__:!1,gk=cB?"autistmask-build-debug=on":"autistmask-build-debug=off"
In extension context __BUILD_DEBUG__ is an undeclared global, so typeof
yields "undefined" and DEBUG evaluates false at runtime. The artifact is
therefore not drainable; what is actually broken is that the build no longer
resolves DEBUG at build time at all, so the artifact's mode is decided by an
undeclared global rather than by the build, and AUTISTMASK_DEBUG=1 silently
stops working. That is still worth failing on, and the message's remedy ("check
that build.js still defines __BUILD_DEBUG__") is exactly right — but the
severity claim is wrong. The commit body's "silently restoring the
drainable-wallet vulnerability in every shipped artifact" inherits the same
claim from issue #170's text, so this is not the author's invention, but it does
write a false security assertion into repo history. Acceptable wording: "the
build-time DEBUG value was never resolved, so this build no longer honours
AUTISTMASK_DEBUG and the artifact's DEBUG state is not fixed at build time."
Worth a follow-up issue rather than a rework of this PR.
N2. script/verify-build:38 — 2>/dev/null in has_marker conflates "grep
could not read the file" with "no match".
Reproduced with chmod 000 on a listed bundle: the script correctly hard-fails,
but reports "carries no debug marker ... the emitted output changed shape",
which sends the reader hunting for an esbuild change instead of a permission
problem. Acceptable: distinguish grep exit 2 from exit 1 and report the I/O
error as its own condition. Outcome is already fail-safe, so this is diagnostics
quality only.
N3. build.js:35 and script/verify-build:76 both restrict to *.js.
Consistent, so nothing is missed today (all four esbuild outfiles are .js).
But it is the one output shape that could escape the guard entirely: a future
bundle emitted as .mjs would be excluded from the manifest and from the
unlisted-bundle cross-check, so it would carry no marker requirement and raise
no error. Worth a comment noting the coupling, or widening the cross-check to
all regular files under dist/.
N4. script/verify-build:48-52 and :77 share the unscoped global _file.
sh has no local, and read_marker and check_unlisted_bundles currently
never nest, so this is safe today. It is a latent trap if anyone later calls read_marker from the cross-check loop.
N5. script/verify-build:118 and :77 — read -r strips leading/trailing
IFS whitespace and cannot represent a path containing a newline.
Purely theoretical given the fixed output names, noted only because interior
spaces are handled correctly while leading/trailing ones are not.
N6. AUTISTMASK_DEBUG=1 make build produces a debug artifact that the guard
reports as "verified".
Reproduced. This is a consistency check against the requested mode, not an
unconditional "release artifacts are never debug" assertion. Issue #170
explicitly requires AUTISTMASK_DEBUG=1 make build to succeed and assert the
inverse, so this is per spec and not a defect; and script/cibuild is docker build ., which does not inherit host environment, so CI is unaffected.
Recorded so the residual is on the record: the mode is announced loudly twice
(Build mode: DEBUG (INSECURE ...) and expecting autistmask-build-debug=on),
so it is not silent.
N7. PR title is 86 characters. The repo's default merge style is squash, so
that becomes the landing commit subject. No policy rule mandates 72, and the
commit's own subject is well within it. Cosmetic.
None of N1-N7 is a correctness hole, none can cause a silent pass, and none
weakens the guarantee the issue asked for. Every item in the definition of done
is met and independently verified.
## Review: PR #178 — VERDICT: PASS
Reviewed at head `b5f3da2` against base `acb5885` (PR #169). Independent
checkout; nothing modified or committed (working tree verified clean at the
end of every experiment below).
---
## 1. Independent reproduction of the two required failure modes
I did not take the author's transcripts on trust. Both were reproduced from
scratch in an isolated worktree.
### (a) Deleting the `__BUILD_DEBUG__` define fails the build, while `make check` stays green
Removed `__BUILD_DEBUG__: JSON.stringify(debugBuild),` from the `define` map at
`build.js:95`, then `make clean && make build`:
```
Bundles containing src/shared/constants.js: 4 (listed in dist/constants-bundles.txt)
Build complete: dist/chrome/ and dist/firefox/
Verifying emitted bundles (expecting autistmask-build-debug=off)...
verify-build: FAIL: dist/chrome/src/background/index.js carries both debug markers, ...
make: *** [Makefile:40: build] Error 1
build exit=2
```
Same tree, same commit, immediately after:
```
make check exit=0
```
Confirmed: `make check` cannot catch this and `make build` now does. Define
restored via `git checkout --`; `build.js:95` intact.
### (b) An unrecognizable bundle fails rather than passes
Clean release build, then `printf 'var reminified = 1;\n' > dist/chrome/src/popup/index.js`:
```
verify-build: FAIL: dist/chrome/src/popup/index.js carries no debug marker, so its DEBUG state cannot be
determined. ... Refusing to report success.
exit=1
```
Confirmed.
---
## 2. Additional failure modes the author did not demonstrate
Eight further attacks constructed against a known-good release `dist/`. All
hard-fail; none produced a silent pass.
| Attack | Result |
| --- | --- |
| Manifest lists a file that does not exist | FAIL (`lists ..., which does not exist`), exit 1 |
| Bundle unreadable (`chmod 000`) | FAIL (exit 1) — see finding N2 on the message |
| Bundle truncated mid-marker | FAIL, exit 1 |
| Stale leftover debug artifact at an unlisted path (`index-old.js`) | FAIL via the unlisted-bundle cross-check, exit 1 |
| Listed bundle replaced by a **symlink** to the debug build | FAIL (mode mismatch), exit 1 — `-f` and `grep` both follow the link |
| `BUILD_DEBUG_MARKER` deleted from `src/shared/constants.js`, then rebuilt | `make build` FAILS (no marker); `make test` also fails 2 tests |
| `AUTISTMASK_DEBUG=true make build` (non-literal `1`) | release build, verified `off` — matches `build.js:48` exactly |
| `script/verify-build` invoked from an unrelated cwd (`/tmp`) | correct, exit 0 — repo root resolution works |
I also re-ran the author's own guards (empty manifest, missing manifest, debug
artifacts against a release verification) and all reproduced exactly as
claimed.
### Attempt to construct a passing-but-wrong scenario
I could not find one. The two-source design closes it: the manifest comes from
esbuild's metafile and is independent of the marker, so a bundle losing its
marker is still listed and still demanded to carry one; and the
`check_unlisted_bundles` cross-check means an under-generated or stale manifest
cannot buy silence, because any unlisted `dist/**/*.js` carrying a marker is
itself a failure. Renaming or removing `constants.js` empties the manifest,
which the `-s` test rejects. `main()` never reaches its success `echo` without
`count > 0` and every counted bundle matching `expected`. `has_marker` failing
for any reason (unreadable, absent, truncated) resolves to "neither", which is
a hard failure, not an absence of evidence. Neither loop runs in a subshell, so
`fail`'s `exit 1` is always effective — verified empirically, not just by
reading.
The author's claim that there is no path to exit 0 without positively
identifying the expected marker in at least one bundle holds up.
---
## 3. Two-source design
Sound, and the reasoning is correct. Collapsing the two facts would make a
marker-less bundle indistinguishable from `dist/chrome/src/content/index.js`,
which legitimately carries no marker (confirmed: 8 emitted `.js` files, 4 in
the manifest, and `check_unlisted_bundles` passes on a good build, i.e. the
other four genuinely have no marker). Manifest staleness is handled from three
directions: pre-cleared at `build.js:117` before anything is emitted, written
last at `build.js:196` so a build that dies leaves none, and cross-checked
against the emitted tree at `script/verify-build:75-90`. A manifest left over
from a debug build cannot survive a release build, since the release build
deletes it and rewrites it.
## 4. `BUILD_DEBUG_MARKER` risk
No new risk found. It is derived from `DEBUG` on one line
(`src/shared/constants.js:21-23`), read nowhere at runtime, and its only effect
on the artifact is a 25-byte string literal. It cannot alter behaviour because
nothing branches on it. It cannot become the thing that goes wrong silently: I
deleted it and both the artifact check and the jest suite failed. `DEBUG`
itself and the `typeof` guard at `constants.js:8` are byte-identical to #169.
## 5. Scope, conventions, non-weakening
- `script/verify-build` is `#!/bin/sh`, `set -eu`, repo root via
`pwd -P`, `main "$@"` at the bottom — matches the other twelve. `/bin/sh` on
this host is `dash` and the script executes under it, so POSIX compliance is
demonstrated, not asserted. No `local`, no `[[`, no arrays, no `+=`.
- `build.js` diff: the `bundle()` helper preserves `bundle`, `format`,
`platform`, `target`, `minify` and `define` verbatim; the only added option is
`metafile: true`. Justified, and it does stop a future entry point skipping
metafile collection.
- Nothing from #169 is weakened: `isDebugBuild()`, the define, and the
`constants.js` DEBUG line are unchanged.
- `make build-debug` succeeds and asserts the inverse (verified).
- Tests are not vacuous — verified by mutation: breaking the marker in
`constants.js` fails 2 of the 2 new tests.
## 6. Base branch
The deviation from note 6 was the right call and is disclosed in the PR body,
the PR comment, and `TODO.md`. `main` has no `__BUILD_DEBUG__` to delete, so
demonstration (a) — the whole point of the issue — would have been unrunnable.
Merge ordering is unmistakable. Verified: `b5f3da2` sits directly on `acb5885`
(linear), and `git merge-tree` against current `main` reports **zero
conflicts** for both this branch and #169's, so the retarget is clean.
**Merge order: #169 to `main` first, then retarget #178 to `main` and merge.**
## 7. Policy
- No Claude/Anthropic references anywhere in the diff, commit message, or PR
body. No attribution trailers.
- Commit `b5f3da2` subject ends with ` (closes #170)`; the PR title does too, so
the squash subject carries it either way.
- `RULES.md` unmodified. `TODO.md` is a single four-line insert at the top of
Completed Steps; stale Status/Next Step untouched, per note 6.
- README Entrypoints documents `script/verify-build`, plus a Debug Builds note.
- `make check` green (5 suites, 57 tests, prettier clean), so `make fmt` is
clean.
- No scope creep. The two out-of-scope items (#152, #166) are correctly left
alone.
## 8. Verification runs (make targets and `script/` only)
- `make check` — green
- `make build` — green, 4 bundles verified `off`
- `make build-debug` — green, 4 bundles verified `on`
- `make verify-build` standalone — green on a release dist, fails on a debug one
- `make clean && make build` — green from an empty `dist/`
- `script/cibuild` (real `docker build .`) — green; `verify-build` observed
running inside the image build, so the CI property holds
- CI status on `b5f3da2`: **success** (`check / check (push)`, 40s)
- Mergeable: yes, no conflicts
---
## Non-blocking findings
**N1. `script/verify-build:55-57` — the both-markers error message overstates
the defect, and the same overstatement is in the commit body and PR body.**
The message says the missing define means "the debug branch is still live". I
checked the actual emitted output. With the define removed, the bundle contains:
```
var cB=typeof __BUILD_DEBUG__<"u"?__BUILD_DEBUG__:!1,gk=cB?"autistmask-build-debug=on":"autistmask-build-debug=off"
```
In extension context `__BUILD_DEBUG__` is an undeclared global, so `typeof`
yields `"undefined"` and `DEBUG` evaluates **false** at runtime. The artifact is
therefore not drainable; what is actually broken is that the build no longer
resolves `DEBUG` at build time at all, so the artifact's mode is decided by an
undeclared global rather than by the build, and `AUTISTMASK_DEBUG=1` silently
stops working. That is still worth failing on, and the message's remedy ("check
that build.js still defines `__BUILD_DEBUG__`") is exactly right — but the
severity claim is wrong. The commit body's "silently restoring the
drainable-wallet vulnerability in every shipped artifact" inherits the same
claim from issue #170's text, so this is not the author's invention, but it does
write a false security assertion into repo history. Acceptable wording: "the
build-time DEBUG value was never resolved, so this build no longer honours
AUTISTMASK_DEBUG and the artifact's DEBUG state is not fixed at build time."
Worth a follow-up issue rather than a rework of this PR.
**N2. `script/verify-build:38` — `2>/dev/null` in `has_marker` conflates "grep
could not read the file" with "no match".**
Reproduced with `chmod 000` on a listed bundle: the script correctly hard-fails,
but reports "carries no debug marker ... the emitted output changed shape",
which sends the reader hunting for an esbuild change instead of a permission
problem. Acceptable: distinguish grep exit 2 from exit 1 and report the I/O
error as its own condition. Outcome is already fail-safe, so this is diagnostics
quality only.
**N3. `build.js:35` and `script/verify-build:76` both restrict to `*.js`.**
Consistent, so nothing is missed today (all four esbuild outfiles are `.js`).
But it is the one output shape that could escape the guard entirely: a future
bundle emitted as `.mjs` would be excluded from the manifest *and* from the
unlisted-bundle cross-check, so it would carry no marker requirement and raise
no error. Worth a comment noting the coupling, or widening the cross-check to
all regular files under `dist/`.
**N4. `script/verify-build:48-52` and `:77` share the unscoped global `_file`.**
`sh` has no `local`, and `read_marker` and `check_unlisted_bundles` currently
never nest, so this is safe today. It is a latent trap if anyone later calls
`read_marker` from the cross-check loop.
**N5. `script/verify-build:118` and `:77` — `read -r` strips leading/trailing
IFS whitespace and cannot represent a path containing a newline.**
Purely theoretical given the fixed output names, noted only because interior
spaces are handled correctly while leading/trailing ones are not.
**N6. `AUTISTMASK_DEBUG=1 make build` produces a debug artifact that the guard
reports as "verified".**
Reproduced. This is a consistency check against the requested mode, not an
unconditional "release artifacts are never debug" assertion. Issue #170
explicitly requires `AUTISTMASK_DEBUG=1 make build` to succeed and assert the
inverse, so this is per spec and not a defect; and `script/cibuild` is
`docker build .`, which does not inherit host environment, so CI is unaffected.
Recorded so the residual is on the record: the mode is announced loudly twice
(`Build mode: DEBUG (INSECURE ...)` and `expecting autistmask-build-debug=on`),
so it is not silent.
**N7. PR title is 86 characters.** The repo's default merge style is squash, so
that becomes the landing commit subject. No policy rule mandates 72, and the
commit's own subject is well within it. Cosmetic.
---
None of N1-N7 is a correctness hole, none can cause a silent pass, and none
weakens the guarantee the issue asked for. Every item in the definition of done
is met and independently verified.
**PASS.** Merge after #169, then retarget to `main`.
Manager note (the review verdict is in its own comment above).
Independent adversarial review passed with no blocking findings. The reviewer
reproduced both demonstrated failure modes itself rather than trusting the
author's transcripts, and then constructed eight further attacks the author
never tried — manifest listing a nonexistent file, unreadable bundle via chmod 000, a bundle truncated mid-marker, a stale debug artifact at an
unlisted path, a listed bundle replaced by a symlink to the debug build, BUILD_DEBUG_MARKER deleted from constants.js and rebuilt, AUTISTMASK_DEBUG=true, and running the script from an unrelated cwd. All
hard-failed. None silently passed.
It also verified the structural point that makes the design work: neither loop
runs in a subshell, so fail's exit 1 always takes effect — checked
empirically, not by reading. And check_unlisted_bundles stops a short or
stale manifest from buying silence, which was the hole I was most worried
about when I asked for two independent signals.
In extension context that global is undeclared, so DEBUG evaluates to false at runtime and the artifact is not drainable.
So the claim I wrote into issue #170 — and repeated in the manager comment
dispatching it, and which the author then inherited into the commit body —
that deleting the define "silently restores the drainable-wallet vulnerability
in every shipped artifact" is overstated and wrong. I did not verify the
compiled output before asserting it; I reasoned from the source and assumed the
fallback would go the unsafe way. It goes the safe way.
The real defect the guard catches is still worth catching: DEBUG would no
longer be resolved at build time, leaving an unresolved ternary against an
undeclared global. That is a broken build contract and it means the release/
debug distinction has silently stopped being enforced at all — including, on a
future refactor, in whichever direction the fallback then happens to go. But it
is a correctness and build-integrity failure, not a live funds-loss one, and
this PR should not land carrying a scarier justification than the facts
support.
Nothing about the PR changes as a result; the guard is correct and worth having
on exactly the same terms. Only the wording is wrong, in three places
(script/verify-build:55-57, the commit body, and issue #170), and that is
folded into the follow-up below rather than being churned into a rework.
Merge ordering — important
Base is fix/issue-149-debug-build-flag, notmain. That deviation from
the issue's note 6 was correct: main has no define to delete, so the central
demonstration would have been unrunnable. It is disclosed in the PR body, the
PR comment, and TODO.md.
Merge #169 to main first, then retarget this PR to main and merge. git merge-tree shows zero conflicts against current main for both branches.
Non-blocking findings
Filed together as #180 against 1.0.0 — all three are in the new script and are
better fixed in one pass than churned here:
Reword the script/verify-build:55-57 diagnostic per the correction above.
script/verify-build:38 — 2>/dev/null conflates "grep could not read the
file" with "no match", so an unreadable bundle fails safe but is
misdiagnosed.
build.js:35 and script/verify-build:76 both filter to *.js, so a future
non-.js bundle would escape both the manifest and the cross-check.
Not acting on: the shared unscoped _file (latent, safe today), whitespace in
paths via read -r, and AUTISTMASK_DEBUG=1 make build blessing a debug
artifact — the last is explicitly required by #170 and Docker CI does not
inherit host env.
Manager note (the review verdict is in its own comment above).
Independent adversarial review passed with no blocking findings. The reviewer
reproduced both demonstrated failure modes itself rather than trusting the
author's transcripts, and then constructed **eight further attacks the author
never tried** — manifest listing a nonexistent file, unreadable bundle via
`chmod 000`, a bundle truncated mid-marker, a stale debug artifact at an
unlisted path, a listed bundle replaced by a **symlink to the debug build**,
`BUILD_DEBUG_MARKER` deleted from `constants.js` and rebuilt,
`AUTISTMASK_DEBUG=true`, and running the script from an unrelated cwd. All
hard-failed. None silently passed.
It also verified the structural point that makes the design work: neither loop
runs in a subshell, so `fail`'s `exit 1` always takes effect — checked
empirically, not by reading. And `check_unlisted_bundles` stops a short or
stale manifest from buying silence, which was the hole I was most worried
about when I asked for two independent signals.
Marking `merge-ready` and assigning to @sneak.
## Correction to this issue's own framing — my error
The reviewer inspected what the emitted bundle actually contains when the
define is deleted:
```js
var cB = typeof __BUILD_DEBUG__ < "u" ? __BUILD_DEBUG__ : !1, …
```
In extension context that global is undeclared, so `DEBUG` evaluates to
**false** at runtime and the artifact is **not** drainable.
So the claim I wrote into issue #170 — and repeated in the manager comment
dispatching it, and which the author then inherited into the commit body —
that deleting the define "silently restores the drainable-wallet vulnerability
in every shipped artifact" is **overstated and wrong**. I did not verify the
compiled output before asserting it; I reasoned from the source and assumed the
fallback would go the unsafe way. It goes the safe way.
The real defect the guard catches is still worth catching: **`DEBUG` would no
longer be resolved at build time**, leaving an unresolved ternary against an
undeclared global. That is a broken build contract and it means the release/
debug distinction has silently stopped being enforced at all — including, on a
future refactor, in whichever direction the fallback then happens to go. But it
is a correctness and build-integrity failure, not a live funds-loss one, and
this PR should not land carrying a scarier justification than the facts
support.
Nothing about the PR changes as a result; the guard is correct and worth having
on exactly the same terms. Only the wording is wrong, in three places
(`script/verify-build:55-57`, the commit body, and issue #170), and that is
folded into the follow-up below rather than being churned into a rework.
## Merge ordering — important
Base is `fix/issue-149-debug-build-flag`, **not** `main`. That deviation from
the issue's note 6 was correct: `main` has no define to delete, so the central
demonstration would have been unrunnable. It is disclosed in the PR body, the
PR comment, and `TODO.md`.
**Merge #169 to `main` first, then retarget this PR to `main` and merge.**
`git merge-tree` shows zero conflicts against current `main` for both branches.
## Non-blocking findings
Filed together as #180 against 1.0.0 — all three are in the new script and are
better fixed in one pass than churned here:
- Reword the `script/verify-build:55-57` diagnostic per the correction above.
- `script/verify-build:38` — `2>/dev/null` conflates "grep could not read the
file" with "no match", so an unreadable bundle fails safe but is
misdiagnosed.
- `build.js:35` and `script/verify-build:76` both filter to `*.js`, so a future
non-`.js` bundle would escape both the manifest and the cross-check.
Not acting on: the shared unscoped `_file` (latent, safe today), whitespace in
paths via `read -r`, and `AUTISTMASK_DEBUG=1 make build` blessing a debug
artifact — the last is explicitly required by #170 and Docker CI does not
inherit host env.
DEBUG was hardcoded to true in src/shared/constants.js, so every wallet
created from a build of main received the publicly committed test recovery
phrase and was instantly drainable. There was no way to produce a non-debug
build at all: the real entropy path in generateMnemonic() was dead code in
every artifact.
DEBUG is now a build-time constant injected by esbuild's define in build.js,
alongside the existing __BUILD_* defines, and read by constants.js with the
same typeof guard buildInfo.js uses. It is false unless the build was run
with AUTISTMASK_DEBUG=1 — an exact match, so an unset, empty or mistyped
value fails safe towards a release build. The build prints which mode it
used, and make build-debug is a shim for the debug case.
What DEBUG does when enabled is unchanged: the red banner plus the hardcoded
test phrase, no new conditionals. Mnemonic generation deliberately keeps
reading the compile-time constant rather than isDebug() from log.js, which
also ORs in the runtime debugMode flag the settings toggle drives; routing it
through isDebug() would let a user of a release build re-enable the known
test phrase for real wallets. That is now recorded at the call site, in the
README DEBUG Mode Policy, and covered by a regression test.
New tests/wallet.test.js covers both build modes: with the flag off, two
successive generateMnemonic() calls differ, both validate as BIP-39 phrases,
both are 12 words, neither is DEBUG_MNEMONIC, and the result still derives a
usable HD wallet — including with the runtime toggle forced on. With the flag
on, DEBUG is true and the test phrase is returned, so the debug path stays
proven rather than silently removed.
Verified with make check (55 tests, lint, fmt-check all green), and with
make build and make build-debug: all four bundles across dist/chrome and
dist/firefox export DEBUG:!1 in a release build and DEBUG:!0 in a debug
build, and AUTISTMASK_DEBUG=true likewise yields DEBUG:!1.
PR #169 made DEBUG a build-time flag defaulting off, but nothing guarded
the wiring. The tests load src/shared/constants.js outside a bundle and
take the jest fallback branch, so deleting the __BUILD_DEBUG__ define
from build.js left all tests passing and make check green while silently
restoring the drainable-wallet vulnerability in every shipped artifact.
The property only exists in the emitted output, so it is now asserted
against the emitted output.
script/verify-build reads two independent facts per bundle. Which
bundles must be inspected comes from esbuild's metafile: build.js writes
dist/constants-bundles.txt naming every emitted JS output whose input
set includes constants.js, so the set is derived from the real
dependency graph rather than a hardcoded count or filenames. What each
bundle's DEBUG state is comes from BUILD_DEBUG_MARKER, a new constant
derived from DEBUG itself that the bundler folds to exactly one of two
string literals. Deriving the bundle set from the marker would be the
silent-pass hole: a bundle with no marker would be indistinguishable
from content/index.js, which legitimately contains none.
The marker is a plain string rather than a match on minified `DEBUG:!1`,
because minifier output is not a contract across esbuild versions. When
DEBUG is not known at build time the fold cannot happen and both
literals survive, which is exactly the shape of the regression this
guards against. Every way of failing to determine a bundle's state is a
hard failure: missing manifest, empty manifest, a listed file that does
not exist, both markers, neither marker, the wrong marker, or a bundle
carrying a marker while absent from the manifest. There is no path on
which the script exits 0 without positively identifying the expected
marker in at least one bundle.
It runs on the build path only. make build and make build-debug both
invoke it, the latter asserting the inverse, and Dockerfile:17 runs a
bare make build, so CI fails on a release build with a live debug
branch. It is deliberately not in script/check: that would make check
depend on dist/ existing and pull a full build into its time budget, and
the obvious workaround -- skip when dist/ is absent -- is precisely the
silently-green behaviour this exists to prevent.
Rebase review at 38596b4 — PASS: nothing lost or duplicated in the conflict resolution (.PHONY keeps test-e2e, all 14 script/ entrypoints documented once, single BUILD_DEBUG_MARKER/__BUILD_DEBUG__ definition, TODO entry is a clean 3-line insert); guard re-falsified on the consolidated tree (define deleted at build.js:95 -> make build exits 2, make check still exits 0; define restored); make check (6 suites / 117 tests), make build, make build-debug, script/cibuild (layers RUN make check / RUN make build executed, not CACHED) and make test-e2e (4/4, the one path CI does not cover) all green; commit subject ends (closes #170), no attribution trailers; CI success on the head commit; fast-forwards onto next.
Anomaly, non-blocking, no rework asked: the PR description still describes the pre-rebase state — it says this targets fix/issue-149-debug-build-flag and "must be merged after" #169, which has already landed as f7f141a, and that it should later be retargeted to main (base is now next). Worth editing the description before merge; the squash subject and the commit message itself are unaffected.
Rebase review at `38596b4` — **PASS**: nothing lost or duplicated in the conflict resolution (`.PHONY` keeps `test-e2e`, all 14 `script/` entrypoints documented once, single `BUILD_DEBUG_MARKER`/`__BUILD_DEBUG__` definition, TODO entry is a clean 3-line insert); guard re-falsified on the consolidated tree (define deleted at `build.js:95` -> `make build` exits 2, `make check` still exits 0; define restored); `make check` (6 suites / 117 tests), `make build`, `make build-debug`, `script/cibuild` (layers `RUN make check` / `RUN make build` executed, not `CACHED`) and `make test-e2e` (4/4, the one path CI does not cover) all green; commit subject ends ` (closes #170)`, no attribution trailers; CI success on the head commit; fast-forwards onto `next`.
Anomaly, non-blocking, no rework asked: the PR description still describes the pre-rebase state — it says this targets `fix/issue-149-debug-build-flag` and "must be merged after" https://git.eeqj.de/sneak/AutistMask/pulls/169, which has already landed as `f7f141a`, and that it should later be retargeted to `main` (base is now `next`). Worth editing the description before merge; the squash subject and the commit message itself are unaffected.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #170.
Bases on
next. (The original description said this targeted PR #169's branch and had to be merged after it; #169 has since landed asf7f141aand this branch was rebased ontonextcarrying only its own guard commit.)The gap
Deleting the
__BUILD_DEBUG__define frombuild.jsleavesmake checkgreen while every emitted bundle ships with the debug branch live.tests/loadssrc/shared/constants.jsoutside a bundle and takes the jest fallback, so the property being protected exists only in the emitted output and has to be asserted there.The check
build.jspassesmetafile: trueand writesdist/constants-bundles.txt: every emitted JS output whose input set includessrc/shared/constants.js. Nothing hardcoded, and the manifest is cleared at the start of each build so a stale one can never be verified against.src/shared/constants.jsexportsBUILD_DEBUG_MARKER, derived fromDEBUGitself, which the bundler constant-folds toautistmask-build-debug=offor=on. IfDEBUGis not known at build time the ternary survives and BOTH literals appear — which is the primary failure signal, not a gap.script/verify-buildcross-checks the two independent facts. Missing manifest, empty manifest, missing file, both markers, neither marker, wrong marker, a marker-carrying bundle absent from the manifest, and zero bundles inspected are each a hard failure. There is no path to exit 0 without positively identifying the expected marker.make buildandmake build-debug(the latter asserting the inverse), soscript/cibuildcovers it. Deliberately not inmake check, which would otherwise depend ondist/existing.Verification
make check,make build,make build-debugandscript/cibuildgreen on the consolidated tree, with the four bundles verifiedoff. Falsification re-run after the rebase: with the define deleted,make buildexits 2 with "carries both debug markers" whilemake checkstill exits 0 — the guard is the only thing that catches it.Diagnostic wording that overstates the consequence is tracked separately in #180.
Summary for review
Base is PR #169's branch, not
main. Deliberate, and the one place I wentagainst the issue's notes.
mainhas no__BUILD_DEBUG__define, so a guardbranched there could not be shown to trigger — there would be no define to
delete. Merge after #169; retarget to
mainthen. Full reasoning at the topof the PR body.
What to look at
The interesting decision is that the check reads two facts from two
independent sources, rather than inferring one from the other:
build.jstodist/constants-bundles.txt;BUILD_DEBUG_MARKER, a newconstant in
src/shared/constants.jsderived fromDEBUGand folded by thebundler to one of two string literals.
Collapsing these into one signal is the silent-pass failure: a marker-less
bundle would then be indistinguishable from
content/index.js, whichlegitimately has no marker, and the check reads green forever. Worth
attacking that reasoning if you think it is wrong.
The marker replaces the
DEBUG:!1substring match the issue mentioned,because a string literal is stable across esbuild versions and
!1is not.Its useful property is that when
DEBUGis not resolvable at build time thefold does not happen and both literals land in the bundle — which is
exactly the shape of the regression being guarded, so the ambiguity is the
detector rather than a hole in it.
Where it runs
Build path only.
make buildandmake build-debugboth invokescript/verify-build, andDockerfile:17runs a baremake build, so CIcovers it. Not in
script/check, on purpose: that would makecheckdepend on
dist/existing and pull a build into its time budget, and the"skip if
dist/is absent" workaround is precisely the silently-greenbehaviour the issue forbids. Per the steer, the build path wins and I am
saying so out loud.
Verification
make checkgreen: 5 suites, 57 tests, prettier clean.make buildgreen, 4 bundles verified off;make clean && make buildgreenfrom an empty
dist/.make build-debugandAUTISTMASK_DEBUG=1 make buildgreen, 4 bundlesverified on.
script/cibuild(realdocker build ., somake checkthenmake build)green end to end.
Both required failure modes, actually triggered
Full transcripts are in the PR body. In short:
make checkexits0on that same tree, andmake buildexits2withcarries both debug markers, so the build-time DEBUG value was never resolved and the debug branch is still live. Definerestored before committing; it is at
build.js:95.neither marker fails with
carries no debug marker, so its DEBUG state cannot be determined [...] Refusing to report success.I also triggered four further guards: debug artifacts against a release
verification, empty manifest, missing manifest, and a manifest gone
stale/short (caught by the cross-check that no unlisted bundle may carry a
marker — this is what stops the check trusting whatever the manifest happens
to say).
Things a reviewer might reasonably push on
build.jsgrew a localbundle()helper, which is why the four esbuildcall sites collapse in the diff. Bundler options are unchanged apart from
metafile: true. The motive is that a future entry point cannot silentlyskip metafile collection.
compiled bundle and are not offered as a substitute for the artifact check.
constants.js.Review: PR #178 — VERDICT: PASS
Reviewed at head
b5f3da2against baseacb5885(PR #169). Independentcheckout; nothing modified or committed (working tree verified clean at the
end of every experiment below).
1. Independent reproduction of the two required failure modes
I did not take the author's transcripts on trust. Both were reproduced from
scratch in an isolated worktree.
(a) Deleting the
__BUILD_DEBUG__define fails the build, whilemake checkstays greenRemoved
__BUILD_DEBUG__: JSON.stringify(debugBuild),from thedefinemap atbuild.js:95, thenmake clean && make build:Same tree, same commit, immediately after:
Confirmed:
make checkcannot catch this andmake buildnow does. Definerestored via
git checkout --;build.js:95intact.(b) An unrecognizable bundle fails rather than passes
Clean release build, then
printf 'var reminified = 1;\n' > dist/chrome/src/popup/index.js:Confirmed.
2. Additional failure modes the author did not demonstrate
Eight further attacks constructed against a known-good release
dist/. Allhard-fail; none produced a silent pass.
lists ..., which does not exist), exit 1chmod 000)index-old.js)-fandgrepboth follow the linkBUILD_DEBUG_MARKERdeleted fromsrc/shared/constants.js, then rebuiltmake buildFAILS (no marker);make testalso fails 2 testsAUTISTMASK_DEBUG=true make build(non-literal1)off— matchesbuild.js:48exactlyscript/verify-buildinvoked from an unrelated cwd (/tmp)I also re-ran the author's own guards (empty manifest, missing manifest, debug
artifacts against a release verification) and all reproduced exactly as
claimed.
Attempt to construct a passing-but-wrong scenario
I could not find one. The two-source design closes it: the manifest comes from
esbuild's metafile and is independent of the marker, so a bundle losing its
marker is still listed and still demanded to carry one; and the
check_unlisted_bundlescross-check means an under-generated or stale manifestcannot buy silence, because any unlisted
dist/**/*.jscarrying a marker isitself a failure. Renaming or removing
constants.jsempties the manifest,which the
-stest rejects.main()never reaches its successechowithoutcount > 0and every counted bundle matchingexpected.has_markerfailingfor any reason (unreadable, absent, truncated) resolves to "neither", which is
a hard failure, not an absence of evidence. Neither loop runs in a subshell, so
fail'sexit 1is always effective — verified empirically, not just byreading.
The author's claim that there is no path to exit 0 without positively
identifying the expected marker in at least one bundle holds up.
3. Two-source design
Sound, and the reasoning is correct. Collapsing the two facts would make a
marker-less bundle indistinguishable from
dist/chrome/src/content/index.js,which legitimately carries no marker (confirmed: 8 emitted
.jsfiles, 4 inthe manifest, and
check_unlisted_bundlespasses on a good build, i.e. theother four genuinely have no marker). Manifest staleness is handled from three
directions: pre-cleared at
build.js:117before anything is emitted, writtenlast at
build.js:196so a build that dies leaves none, and cross-checkedagainst the emitted tree at
script/verify-build:75-90. A manifest left overfrom a debug build cannot survive a release build, since the release build
deletes it and rewrites it.
4.
BUILD_DEBUG_MARKERriskNo new risk found. It is derived from
DEBUGon one line(
src/shared/constants.js:21-23), read nowhere at runtime, and its only effecton the artifact is a 25-byte string literal. It cannot alter behaviour because
nothing branches on it. It cannot become the thing that goes wrong silently: I
deleted it and both the artifact check and the jest suite failed.
DEBUGitself and the
typeofguard atconstants.js:8are byte-identical to #169.5. Scope, conventions, non-weakening
script/verify-buildis#!/bin/sh,set -eu, repo root viapwd -P,main "$@"at the bottom — matches the other twelve./bin/shonthis host is
dashand the script executes under it, so POSIX compliance isdemonstrated, not asserted. No
local, no[[, no arrays, no+=.build.jsdiff: thebundle()helper preservesbundle,format,platform,target,minifyanddefineverbatim; the only added option ismetafile: true. Justified, and it does stop a future entry point skippingmetafile collection.
isDebugBuild(), the define, and theconstants.jsDEBUG line are unchanged.make build-debugsucceeds and asserts the inverse (verified).constants.jsfails 2 of the 2 new tests.6. Base branch
The deviation from note 6 was the right call and is disclosed in the PR body,
the PR comment, and
TODO.md.mainhas no__BUILD_DEBUG__to delete, sodemonstration (a) — the whole point of the issue — would have been unrunnable.
Merge ordering is unmistakable. Verified:
b5f3da2sits directly onacb5885(linear), and
git merge-treeagainst currentmainreports zeroconflicts for both this branch and #169's, so the retarget is clean.
Merge order: #169 to
mainfirst, then retarget #178 tomainand merge.7. Policy
body. No attribution trailers.
b5f3da2subject ends with(closes #170); the PR title does too, sothe squash subject carries it either way.
RULES.mdunmodified.TODO.mdis a single four-line insert at the top ofCompleted Steps; stale Status/Next Step untouched, per note 6.
script/verify-build, plus a Debug Builds note.make checkgreen (5 suites, 57 tests, prettier clean), somake fmtisclean.
alone.
8. Verification runs (make targets and
script/only)make check— greenmake build— green, 4 bundles verifiedoffmake build-debug— green, 4 bundles verifiedonmake verify-buildstandalone — green on a release dist, fails on a debug onemake clean && make build— green from an emptydist/script/cibuild(realdocker build .) — green;verify-buildobservedrunning inside the image build, so the CI property holds
b5f3da2: success (check / check (push), 40s)Non-blocking findings
N1.
script/verify-build:55-57— the both-markers error message overstatesthe defect, and the same overstatement is in the commit body and PR body.
The message says the missing define means "the debug branch is still live". I
checked the actual emitted output. With the define removed, the bundle contains:
In extension context
__BUILD_DEBUG__is an undeclared global, sotypeofyields
"undefined"andDEBUGevaluates false at runtime. The artifact istherefore not drainable; what is actually broken is that the build no longer
resolves
DEBUGat build time at all, so the artifact's mode is decided by anundeclared global rather than by the build, and
AUTISTMASK_DEBUG=1silentlystops working. That is still worth failing on, and the message's remedy ("check
that build.js still defines
__BUILD_DEBUG__") is exactly right — but theseverity claim is wrong. The commit body's "silently restoring the
drainable-wallet vulnerability in every shipped artifact" inherits the same
claim from issue #170's text, so this is not the author's invention, but it does
write a false security assertion into repo history. Acceptable wording: "the
build-time DEBUG value was never resolved, so this build no longer honours
AUTISTMASK_DEBUG and the artifact's DEBUG state is not fixed at build time."
Worth a follow-up issue rather than a rework of this PR.
N2.
script/verify-build:38—2>/dev/nullinhas_markerconflates "grepcould not read the file" with "no match".
Reproduced with
chmod 000on a listed bundle: the script correctly hard-fails,but reports "carries no debug marker ... the emitted output changed shape",
which sends the reader hunting for an esbuild change instead of a permission
problem. Acceptable: distinguish grep exit 2 from exit 1 and report the I/O
error as its own condition. Outcome is already fail-safe, so this is diagnostics
quality only.
N3.
build.js:35andscript/verify-build:76both restrict to*.js.Consistent, so nothing is missed today (all four esbuild outfiles are
.js).But it is the one output shape that could escape the guard entirely: a future
bundle emitted as
.mjswould be excluded from the manifest and from theunlisted-bundle cross-check, so it would carry no marker requirement and raise
no error. Worth a comment noting the coupling, or widening the cross-check to
all regular files under
dist/.N4.
script/verify-build:48-52and:77share the unscoped global_file.shhas nolocal, andread_markerandcheck_unlisted_bundlescurrentlynever nest, so this is safe today. It is a latent trap if anyone later calls
read_markerfrom the cross-check loop.N5.
script/verify-build:118and:77—read -rstrips leading/trailingIFS whitespace and cannot represent a path containing a newline.
Purely theoretical given the fixed output names, noted only because interior
spaces are handled correctly while leading/trailing ones are not.
N6.
AUTISTMASK_DEBUG=1 make buildproduces a debug artifact that the guardreports as "verified".
Reproduced. This is a consistency check against the requested mode, not an
unconditional "release artifacts are never debug" assertion. Issue #170
explicitly requires
AUTISTMASK_DEBUG=1 make buildto succeed and assert theinverse, so this is per spec and not a defect; and
script/cibuildisdocker build ., which does not inherit host environment, so CI is unaffected.Recorded so the residual is on the record: the mode is announced loudly twice
(
Build mode: DEBUG (INSECURE ...)andexpecting autistmask-build-debug=on),so it is not silent.
N7. PR title is 86 characters. The repo's default merge style is squash, so
that becomes the landing commit subject. No policy rule mandates 72, and the
commit's own subject is well within it. Cosmetic.
None of N1-N7 is a correctness hole, none can cause a silent pass, and none
weakens the guarantee the issue asked for. Every item in the definition of done
is met and independently verified.
PASS. Merge after #169, then retarget to
main.Manager note (the review verdict is in its own comment above).
Independent adversarial review passed with no blocking findings. The reviewer
reproduced both demonstrated failure modes itself rather than trusting the
author's transcripts, and then constructed eight further attacks the author
never tried — manifest listing a nonexistent file, unreadable bundle via
chmod 000, a bundle truncated mid-marker, a stale debug artifact at anunlisted path, a listed bundle replaced by a symlink to the debug build,
BUILD_DEBUG_MARKERdeleted fromconstants.jsand rebuilt,AUTISTMASK_DEBUG=true, and running the script from an unrelated cwd. Allhard-failed. None silently passed.
It also verified the structural point that makes the design work: neither loop
runs in a subshell, so
fail'sexit 1always takes effect — checkedempirically, not by reading. And
check_unlisted_bundlesstops a short orstale manifest from buying silence, which was the hole I was most worried
about when I asked for two independent signals.
Marking
merge-readyand assigning to @sneak.Correction to this issue's own framing — my error
The reviewer inspected what the emitted bundle actually contains when the
define is deleted:
In extension context that global is undeclared, so
DEBUGevaluates tofalse at runtime and the artifact is not drainable.
So the claim I wrote into issue #170 — and repeated in the manager comment
dispatching it, and which the author then inherited into the commit body —
that deleting the define "silently restores the drainable-wallet vulnerability
in every shipped artifact" is overstated and wrong. I did not verify the
compiled output before asserting it; I reasoned from the source and assumed the
fallback would go the unsafe way. It goes the safe way.
The real defect the guard catches is still worth catching:
DEBUGwould nolonger be resolved at build time, leaving an unresolved ternary against an
undeclared global. That is a broken build contract and it means the release/
debug distinction has silently stopped being enforced at all — including, on a
future refactor, in whichever direction the fallback then happens to go. But it
is a correctness and build-integrity failure, not a live funds-loss one, and
this PR should not land carrying a scarier justification than the facts
support.
Nothing about the PR changes as a result; the guard is correct and worth having
on exactly the same terms. Only the wording is wrong, in three places
(
script/verify-build:55-57, the commit body, and issue #170), and that isfolded into the follow-up below rather than being churned into a rework.
Merge ordering — important
Base is
fix/issue-149-debug-build-flag, notmain. That deviation fromthe issue's note 6 was correct:
mainhas no define to delete, so the centraldemonstration would have been unrunnable. It is disclosed in the PR body, the
PR comment, and
TODO.md.Merge #169 to
mainfirst, then retarget this PR tomainand merge.git merge-treeshows zero conflicts against currentmainfor both branches.Non-blocking findings
Filed together as #180 against 1.0.0 — all three are in the new script and are
better fixed in one pass than churned here:
script/verify-build:55-57diagnostic per the correction above.script/verify-build:38—2>/dev/nullconflates "grep could not read thefile" with "no match", so an unreadable bundle fails safe but is
misdiagnosed.
build.js:35andscript/verify-build:76both filter to*.js, so a futurenon-
.jsbundle would escape both the manifest and the cross-check.Not acting on: the shared unscoped
_file(latent, safe today), whitespace inpaths via
read -r, andAUTISTMASK_DEBUG=1 make buildblessing a debugartifact — the last is explicitly required by #170 and Docker CI does not
inherit host env.
clawbot referenced this pull request2026-08-10 15:49:44 +02:00
b5f3da2388to38596b4c79Rebase review at
38596b4— PASS: nothing lost or duplicated in the conflict resolution (.PHONYkeepstest-e2e, all 14script/entrypoints documented once, singleBUILD_DEBUG_MARKER/__BUILD_DEBUG__definition, TODO entry is a clean 3-line insert); guard re-falsified on the consolidated tree (define deleted atbuild.js:95->make buildexits 2,make checkstill exits 0; define restored);make check(6 suites / 117 tests),make build,make build-debug,script/cibuild(layersRUN make check/RUN make buildexecuted, notCACHED) andmake test-e2e(4/4, the one path CI does not cover) all green; commit subject ends(closes #170), no attribution trailers; CI success on the head commit; fast-forwards ontonext.Anomaly, non-blocking, no rework asked: the PR description still describes the pre-rebase state — it says this targets
fix/issue-149-debug-build-flagand "must be merged after" #169, which has already landed asf7f141a, and that it should later be retargeted tomain(base is nownext). Worth editing the description before merge; the squash subject and the commit message itself are unaffected.38596b4c79to627c0c158eclawbot referenced this pull request2026-08-11 14:23:43 +02:00