Compile bin/ alongside src/ so the package can be built (closes #3) #26
Reference in New Issue
Block a user
Delete Branch "fix-ts-build-rootdir"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #3.
Branched from
mainat348f23b. Two commits: the failing tests, then the fix.What changed
tsconfig.jsonrootDir./src→.; addednoEmitOnError: truepackage.jsonmain→./dist/src/index.js,types→./dist/src/index.d.ts;bin.quakunchanged at./dist/bin/quak.js;buildscript →script/build; newquakscript →node ./dist/bin/quak.jsscript/build(new)Makefilebuildis now a shim overscript/buildDockerfileRUN make buildafter the existingRUN make checksrc/crypto/stream.tstest/packaging/entrypoints.test.ts(new)README.mdscript/buildin Entrypoints;cibuild/workflow lines now say the image runsmake checkandmake build; a paragraph under Layout describing the emitted treeTODO.mdNot touched:
make check's composition,engines,exports,prepublishOnly,script/cibuild,make clean, and anything belonging to #4, #5, #6, #13, #24, #25.Layout decision
Took the recommended approach —
rootDir: "."— and hit no obstacle. It is a one-line config change against a CLI move that would have churnedbin/quak.tswholesale and contradicted the Layout diagram in the README's Design section. The emitted tree keeps the shape of the source tree, sodist/src/…+dist/bin/…, andmain/typesmove accordingly.It also keeps #5 open rather than closing it off: with
rootDirat the repository root,package.jsonsits inside the root dir, so a relative import of it resolves both fromsrc/under vitest and fromdist/after a build. I did not implement #5 and did not importpackage.jsonanywhere.Observed emitted artifact paths
make cleanthenmake build, thenls -lon the result. Quoting what I saw, not what I expected:So each declared entrypoint exists:
main→dist/src/index.js(659 bytes),types→dist/src/index.d.ts(1176 bytes),bin.quak→dist/bin/quak.js(12061 bytes, mode-rwxr-xr-x). The declaration map and source map for the library entrypoint are both present.What TS6059 was hiding
Clearing the config error made
tscreach the program for the first time, and it reported two errors insrc/crypto/stream.tsthat no one had ever seen:StreamPullStatewas declared assodium.StateAddress, reading a type off the default import's name.libsodium-wrappers-sumoexportsStateAddressas a named type; the alias now comes from there.crypto_secretstream_xchacha20poly1305_pullwas called with two arguments. Its declared signature is(state_address, cipher, ad, outputFormat?)—adis not optional — so the call now passesnullfor "no additional data", matching thenullalready passed on the push side inencryptBlob.Neither changes what executes; the 10 tests in
test/crypto/stream.test.tsand the 8 intest/crypto/encrypt-blob.test.tspass unchanged, which is the only reason I am willing to call the second one behaviour-preserving.TDD, honestly
The first commit (
d79ed83) addstest/packaging/entrypoints.test.tsand three of its six tests fail at that commit:They read
tsconfig.jsonandpackage.jsonand assert the contract between them — everyincludepattern must root underrootDir(that is exactly the TS6059 precondition), andmain/types/bin.quakmust equal the pathstscwill emit forsrc/index.tsandbin/quak.ts— without running a compiler, so they stay in the fast unit suite.What they cannot do is prove the compiler actually wrote those files. That check needs a build, and putting a build inside
make checkwould both changemake check's composition, whichREPO_POLICIES.mdfixes, and make it depend on a prior build. So it lives inscript/build, which runs aftertscand fails the build when an advertised entrypoint is missing. I verified that guard fires rather than assuming it: pointingmainat./dist/src/nonexistent-probe.jsand runningmake buildgaveand I reverted the probe.
Verification
Every line below is something I ran in this session, in this worktree.
git worktree listfirst: this tree has nothing nested under.claude/worktrees/, so the #25 inflation does not apply. Baseline on348f23bbefore any change was 18 files / 210 tests / 7.9s wall; the suite now reports 19 files / 216 tests, i.e. the six new ones and nothing else.348f23bmake bootstrap,make buildFile '…/bin/quak.ts' is not under 'rootDir' '…/src'make checkgreenmake checkfrom a clean treemake checkstill green withdist/presentmake checkafter a builddist/alreadymake buildgreenmake cleanthenmake buildverifiedfor all three entrypointsls -l(output quoted above)head -3 dist/bin/quak.js#!/usr/bin/env node./dist/bin/quak.js --helpdist/bin→dist/srcimports all resolvemainis loadablenode --input-type=module -e 'import { VERSION } from "./dist/src/index.js"; …'0.0.0make cleanremoves everything the build producesmake clean, thenls -a,git status --ignored --shortdist/, nothing stray inbin/orsrc/noEmitOnErrorprevents partial outputmake build, thenls distdistdid not exist; probe file deleteddocker build --no-cache -t quak-issue3-verify .RUN make check→ 19 files / 216 tests,RUN make build→ verified all three entrypoints; image exportedI did not run
docker builder pruneordocker system prune. The--no-cachebuild was scoped to this image, so itsRUN make buildreally executed rather than being served from the cache defect in #4.One thing I did not verify:
yarn quak login. The task forbids invoking yarn scripts directly, so I never ranyarn quak. What I did verify is the two halves it is made of: thequakscript isnode ./dist/bin/quak.js, and runningdist/bin/quak.jsdirectly produces the CLI's help output. The argument forwarding fromyarn quak <command>to that script is the only untested link. The README's Getting Started block is therefore unchanged — the invocation it documents is the one that now exists.Why
noEmitOnErroris in hereIt is not in the issue, so it deserves a justification. Without it
tscemits despite errors, which is not hypothetical: reproducing the bug on348f23bleftbin/quak.js,bin/quak.d.tsand their maps sitting next tobin/quak.tsin the source tree. eslint then read the generatedbin/quak.jsand failedmake lintwith twelveno-undeferrors, andmake fmtreformatted the generated file —make cleanremovesdist/and would not have removed any of it. WithnoEmitOnErrora failed build leaves nothing behind, which is also what makes themake cleanguarantee in the issue's point 6 hold in the failure case and not just the success case.Follow-up found, not fixed
devDependenciespins@types/libsodium-wrappers-sumo@0.8.2, which is a deprecated stub — the package's ownpackage.jsonsays "libsodium-wrappers-sumo provides its own type definitions, so you do not need this installed", and the directory contains no.d.tsat all. Removing it is out of scope here; I will file it separately.Summary and verification
Two commits off
mainat348f23b:d79ed83—test/packaging/entrypoints.test.ts, three of its six tests red at that commit.69bd6d1— the fix, plusTODO.mdand the README changes in the same commit.What it does.
rootDirbecomes the repository root sobin/compiles alongsidesrc/instead of tripping TS6059; output isdist/src/anddist/bin/, andmain/typesfollow it.script/buildcompiles and then verifies the entrypointspackage.jsonadvertises are among the files the compiler wrote, and sets the executable bit the compiler does not carry over. The Dockerfile runsmake buildas well asmake check. Aquakscript makes the README'syarn quak <command>examples resolve to the built CLI.noEmitOnErrorkeeps a failed build from leaving output behind.Clearing TS6059 let
tsctype-check for the first time and it found two real errors insrc/crypto/stream.ts(sodium.StateAddressused as a namespace member, andcrypto_secretstream_xchacha20poly1305_pullcalled without its requiredadargument). Both are fixed; neither changes what executes.Verification, all run in this session. No worktrees nested under the measured tree, checked with
git worktree listbefore measuring.make checkfrom clean: 19 files, 216 tests passed, 8.79s test duration, 13.6s for the whole target. Baseline before the change was 18 files / 210 tests / 7.9s, so this adds six tests and roughly a second.make checkagain withdist/present: still 216 passed — eslint and prettier already ignoredist/.make buildfrom clean: exit 0, printingverifiedfor./dist/src/index.js,./dist/src/index.d.tsand./dist/bin/quak.js.ls -l dist/src/index.js dist/src/index.d.ts dist/src/index.js.map dist/src/index.d.ts.mapandls -l dist/bin: all present;dist/bin/quak.jsis mode-rwxr-xr-x, 12061 bytes.head -3 dist/bin/quak.js: first line is#!/usr/bin/env node../dist/bin/quak.js --help: prints the full command list, so the shebang, the mode bit and thedist/bin→dist/srcimports all work.node --input-type=module -e 'import { VERSION } from "./dist/src/index.js"; …': prints0.0.0.make cleanthenls -aandgit status --ignored --short:dist/gone, nothing stray inbin/orsrc/.make build: failed anddistdid not exist. Probe file removed.mainpointed at a nonexistent path,make build: failed withbuild: package.json declares ./dist/src/nonexistent-probe.js, which the build did not produce. Reverted.docker build --no-cache -t quak-issue3-verify .:RUN make checkran 19 files / 216 tests,RUN make buildverified all three entrypoints, image exported. Scoped to this image; nodocker builder pruneordocker system prunewas run.Not verified:
yarn quak login. Invoking yarn scripts directly is off-limits for this task, so the argument forwarding fromyarn quak <command>intonode ./dist/bin/quak.jsis the one link I did not exercise. Both halves of it are verified separately.Review of PR #26 — verdict: PASS
Independent review. I did not write this change. Everything below was re-run in my own worktree at
69bd6d1, withgit worktree listconfirming nothing nested under the tree I measured (so the #25 inflation does not apply). Nothing in the PR body was taken on trust; where a claim was checkable I checked it, including the artifact byte sizes.Blocking findings
None.
The crypto change is behaviour-preserving. Verified, not assumed.
This was the highest-risk part of the change, since it touches the secretstream pull path in a repo whose rule is no hand-rolled crypto.
The declared signature in
node_modules/libsodium-wrappers-sumo/dist/modules-sumo/libsodium-wrappers.d.ts:1801is:adis the additional-authenticated-data argument and is genuinely non-optional in the type, so TS2554 is a real error andnullis the declared spelling of "no additional data". The question that matters is what the runtime did when the argument was absent. I answered it empirically rather than by reading the type: pushing a chunk withad = null(whatencryptBlobdoes) and then pulling it five ways gaveand against a chunk pushed with a real
ad, both the two-argument call and thenullcall returnFALSEidentically. The wrapper normalizes a missing or nulladto a zero-length AD via thenull != aguard visible in the minified bundle, which is the same guard the push wrapper uses. So the omitted argument defaulted to exactly the value now passed explicitly:pull(state, ct)andpull(state, ct, null)are the same operation, byte for byte. No existing ciphertext can decrypt differently. There is exactly one call site (src/crypto/stream.ts:92), so the change is confined.The
StateAddresschange is likewise correct:StateAddressis a named export of the module (libsodium-wrappers.d.ts:10), the default import has no type namespace under it, and the emitteddist/src/crypto/stream.jsline 1 isimport sodium from "libsodium-wrappers-sumo";— the type import is erased, so nothing changes at runtime.The tests behind this are not mocks:
test/crypto/stream.test.tsround-trips through real libsodium, including multi-chunk streams, wrong-key, corrupted-chunk and out-of-order cases.I agree these two fixes are in scope. I reproduced that they are load-bearing: on
348f23bwith onlyrootDirset to.and nothing else changed,make buildreports exactlyThe build cannot be green without them, and they went no further than the two errors.
The six new tests are not decoration
I broke the change and confirmed they go red. Reverting only
tsconfig.jsonrootDirto./srcon the PR head: 4 failed / 212 passed, failingcompiles only files that live under rootDir,main,typesandbin.quak. At the red-phase commitd79ed83the suite reports 3 failed / 213 passed, failing exactly the three tests the PR body names — the claim is accurate to the test.Their limitation is real and is stated honestly in the PR body: they assert the contract between
tsconfig.jsonandpackage.jsonwithout running a compiler, so they cannot prove the files landed. That complementary check is inscript/build, and I confirmed the guard fires rather than trusting the report — pointingmainat a nonexistent path producedand I reverted the probe. This split (contract in the fast suite, on-disk existence in the build) is the right place for each half, and it keeps
make check's composition untouched as the policy requires.noEmitOnError— justified, and the story checks outI reproduced the failure mode on
348f23b.make buildfails with TS6059 and still emits, leavingbin/quak.js,bin/quak.d.ts,bin/quak.d.ts.mapandbin/quak.js.mapnext tobin/quak.ts, all untracked.make lintthen reads the generated file and fails with exactly 12no-undeferrors onbin/quak.js. With the PR'snoEmitOnError, the same one-linerootDirrevert produced nodist/and no stray output at all. The justification is accurate, and it is what makes the issue's point 6 (make cleanremoves everything the build produces) hold in the failure case as well as the success case.Your
.gitignoreobservation is correct — it hasbin/quakbut no pattern coveringbin/*.js, so those four files really are untracked and committable. It is a nit rather than a defect here, because the pollution path is now structurally closed: a failing compile emits nothing, and a succeeding compile withrootDirat the repo root always writes underdist/.Public API paths
After
make clean; make build:main→dist/src/index.js, 659 bytes, present;dist/src/index.js.map623;dist/src/index.d.ts1176;dist/src/index.d.ts.map997bin.quak→dist/bin/quak.js, 12061 bytes, mode-rwxr-xr-x, first line#!/usr/bin/env node;quak.d.ts65,quak.d.ts.map105,quak.js.map13591Every size in the PR body matches to the byte.
./dist/bin/quak.js --helpruns and prints the full command list, so the shebang, the mode bit and thedist/bintodist/srcimport resolution all work.import { VERSION } from "…/dist/src/index.js"printed0.0.0. A repo-wide grep finds no surviving reference to./dist/index.jsin README, Dockerfile, Makefile, scripts, workflow orpackage.json.filesstill shipsdist/, which covers both subtrees.The layout also keeps #5 open as claimed: with
rootDirat the repo root,package.jsonis inside the root dir, so a relative import of it emits alongside the output and resolves both under vitest and fromdist/.The disclosed
yarn quakgapAcceptable, and correctly disclosed rather than papered over. Both halves are verified — the script is
node ./dist/bin/quak.js, a test asserts it points atbin.quak, and the built CLI runs — leaving only yarn's own argument forwarding untested, which is yarn behaviour rather than repo code. The README's Getting Started commands use--collectionand--out, neither of which collides with a yarn global flag. If you want it closed later, the way that stays inside the rules is a smoke invocation of the built CLI fromscript/build(run it with--versionand assert the output); note that even that does not exerciseyarn quak <command>forwarding itself.Verification I ran
make checkon head, clean treemake checkwithdist/presentgit statusclean afterwards, so it modifies nothingmake buildverifiedprinted for all three entrypoints, 1.8smake fmtprettier --checkgreenmake cleandist/completely, tree cleandocker build --no-cache -t …scoped to this imageWORKDIRwasCACHED;RUN make checkexecuted in 29.0s printing 19 / 216, andRUN make buildexecuted in 6.6s printing all threebuild: verifiedlines. Under the 5-minute policy cap69bd6d1check / check (push)successmainat348f23b; fast-forward, no conflictsI did not run
docker builder pruneordocker system prune. I removed the image I built and left the worktree clean; nothing was committed or pushed.Policy and hygiene
(closes #3). Two commits, tests first (red atd79ed83, verified), implementation second.TODO.mdand the README changes are in the implementation commit. Markdown is prettier-clean.make check's composition,engines,exports,prepublishOnly,script/cibuildandmake cleanare all untouched. The Dockerfile change is one additiveRUN make buildline, which is what the issue asked for and is minimal enough not to collide with #4.devDependenciesis untouched, so #27 was genuinely deferred and not partly done here.yarn buildnow routes throughscript/build, so the documented flow gets the same entrypoint verification asmake build.Nits, none blocking
.gitignorehas nobin/*.js,bin/*.d.tsorbin/*.mappattern. Defence in depth against the failure mode above, which is otherwise closed bynoEmitOnError.script/build:Object.values(pkg.bin ?? {})silently iterates characters ifbinis ever written in npm's legal string form. Atypeof pkg.bin === "string" ? [pkg.bin] : Object.values(…)normalization costs one line.script/build:statSync(declared)also succeeds for a directory, so an entrypoint pointing at one would "verify"..isFile()would tighten it.script/build: the embeddednode -eblock usesrequireand so depends on--evaldefaulting to CommonJS despite"type": "module". It works on this host and innode:22-alpine(I ran both), but--input-type=commonjswould make it explicit.test/packaging/does not mirrorsrc/the way the README says tests should. Defensible, since it tests the manifest rather than a module, but it is a deviation.tsconfig.jsonincludestill omitstest/**/*, so test sources are never type-checked bymake build, andscript/checkrunsprettier --checktwice (once insidescript/lint, once asscript/fmt-check).Conclusion
PASS. The change satisfies every point of the issue's definition of done, the one gap is disclosed accurately rather than overclaimed, and the single genuinely risky edit — the secretstream pull argument — is behaviour-preserving, which I established from libsodium's own behaviour rather than from the test suite passing. I would be comfortable with this merging to
main.Manager note: merged
Passed review on the first cycle with no blocking findings — the first PR in this repo to do so.
Merged fast-forward to
mainat69bd6d1; branch deleted. Issue #3 closed by the commit subject.Verified on
mainafter the merge, withgit worktree listchecked first so #25 could not inflatethe numbers:
make checkgreen at 19 files / 216 tests / 7.56s, andmake buildgreen forthe first time in this repository's history, printing
verifiedfor all three declaredentrypoints plus the shebang check. Working tree clean afterwards.
Why this one passed cleanly
Worth recording, because four previous cycles failed on the same thing. Every claim in the PR body
was reproducible, and the two that mattered most were reproduced by the reviewer independently
rather than accepted:
script/buildentrypoint guard genuinely fires — the reviewer pointedmainat a bogus pathand got the failure, rather than trusting the author's identical experiment.
noEmitOnErrorjustification was checked against reality: on348f23bthe broken build doesemit
bin/quak.{js,d.ts,…}into the source tree, andmake lintthen fails with exactly thetwelve
no-undeferrors claimed.The author also disclosed the one gap they could not close —
yarn quak <cmd>argumentforwarding, untestable without invoking yarn directly, which this repo forbids — instead of
quietly asserting it worked. That is the behaviour the previous four failures were trying to
produce.
The part that was not a config fix
Clearing TS6059 let
tscreach the program for the first time and surfaced two errors insrc/crypto/stream.tsthat no build had ever reached. That put a change into the cryptographiccore of a repo whose stated rule is "no hand-rolled crypto", so I directed the reviewer to treat
it as the primary risk rather than a footnote.
They settled it empirically rather than by observing that the tests still pass. The missing third
argument to
crypto_secretstream_xchacha20poly1305_pullisad(additional authenticated data),declared non-optional; the author now passes
null, matching thenullalready passed on the pushside in
encryptBlob. The reviewer probed real libsodium and confirmed thatpull(state, ct),…, null,…, undefined,…, new Uint8Array(0)and…, ""all return identical plaintext andtag=3, that…, "x"returns FALSE, and that against a chunk pushed with real AD both the 2-argand
nullforms fail identically. The wrapper normalises missing andnullto zero-length ADthrough the same guard the push side uses. Behaviour-preserving, established from the library
rather than inferred from green tests.
They also confirmed both crypto fixes were forced and minimal: on
348f23bwith onlyrootDirchanged,
make buildreports exactly those two errors and nothing else.What this unblocks
#5 (single-source the version), #6 (packaging metadata) and #13 (README API reference) were all
waiting on the emitted layout. The chosen layout deliberately keeps #5 possible — with
rootDiratthe repository root,
package.jsonresolves from bothsrc/under vitest anddist/after abuild.
mainnow has a real build gate:script/buildfails if a declared entrypoint is missing, and theDockerfile runs
make buildas well asmake check, so the class of error that produced thisissue cannot reach
mainagain.Deferred
#27 —
@types/libsodium-wrappers-sumo@0.8.2is a deprecated stub containing no.d.tsat all;the library ships its own types. Genuinely deferred, not partially done —
devDependenciesisuntouched on this branch. Now cheap to verify, since
make buildactually works.Seven non-blocking nits are on the PR; the substantive one is that
.gitignorehasbin/quakbutno pattern covering generated
bin/*.js.noEmitOnErrorcloses that path structurally, so it isdefence in depth rather than a live hole.
State
mainat69bd6d1.1.0.0milestone: 3 of 15 closed. Next up is #4 —make dockergreen, whichnow also carries the
script/cibuildcache defect that lets a build report a green it did notearn.