* feat(updater): manual cache clear + installer-reinstall floor on the installed outer version
Two disaster-recovery capabilities for the packaged updater:
- Reuse control.launcher.version.min properly: the gate now compares the
physically installed outer package version (read at check time from the
outer bundle's open-design-config.json via the launcher launch path;
OD_UPDATE_INSTALLED_VERSION overrides for tests), not the running payload
version. When the floor trips - or min is set but the outer version is
unreadable (conservative) - the updater selects the installer artifact and
offers a same-version installer reinstall when no newer release exists
(clamped to min <= candidate so the offer always converges). The status
snapshot carries a reinstall block {reason, installedVersion, minVersion,
url}; UpdateDialog/UpdaterPopup render reinstall copy with an optional
operator jump link. tools-release publish-metadata publishes the block from
RELEASE_LAUNCHER_VERSION_MIN(_URL) (per-channel repo vars with a _STABLE
fallback wired in all release workflows) and hard-fails when min exceeds
the release version; verify-metadata re-checks the published block and
summary-metadata surfaces the floor.
- Manual clear-cache action (od:update:clear-cache IPC, sidecar clear-cache
action, Settings > About row with a two-stage inline confirm): resets
one-shot update state (downloaded release, install freeze) back to idle,
purges releases/staging/downloads/.back, removes a stale launcher
attempt.json plus any non-confirmed desktop-handoff journal, and deletes
non-retained payload versions. Runtime active/lastSuccessful versions,
retained cleanup entries, install.json, and a confirmed handoff journal
are never touched; locked files defer through the existing cleanup.json
retry machinery.
Also: tools-pack --update-action now validates through sidecar-proto's
exported isDesktopUpdateAction so clear-cache reaches the ops surface;
tools-serve updater fixture gains control min/url knobs and CLI flags;
9 new i18n keys across all 19 locales; tools/pack/AGENTS.md documents the
gate semantics and clear-cache boundaries.
Validated locally per the updater matrix (desktop/web/tools-serve/tools-pack
suites, guard, typechecks) plus a mac release-beta closed loop: platform full
spec, same-version reinstall against a real outer/payload version split,
clear-cache recovery, and a real-feed payload update to 0.15.1-beta.4.
* test(updater): close Windows recovery acceptance
* test(updater): mirror Windows same-version reinstall recovery on the mac lane
Port the Windows runSameVersionUpdaterRecoveryAcceptance shape into the mac
full profile: after the payload cold-start acceptance, restart the packaged
app against a fixture publishing an installed-outer floor at the running
payload version and assert the same-version installer reinstall snapshot
(reason/installedVersion/minVersion/url), the popup reinstall learn-more
link, clear-cache back to idle with launcher pointers untouched, explicit
re-check recovery, and the installer open in dry-run mode — macOS has no
silent DMG install to execute, unlike the Windows NSIS transaction lane.
Extends the popup eval expression/validator with reinstallLinkVisible and
records an updaterRecovery block in the smoke summary. Full-profile test
timeout raised 240s -> 360s for the extra app restart.
Validated locally: release-smoke full profile green in 80s with the recovery
summary populated (reinstall block exact, cleared idle, recovered dmg,
dryRun install).
* refactor(release): centralize launcher version floor channel policy in one resolver
Channel policy for control.launcher.version.{min,url} is now managed as one
repo-vars pair per channel (RELEASE_LAUNCHER_VERSION_MIN[_URL]_<CHANNEL>).
Workflows pass the channel pair plus the STABLE pair through verbatim — the
|| fallback expressions are gone from YAML — and the shared resolver in
tools/release/src/storage/launcher-version-floor.ts owns resolution and
validation: pair-level stable fallback for non-stable channels (never mixing
one channel's min with another's url), version format, http(s) url, orphan
url rejection, and the floor <= releaseVersion guard. publish-metadata and
verify-metadata consume the same resolver, so verify re-derives the expected
block from the same channel policy instead of trusting a pre-resolved env.
release-workflows.test.ts pins the verbatim passthrough keys in all five
release lanes and rejects any reintroduced YAML fallback.
* feat(updater): let clear-cache rebuild a provably-owned corrupt update store
A corrupt store is one of the blocking scenarios the manual clear exists to
recover from, yet the action previously bailed on the very store errors it
should heal. clear-cache now rebuilds the update root when — and only when —
ownership is provable: the current-generation sentinel is the proof, and
everything else inside an owned root is updater cache by definition. Roots
without a sentinel, with a foreign-generation marker, or failing IO are never
touched; the original store error surfaces unchanged. Four specs pin the
boundary (corrupt metadata rebuilt, stray entries rebuilt, unowned root
refused with files intact, foreign-generation marker refused).
* fix(launcher): pre-arm delegated payload launches so pre-bookkeeping crashes roll back
A payload that spawned successfully but died before reaching its own launcher
bookkeeping left no attempt.json behind, so every later cold start re-selected
the same broken active pointer and re-delegated to the dead payload — a
permanent dead loop the installer fallback could never surface (found while
designing the crash-rollback acceptance).
The delegating parent now arms attempt.json BEFORE spawning the payload and
passes the pointer as --od-launcher-delegated-* argv. Selection gains an
active-delegated reason: an attempt matching active AND the delegated pointer
is the launch in progress, not a previous failure; only an exact active match
qualifies, so a stale pointer cannot defeat the rollback. Rollback
(last-successful) delegations deliberately carry no marker and never re-arm —
the attempt on disk IS the rollback evidence the child must re-derive.
Covered end to end: packaged outer delegation pre-arms (and skips pre-arm on
rollback), the updater's payload activation pre-arms and threads the pointer
through the deferred relaunch, and confirm clears the attempt for delegated
launches. launcher-proto 18, packaged 237, desktop 264 all green.
* fix(updater): scrub a stale payload install freeze after a rollback
A payload install freezes the updater with installResult.activeVersion
awaiting the relaunch. When that payload crashed and the launcher rolled
back, the app restarts on the OLD version with the freeze still persisted —
every later check became a frozen no-op, so a rolled-back install never saw
another update offer (the crash-rollback acceptance exposed this in its
self-heal phase). Cold-start restore now detects the broken promise —
installResult.activeVersion newer than the running version on a live process
— and drops the freeze and stale install result while keeping the verified
downloaded release actionable. The installer-artifact freeze (no
activeVersion) keeps its existing restore semantics. Red spec:
'clears a stale payload install freeze when the promised relaunch never
became the running version'.
* test(updater): full-lifecycle acceptance — silent startup, crash rollback, coverage map
Two new packaged acceptance tests on both platform lanes (mac validated
locally; win symmetric for the Windows box):
- 'applies a downloaded payload silently on the next cold start': enables the
daemon-owned allowSilentUpdates through the production GET+PUT
/api/app-config path, restarts, and asserts the first scheduler tick
installs, quits, and relaunches with converged pointers — no updater action
issued.
- 'rolls back a crashing payload and self-heals on the next good update':
repacks the real payload archive with a crash-before-bookkeeping stub
(mac: shebang script via ditto; win: a real executable — System32 where.exe
— via the installed app's bundled 7z, since a script cannot stand in for an
exe), drives the update through the popup, asserts the pre-armed attempt
survives the crash as rollback evidence, cold-starts into the rolled-back
base version (degraded steady state pinned: broken pointer stays active
with its attempt), then self-heals via a version-bumped healthy payload —
real recovery releases ship as version+1.
Spec hygiene: resetPackagedRuntimeState wipes launcher/runtime/daemon state
before each lifecycle test (uninstall keeps runtime data by design, and
leftover pointers silently delegate fresh installs onto stale payloads);
mac waitForHealthyDesktopVersion gains win's requireSettledLauncher opt-out
for rollback waits.
docs/testing/updater-lifecycle.md is the new lifecycle-to-test coverage map
(incl. deliberate manual-only nodes), linked from tools/pack/AGENTS.md and
the e2e coverage status doc.
* fix(updater): harden Windows full-lifecycle acceptance
* test(release): make channel-prepare hermetic against local git tags
prepare-preview derives its latest-stable floor from the clone's real
open-design-v* tags, so the channel-prepare tests were green on tagless CI
checkouts but permanently red on any developer clone once shipped stable
tags advanced past main's package version (0.15.1 vs v0.15.1/v0.16.0 today).
The spawned scripts now run with GIT_DIR pointing at a throwaway repo
holding exactly the tags each test declares, and a new negative case pins
the floor rejection itself (packaged version below a v99.0.0 stable tag).
* test(e2e): model delegated payload launch
Pass the updater's pre-armed payload pointer into the cross-runtime launcher simulation so it represents the spawned payload process instead of a later crash-recovery launch.
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* test(web): wait for question form readiness
Wait for the asynchronously derived submit-disabled state before asserting it, removing the suite-order race observed by the Web workspace tests check.
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
* fix(updater): preserve recovery state across locks and restarts
Generated-By: looper 0.11.0 (runner=fixer, agent=codex)
---------
Co-authored-by: Looper <looper@noreply.github.com>
18 KiB
tools/pack
Follow the root AGENTS.md and tools/AGENTS.md first. This tool owns the repo-external packaged build/start/stop/logs command surface.
Owns
- Local packaging orchestration for packaged Open Design artifacts.
- mac build/install/start/stop/logs/uninstall/cleanup smoke commands.
- Windows NSIS build/install/start/stop/logs/uninstall/cleanup/list/reset smoke commands.
- Windows registry observation/cleanup must go through
reg.exeand stay scoped to entries matching the namespace install/uninstaller paths. - Windows lifecycle logs must expose NSIS automation logs/markers/timings in addition to app runtime logs.
- Linux AppImage build/install/start/stop/logs/uninstall/cleanup smoke commands.
- Linux headless (no-Electron) install/start/stop via
--headlessflag oninstall,start, andstop. - Linux containerized builds via
electronuserland/builderDocker image for distro-agnostic glibc compat. - Consuming sidecar/process/path primitives from
@open-design/sidecar-proto,@open-design/sidecar, and@open-design/platform.
Does not own
- Product business logic.
- Sidecar protocol definitions.
- A second process identity model.
- Product/business update runtime integration.
Rules
- Do not hand-build
--od-stamp-*args; usecreateProcessStampArgswithOPEN_DESIGN_SIDECAR_CONTRACT. - Do not use port numbers in data/log/runtime/cache path decisions. Namespace decides paths; ports are only transient transports.
- Public release artifacts must use channel-specific app identity: stable uses
Open Design, beta usesOpen Design Beta, prerelease usesOpen Design Prerelease, and preview usesOpen Design Preview. Local tools-pack installs may still use namespace-scoped install paths only as a developer multi-instance validation convention. - Do not let namespace-named
.appinstalls change data/log/runtime/cache path conventions. --dircontrols tools-pack output/runtime/install validation roots only. It must not be treated as the cache root. The default workspace tools-pack cache is the hot path.--cache-diris a special-case escape hatch for cache isolation or cold-cache validation, not a routine QA/build parameter.- Use
--portablefor public/release artifacts so packaged config does not bake local tools-pack runtime roots from the build machine. - Pack resource files used by electron-builder belong under
tools/pack/resources/; do not point pack logic at Downloads, web public assets, docs assets, or other app-owned resource paths. - For ordinary Windows NSIS smoke tests, use short namespaces such as
rg,smoke, ornsis-a. NSIS extracts deeply nested Next.js standalone files under the namespace-scoped install directory; long namespaces can push installed paths past the traditional Windows 260-character limit even when builderwin-unpackedoutput is correct. During merge regression, namespaceregression-merge-nsisproduced an installed path length of 264 characters and missednext/dist/server/route-matcher-providers/helpers/cached-route-matcher-provider.jsin the installed directory, while the same NSIS smoke passed with namespacerg. Use long namespaces only when intentionally testing installer path-length behavior.
Packaged auto-update architecture and harness
Read this section before changing packaged auto-update behavior. The updater crosses package, desktop, web UI, release-feed, and installer surfaces, so bugs often hide between otherwise-green package tests.
Architecture map
apps/desktop/src/main/updater.tsowns updater state, release metadata parsing, artifact selection, checksum verification, download-store ownership, progress events, and opening the downloaded installer. It is pure main-process logic and is tested underapps/desktop/tests/main/updater.test.ts.apps/desktop/src/main/runtime.tsexposes updater IPC to the renderer throughod:update:status|check|download|install|quitand emitsod:update:status-changed. Keep installer launch separate from process shutdown; quit is an explicit post-installer action.apps/desktop/src/main/index.tswires the scheduler and the packaged macOS app-menu update item. The native item mirrors updater state and opens the renderer-owned update dialog; it must not create a second updater or a native result dialog. Windows and Linux menus do not expose update actions.apps/web/src/lib/updater.tsnormalizes host updater snapshots into UI-ready state.apps/web/src/components/UpdaterPopup.tsxremains the ready-update surface in the left rail.apps/web/src/components/UpdateDialog.tsxowns the explicit macOS app-menu check flow. All visible copy and native menu labels must go throughapps/web/src/i18n.packages/launcher-protoowns launcher pointer, attempt, and desktop-handoff journal shapes plus payload selection.runtime.jsontogether withattempt.jsonis the only payload-version state machine.apps/packaged/src/index.tsdelegates to the selected payload desktop before initializing the outer Electron runtime, then passes packagedappVersionand namespace-scopedupdateRootinto desktop main only when the outer itself must run.apps/daemon/src/sidecar/payload-desktop-handoff.tsis the isolated compatibility bridge for historical outers that cannot delegate. It rearms the selected payload with the real previous pointer, launches that payload's desktop after the old outer exits, and persists a small desktop-binding journal for later shortcut cold starts. The journal is not a second version selector.install.jsoncontinues to identify the physically installed outer executable for recovery. Payload activation or handoff must not rewrite it to a versioned payload executable.tools/serveowns deterministic local updater fixtures only. It must not contain product updater runtime logic.tools/packowns packaged build/install/start/inspect/logs/uninstall/cleanup and the platform installer harness, including Windows NSIS registry observation and cleanup.
Release metadata shape
The runtime updater reads https://releases.open-design.ai/<channel>/latest/metadata.json unless OD_UPDATE_METADATA_URL overrides it. For package-launcher updates:
- A valid packaged-launcher context prefers
platforms.<platform>.artifacts.payload; the platform installer (dmgon macOS orinstalleron Windows) remains the recovery/fallback path. - The artifact must have a checksum, preferably
sha256Url; the updater verifies bytes before exposing an install action. OD_UPDATE_CURRENT_VERSIONmay override the packaged version for tests, but user-flow package validation should prefer building the package with the intended--app-version.- Release metadata may include
releaseNote.content, with adefaultLocaleand locale descriptors containingurl,mediaType,sha256, andsize. The updater does not currently consume this block;tools-releaseowns its publication and verification independently from updater UI behavior. - Release metadata may include
control.launcher.version.{min, url}— the installer-reinstall floor. The updater comparesminagainst the physically installed outer package version (read at check time from the outer bundle'sopen-design-config.jsonvia the launcher launch path;OD_UPDATE_INSTALLED_VERSIONoverrides it for tests), NOT the running payload version — payload updates never touch the outer bundle, so a broken outer generation must reach the installer path even when its payload is current. When the floor trips (orminis set but the outer version is unreadable — conservative), the updater selects the installer artifact and, when no newer release exists, offers a same-version installer reinstall; snapshot fieldreinstallcarries{reason, installedVersion, minVersion, url}and the web UI presents the optional operatorurlas a jump link with default i18n copy as fallback. Publication: channel policy is managed as one repo-vars pair per channel —RELEASE_LAUNCHER_VERSION_MIN_<CHANNEL>+RELEASE_LAUNCHER_VERSION_MIN_URL_<CHANNEL>— passed through workflows verbatim (no YAML fallback expressions) and resolved by the shared resolver intools/release/src/storage/launcher-version-floor.ts: a non-stable channel whose own pair is unset falls back to the STABLE pair as a unit, and format/https/floor validation is applied at that single point.publish-metadatahard-fails whenminexceeds the release version — a floor this release cannot satisfy would make the same-version reinstall offer nag forever.verify-metadatare-resolves the same channel policy and checks the published block against it;summary-metadatasurfaces the floor in the step summary. - The updater exposes a manual disaster-recovery
clear-cacheaction (od:update:clear-cacheIPC, sidecar actionclear-cache, Settings → About "Clear update cache" row with a two-stage inline confirm). It resets one-shot update state (downloaded release, install freeze) back toidle, purgesreleases/,staging/,downloads/, and.back/, removes a stale launcherattempt.jsonplus any non-confirmeddesktop-handoff journal, and deletes non-retained launcher payload versions. Runtimeactive/lastSuccessfulversions, explicitretainedcleanup entries,install.json, and aconfirmedhandoff journal are never touched; locked files defer through the existing cleanup.json retry machinery. An installer helper already spawned by a prior install action is not cancelled. - Release-note source lives at
docs/CHANGELOG/v<full-releaseVersion>/<locale>.md. All channels use the same publication pipeline, while stable additionally requiresenandzh-CNbefore platform builds proceed. - Post-update "what's new" highlights are NOT carried in release
metadata.json. The daemon's/api/whats-newfetches a single hand-curated document on a dedicated R2 bucket (https://whatsnew.open-design.ai/whats-new.json, overridable withOD_WHATS_NEW_URL); the web home surface shows a one-time card driven by that document'sid, not the running version. Operators edit that one file after a release — there is no per-version publish tooling.
Channel identity rules
Channel identity must be stable across install, update install, shortcuts, registry entries, and app data:
- Stable:
Open Design, namespacedefaultor stable release namespace. - Beta Windows:
Open Design Beta, namespacerelease-beta-win, uninstall keyOpen Design-release-beta-win. - Prerelease Windows:
Open Design Prerelease, namespacerelease-prerelease-win, uninstall keyOpen Design-release-prerelease-win. - Preview Windows:
Open Design Preview, namespacerelease-preview-win, uninstall keyOpen Design-release-preview-win. - Beta-like ad hoc namespaces such as
beta-local-floware test namespaces, not the beta channel. They must not be used for user-flow beta validation because they create a different registry key while sharing a confusing display name/path.
If a local release-channel package is meant to be updated by a real feed, build it with the matching release namespace and an older matching --app-version such as --namespace release-beta-win --app-version 0.10.0-beta.1 or --namespace release-prerelease-win --app-version 0.10.0-prerelease.1. Otherwise the installed package and the downloaded package can appear as separate registry entries even though they target the same display name.
Deterministic fixture harness
Use tools-serve start updater for fast, deterministic tests and e2e automation where network release state is not the thing under test. Fixture flow:
pnpm tools-serve start updater --json --channel beta --version 99.0.0-beta.1 --platform win
Then launch packaged desktop with:
OD_UPDATE_ENABLED=1
OD_UPDATE_METADATA_URL=<fixture metadataUrl>
OD_UPDATE_CURRENT_VERSION=99.0.0-beta.0
OD_UPDATE_OPEN_DRY_RUN=1
OD_UPDATE_AUTO_CHECK=1
This harness is appropriate for asserting IPC, popup rendering, progress, checksum/download-store behavior, and dry-run installer opening. It is not a full user-view validation because it replaces the public release feed and uses synthetic artifact bytes.
High-confidence local user-flow acceptance
Use this when validating release-channel behavior before handing a Windows beta build to a human tester. This path intentionally avoids mock services and exercises the selected real beta feed. For the self-hosted release-beta-s lane, the real feed is the Nexu S3 origin configured by release_public_origin, currently https://s3.nexu.space/od-releases.
- Confirm the latest beta metadata first:
curl.exe --ssl-no-revoke -fsSL https://releases.open-design.ai/beta/latest/metadata.json
For release-beta-s, check the internal feed instead:
curl.exe --ssl-no-revoke -fsSL https://s3.nexu.space/od-releases/beta/latest/metadata.json
- Build a non-portable Windows beta package with the real beta namespace and a version lower than latest:
pnpm tools-pack win build --dir C:\odtp-beta-release-fixed --namespace release-beta-win --to nsis --app-version 0.8.0-beta.5 --json
- Give the tester the generated installer:
C:\odtp-beta-release-fixed\out\win\namespaces\release-beta-win\builder\Open Design-release-beta-win-setup.exe
- Expected user flow:
- User installs
0.8.0-beta.5through the NSIS UI. - User launches
Open Design Beta. - App auto-checks the real beta feed and selects the latest Windows launcher payload when the package-launcher context is valid. The installer is the fallback path when the payload artifact or launcher context is unavailable.
- For the payload path, the app downloads
platforms.win.artifacts.payload, verifies sha256, prepares the payload under%APPDATA%\Open Design\launcher\channels\beta\namespaces\release-beta-win\versions\<version>\payload, and shows the web updater popup. - The native Windows File menu must not expose update actions. On macOS, the app menu exposes the state-aware update item and opens the renderer update dialog without making background checks intrusive.
- The updater popup uses i18n strings and download progress must not flash to 100% before real bytes arrive.
- Applying the payload update should quit and relaunch the exact executable under the prepared version's
payloaddirectory, then mark launcheractiveandlastSuccessfulto that version and clearattempt.json. - A historical outer may first create a mixed generation. Its daemon-sidecar compatibility handoff must replace the historical desktop with the exact payload desktop executable, preserve the true previous pointer for recovery, and leave the handoff journal either absent or
confirmed—never stranded inpreparedorarmed. - After a full stop, launching the installed shortcut/outer again must still converge on the same active payload desktop and preserve daemon/API behavior, including a real PPTX export.
- If the updater falls back to the installer path, clicking
Open installeropens the real downloaded beta installer. Installing it should overwrite the sameOpen Design-release-beta-winregistry key, not create a second beta key.
- Registry and launcher sanity check after beta.6 update:
Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -like 'Open Design*' } |
Select-Object PSChildName,DisplayName,DisplayVersion,InstallLocation
Get-Content "$env:APPDATA\Open Design\launcher\channels\beta\namespaces\release-beta-win\runtime.json"
For a clean beta channel result, expect one beta entry with PSChildName Open Design-release-beta-win and the latest DisplayVersion.
For the payload path, also expect launcher active.version and lastSuccessful.version to match the latest beta version, attempt.json to be absent, and the running desktop executable to resolve below that version's payload directory. desktop-handoff.json may be absent for a current outer or confirmed for a historical outer; prepared and armed are not successful terminal states.
Windows Settings > Apps may cache uninstall metadata within the current view. If Settings still shows the previous beta version after the registry query is correct, switch away from the Apps view and back, or reopen Settings, before treating it as an installer failure. The registry query above is the source of truth for this harness.
- Avoid leaving validation residue. Stop running app processes first, then use tools-pack uninstall/cleanup for tool-managed namespaces. Only delete explicit temp roots after verifying the resolved path is exactly the intended directory.
--diris an output/runtime root, not the default cache root. Do not add--cache-dirto routine validation; it is an escape hatch for cache isolation or cold-cache validation only.
pnpm tools-pack win stop --dir C:\odtp-beta-release-fixed --namespace release-beta-win --json
pnpm tools-pack win uninstall --dir C:\odtp-beta-release-fixed --namespace release-beta-win --remove-product-user-data --remove-data --remove-logs --remove-sidecars --json
pnpm tools-pack win cleanup --dir C:\odtp-beta-release-fixed --namespace release-beta-win --remove-product-user-data --remove-data --remove-logs --remove-sidecars --json
Validation matrix for updater changes
docs/testing/updater-lifecycle.md is the full lifecycle-to-test coverage map (including deliberate manual-only nodes); consult it to find the owning tests for the node you touched, then run the narrow tests plus the repo checks:
pnpm --filter @open-design/desktop test -- tests/main/updater.test.ts tests/main/updater-host-boundary.test.ts tests/main/preload-host-boundary.test.ts
pnpm --filter @open-design/web test -- tests/components/UpdaterPopup.test.tsx tests/lib/updater.test.ts
pnpm --filter @open-design/tools-serve test
pnpm --filter @open-design/tools-pack test -- tests/win-identity.test.ts tests/win-app.test.ts tests/win-builder.test.ts
pnpm --filter @open-design/desktop typecheck
pnpm --filter @open-design/web typecheck
pnpm --filter @open-design/tools-pack typecheck
pnpm --filter @open-design/tools-serve typecheck
git diff --check
pnpm guard
pnpm typecheck
Run the high-confidence local user-flow acceptance whenever a change touches real release feed selection, channel identity, Windows registry/install behavior, installer opening, or visible updater UI behavior.
For launcher payload or handoff changes, also run the platform full spec. The full profile must validate exact desktop executable identity, a real PPTX response, a complete stop followed by an installed-outer cold start, and the same checks again after restart. Windows beta full validation must use release-beta-win; a beta-like local namespace is not equivalent delivery evidence.