A green build is not an installed browser
Voyager's house sign repeats one line in several places: verify from /Applications/Voyager.app, never from swift build. A passing compile says the code type-checks. What Dad uses is the bundle in Applications, signed, with its icon, its version and his session. So scripts/build-app.sh treats build, bundle, sign, verify and install as one transaction. Either all of it happens, or Dad's installed copy is left as it was.
Stage, sign, verify
- Refuse early. The version in
cwk-product.jsonmust parse (Voyager is pre-1.0), the signing identity must be installed, and the icon must exist, all before a compile starts. - Build and number. A release build, and a build number equal to the repository's commit count, so every bundle says how much history it was built from.
- Stage. The bundle is assembled in a temporary folder under
.build: the binary, Info.plist, the icon, a copy ofcwk-product.json, and Pippa's mark rendered from the kit at build time, because the repository keeps no copy of it. - Sign and read back. Sign with the identity from the last lesson, verify deeply and strictly, then check the authority, the runtime and the designated requirement. Only a bundle that passes is copied to
build/.
Interrupts are routed through the exit trap, so a build stopped with Control-C still removes its staging folder, and every delete the script performs is checked against the few paths it owns.
Installing over a running browser
Dad's copy is usually open. The install copies the verified bundle into /Applications under a hidden name stamped with the script's process id and verifies that copy too. Then it asks the running Voyager to quit with SIGTERM (first block). Voyager saves its session on SIGTERM exactly as it does when Dad quits from the menu, so his tabs come back. The script waits up to ten seconds. If Voyager is still running, perhaps because a page holds a dialog open, the script stops and says so. Nothing is killed.
Once Voyager has quit, the old bundle is moved aside, the new one is moved into place and verified where it stands. Until that verification passes, an exit trap stands ready to put the previous bundle back. Telling Launch Services comes after it and is best effort: a failed registration is ignored, the install counts as done, and the previous bundle is deleted. An install killed so hard that its trap never ran leaves pid-stamped leftovers, and the next run removes the ones whose process is dead.
The doctor reads, never writes
scripts/doctor.sh answers what is built, what is installed and what Voyager holds, without changing any of it. It reports the product version and each bundle's own numbers apart, because they answer different questions. It names the signing identity, then the system WebKit version: the engine is the system's, and Voyager reports it and never pins it. For the built and the installed app it prints version, build, signature state, authority, runtime, requirement and the executable's hash. For Voyager's folder it prints only counts: tabs in the saved session, identities, departures recorded and how many have no reason. Any problem ends in VOYAGER_DOCTOR_FAIL with the count.
Checks that cannot touch Dad's state
A smoke test that runs the real app writes into the real app's folder, Keychain and Pippa. Voyager closes that door by identity, with the kit's isolation gate (second block). build-app.sh --verification builds a separate bundle, com.cwk.voyager.verify, with no URL or document types, so macOS never offers it as a browser. Only that bundle may honour --isolated. The shipping app asked to run isolated prints the refusal and exits, because a check that quietly fell back to the real folder is the exact accident the gate exists to prevent. The overrides a check sets, such as a stand-in Pippa's address, read as empty outside isolation, and inside it Pippa's address must be loopback.
An isolated run lives in a throwaway container, starts from settings that reach nothing of the family's (no catalog, a blank new tab, page zoom 1), and keeps saved logins in a memory vault instead of the Keychain.
smoke.sh walks the app twice
scripts/smoke.sh builds the verification app and starts a stand-in Pippa on a loopback port: a small server whose panel asks which page is in front and writes down what it is told, plus a test page with a password field and a sign-in form. It launches the app twice (third block), once to use it and once to see the session come back, each run limited to 60 seconds. Its 58 check lines cover the window and tabs, find, zoom, bookmarks, pinning and drags, Incognito, Home, the mouse's side buttons, keys after a tab switch, the settings window, the panel told about the page without the password's value, a login saved and filled back, a companion source shared and cleared, Pippa herself as a pinned tab, and a restore with only the front tab awake. Then it deletes the container, the verification build's WebKit data and its preferences.