# PRD: AV-Safe Build Path (Opt-In / Fallback EXE Backend)

**Status:** Draft v5 -- added explicit PATH/VIRTUAL_ENV cleanliness requirement for Tier B; confidence held at Yellow against a third-party Green recommendation, with reasoning documented. Two previously-open engineering/product questions resolved below by a repo agent with direct codebase access (see "Notes from Claude" at the end). Re-checked 2026-07-20 against Nuitka's live changelog/issue tracker (downtime research, no code written) -- Finding 1's core MinGW64/3.13+ blocker still holds, but a narrower Zig-fallback data point was updated in place; see the inline "Re-checked 2026-07-20" note under Finding 1. **Priority: implementation started 2026-07-20 (owner green light) -- Phase 1 requirements 1-4 SHIPPED (failure-simulation tests, dispatch, and Tier A itself -- Nuitka fallback build in the existing environment, no reprovisioning). Requirement 9 (P1, the elective "want an optimized build too?" upsell) is ALSO SHIPPED (2026-07-21). Tier A's exact Nuitka CLI flags are confirmed correct against a real Windows CI run (2026-07-20) -- see requirement 4's status note. Requirement 5 (Tier B reprovisioning) is EXPLICITLY DEFERRED by direct owner instruction (2026-07-21): "Until I run the bootstrapper myself and have real problems, I don't want to do the reprovision rollback." Requirements 6/8 depend on Tier B and are deferred with it -- see requirement 5's own status note and CLAUDE.md's Active Backlog item 6.**
**Owner:** Supervisor (Python_vs_Windows)
**Related:** Competitive Brief -- Python_vs_Windows (July 2026), PyInstaller AV false-positive research

---

## Research Findings (Spike Results)

These findings resolve the two blocking open questions from Draft v1 and materially change P0/P1 scope. Treat this section as the evidence base for the requirements below.

### Finding 1 -- Nuitka's MinGW64 auto-download does not work on Python 3.13+, AND the fallback must run in the same environment as the installed packages (corrected in v3, refined into a two-tier design in v4)

Nuitka's own current documentation states plainly that its auto-downloaded MinGW64 compiler does not work with Python 3.13 or higher. An open GitHub issue against Nuitka confirms the practical failure mode: attempting a non-MSVC build on Python 3.13 produces a fatal error telling the user to either use Python 3.12 or fall back to `--msvc=latest`, with a note that MinGW64 support for 3.13+ is expected to return in a future Nuitka release but had not landed as of this research. A separate, still-open tracking issue ("Restore MinGW64 compiler support for Python 3.13 and later") confirms this is a known, unresolved gap, not a one-off report.

**Why this matters here:** a fresh bootstrap in mid-2026 will very likely resolve to Python 3.13 or 3.14 by default. Without a mitigation, the Nuitka fallback would silently require MSVC/Visual Studio on first trigger -- a large install that is not present on a clean machine and directly violates the Prime Directive's zero-prerequisite promise.

**v2 mitigation was incomplete and has been corrected.** The v2 draft specified spinning up an isolated Python 3.12 interpreter via `uv python install 3.12` for the fallback build. This was wrong in an important way: Nuitka, like PyInstaller, must run under the specific interpreter where the target script's third-party packages are actually installed -- it introspects the live environment to resolve imports and locate compiled extensions. Nuitka's own documentation is explicit about this (it recommends invoking it as `python -m nuitka` specifically to guarantee the correct interpreter is used, and warns that missing dependencies in the compiling environment will produce a compiled program that "will equally not run"). A bare, empty Python 3.12 interpreter with nothing installed would fail immediately with missing-import errors -- worse than the AV block it was meant to fix.

**Corrected mitigation, now restructured as two tiers instead of always reprovisioning.** Nuitka's own documentation confirms it checks for an installed Visual Studio 2022+ (MSVC) automatically and uses it by default if present -- and MSVC does not have the Python 3.13+ restriction that the auto-downloaded MinGW64 path has (Nuitka's own error message for the 3.13+/MinGW64 case literally suggests `--msvc=latest` as the workaround, confirming MSVC works fine on 3.13+). This means the expensive reprovisioning path isn't always necessary -- it's only needed on a genuinely clean machine with no compiler at all.

Per direction: don't build a separate, independent "is MSVC installed" check -- that kind of detection (registry probing, path guessing) is exactly the kind of fragile fingerprinting Finding 2 already argued against, and a stale or partial detection could produce a false "yes" that then fails inside Nuitka anyway. Instead:

- **Tier A:** attempt Nuitka directly inside the *existing* environment already used for the PyInstaller build (same Python version, same installed packages -- no reprovisioning at all). Let Nuitka do its own internal compiler discovery (MSVC first, then its MinGW64 auto-download attempt). This is the cheap path: if the machine happens to have MSVC, or if the main environment's Python is already <=3.12, this succeeds with no extra environment work.
- **Tier B (only if Tier A fails):** fall back to the reprovisioned, pinned-3.12, dependency-reinstalled environment described above. This tier absorbs the "clean machine, no MSVC, Python 3.13+" case specifically.

This is strictly cheaper than always reprovisioning, and doesn't require Python_vs_Windows to guess anything about the machine's compiler state up front -- it just tries the fast path and reads Nuitka's own failure signal if that doesn't work.

**Explicit limitation to document, not hide:** this only works for scripts that don't rely on Python 3.13+-only language features. That's an acceptable trade for a fallback path -- it doesn't need to be perfect, it needs to beat "the app doesn't run at all." This mirrors an existing, proven pattern already in this codebase: `pipreqs` is already pinned to an older Python version for the same class of reason (its own dependencies don't yet support the newest Python). This isn't a novel architectural risk -- it's the same shape of problem the bootstrapper has already solved once.

**Scoped philosophy resolution, not a global tradeoff.** There's a real tension between "always fetch the latest Python so user scripts can use the newest features" and "pin to a known-stable version so orchestration tooling doesn't break" -- but this PRD doesn't need to resolve that tension globally. The main bootstrap path keeps fetching the latest/appropriate Python for the user's script, unchanged. Only the Nuitka fallback specifically -- a narrow, self-contained branch -- pins to 3.12, because that constraint belongs to Nuitka itself, not to the tool's general philosophy. No global architecture decision is required here.

> **Repo-agent note (see "Notes from Claude" at the end of this document):** the owner pushed back hard on the idea of this scoped resolution ever generalizing into a bootstrapper-wide "always lag behind latest by a version or two" default. That pushback is recorded in full at the end of this file, including the specific worst-case scenario the owner wants any future reader of this PRD to weigh before extending Tier B's pinning logic anywhere else.

The Zig backend was also investigated as an alternative to MinGW64. It's not a reliable substitute right now -- Windows support is currently limited to x64 only, and there are recent open crash reports against `--zig`, suggesting it isn't mature enough to be the primary fallback compiler today. Worth re-checking in 6 months; not worth building around now.

> **Re-checked 2026-07-20 (downtime research pass, no code changes made):** confirmed via Nuitka's
> own changelog and GitHub issue tracker that the MinGW64/Python 3.13+ restriction Finding 1 cites
> is still unresolved as of Nuitka 4.1.2 (the latest release, June 2026) -- the "Restore MinGW64
> compiler support for Python 3.13 and later" tracking issue (Nuitka's own GitHub issue #3654)
> remains open, and the changelog itself notes the fix "was supposed to appear in Nuitka 2.8, but
> unfortunately it didn't happen." No change to Tier A/
> Tier B's design is warranted from this alone -- the core assumption still holds.
>
> **One narrower, genuinely new data point worth flagging for whoever picks this PRD up: Zig may be
> a more viable *automatic* fallback than Finding 1 assumed, specifically for Tier A.** Nuitka's
> changelog confirms a real bug fix in 4.0.4 -- "compiling with newer Python versions did not fall
> back to Zig when [MSVC/MinGW64] was unusable" -- meaning Nuitka's own internal compiler-discovery
> chain (which Tier A already relies on entirely, by design, per requirement 4's "let Nuitka do its
> own internal compiler discovery... do not build an independent detection check") may now
> correctly land on Zig automatically on a clean Windows x64 machine with no MSVC, rather than
> failing outright and forcing the more expensive Tier B reprovisioning. Separately, the specific
> "--zig crashes" issue that most recently surfaced in a search (issue #3725, opened January 2026)
> turned out to be scoped to macOS on Apple Silicon (M3), not Windows -- so Finding 1's "recent open
> crash reports against --zig" citation should be re-verified for Windows-x64 specificity before
> being repeated as a blanket maturity concern; it may not generalize to this project's actual
> target platform as cleanly as originally stated. **This does not change any requirement or
> acceptance criterion above** -- Tier A's design already defers entirely to Nuitka's own compiler
> discovery and doesn't need to know or care whether that lands on MSVC, MinGW64, or Zig
> specifically -- but it's a reason for cautious optimism that Tier A's real-world success rate
> (see the Success Metrics section's "% of simulated fallback runs that resolve via Tier A alone"
> leading indicator) may be higher than Finding 1's original framing implied. Not independently
> verified end-to-end (no Windows machine available in this research pass); flagged for
> confirmation whenever Phase 1 is actually implemented and its CI failure-simulation tests
> (requirement 1) can observe this for real.

### Finding 2 -- AV interference is hard to fingerprint precisely, so don't try

Windows Defender and third-party AV typically lock a file with an access-denied state before any deletion occurs, which is indistinguishable from an ordinary file-lock error caused by something else entirely. Trying to precisely determine "this specific error means AV" is fragile and not worth the engineering cost. **Adopted simplification:** treat any file-access error or missing-output-file condition in the build output directory as a single fallback trigger category, without trying to attribute a specific cause.

### Finding 3 -- the "interpreter wrapper" alternative is real but changes the deliverable shape, not adopted as default in v1

A tiny native launcher that shells out to a portable copy of uv (running the raw script directly in a venv, skipping compiled-executable packaging entirely) genuinely sidesteps AV heuristics, because there's no self-extracting-archive-to-temp-dir pattern at all -- that pattern is what AV heuristics actually react to. This is a legitimate, lower-risk idea. **Not adopted as the v1 default**, and now more firmly deprioritized: for an audience of non-technical users, a multi-file folder deliverable is a real usability hazard, not just a distribution-model footnote -- users reliably copy only the `.exe` off a desktop and separate it from its dependency folder, breaking the app. Single-file output should be protected as the default for local runs unless the AV problem becomes severe enough to outweigh that risk. Remains a P2 candidate, not a v1 direction.

### Finding 4 -- distribution is a nice-to-have, not a hard requirement, and needs its own small, separate treatment

The original purpose of producing a compiled `.exe` at all was distribution ("share this with a friend so they don't have to go through setup") -- that goal got obscured over time as the compiled exe also became the primary local-run vehicle for working around non-idempotent setup issues. Worth restating explicitly: **distribution is a nice-to-have, not a requirement this PRD needs to guarantee.** That reframing simplifies scope considerably:

- **Post-run debrief message.** After a successful build (PyInstaller or Nuitka), if the output is a single portable file, tell the user in plain language how to share it with someone else. If the output is folder-based (e.g., Nuitka `--standalone` mode), see Finding 5 below -- the tool packages that automatically rather than asking the user to.
- **Zip-extraction guard, for folder-based deliverables only.** When a user double-clicks a zip file on Windows, Explorer often opens it as a browsable view while the contents are still running from a temporary, non-extracted location -- if the user launches an app from inside that view, relative paths to sibling dependency files break. Any folder-based deliverable this project produces should check, at launch, whether it's running from such a temporary/unextracted location and -- if so -- show a plain-language message telling the user to click "Extract All" first, rather than failing with a confusing path error. This does not apply to single-file `.exe` output, which has no sibling files to lose.

### Finding 5 -- the tool should zip folder-based deliverables itself, not ask the user to

Asking a non-technical user to manually zip a folder is a real point of failure -- they may not do it correctly, or may end up with a second folder that looks identical to the first (a classic "is this the right one?" moment with no distinguishing signal). **Corrected direction:** when the deliverable is folder-based, the bootstrapper creates the zip archive itself, with a distinct, purpose-signaling filename (e.g. `<AppName>_ready_to_share.zip`, not a generic name that could be confused with the source folder). The post-run debrief message then simply says "send them this file" -- the same message shape as the single-file case, just pointing at a zip instead of an `.exe`. The zip-extraction guard (above) still matters on the *recipient's* end, since they're the one who'll eventually double-click it.

### Finding 6 -- the restart-based fallback needs explicit loop avoidance, and must never require the user to manually re-run anything

If Tier B (reprovisioned 3.12 environment) is implemented via a process restart, the tool must track, internally, whether the current run is already a forced fallback re-entry -- distinct from a `runtime.txt` the user authored themselves before the bootstrapper ever ran. Conflating those two would either (a) cause the tool to treat a user's own version pin as a fallback marker and behave unexpectedly, or (b) risk re-triggering fallback logic recursively if a marker isn't checked. **Adopted design:** use an internal-only marker (e.g., an environment variable or a dedicated marker file distinct from `runtime.txt`, such as `.pvw_fallback_state`) that the bootstrapper sets immediately before a Tier B restart and checks immediately on startup. If that marker is already present when a failure occurs, the tool stops with a clear final failure message instead of attempting another fallback -- there is no Tier C. And critically: if a restart is used at all, it must be the *tool* re-invoking itself automatically (e.g., the `.bat` re-launching itself with the marker set), never a "please double-click this again" instruction to the user -- the zero-terminal, one-double-click promise applies across the whole fallback sequence, not just the first attempt.

**Related risk if a restart is used: stale `PATH`/`VIRTUAL_ENV` from the main run.** A restarted process inherits its parent's environment variables by default. If the main run's `VIRTUAL_ENV` and `PATH` (pointing at the 3.13+ environment) aren't explicitly cleared or overridden before the restart, the provider-tier chain in the restarted process could resolve back to the already-active 3.13+ interpreter instead of the pinned 3.12 one -- silently defeating the entire point of Tier B. This needs to be handled explicitly (clear/override those variables before restart, or reference the 3.12 interpreter by explicit resolved path rather than relying on `PATH` lookup), not assumed to work by default.

## Problem Statement

PyInstaller-built executables are increasingly flagged and quarantined by antivirus engines and Windows Defender heuristics -- a known, worsening industry problem in 2026, serious enough that competing commercial products now market themselves specifically around eliminating it. Python_vs_Windows's Prime Directive promises a non-technical user a working double-click EXE from a raw `.py` file. If the produced EXE gets quarantined, the entire tool looks broken to that user -- even though the root cause is upstream in how PyInstaller packages executables, not in Python_vs_Windows's own logic. This risk has not yet caused a real reported failure, but it is anticipated and worth getting ahead of.

## Goals

1. When PyInstaller build fails for any reason -- including suspected AV interference -- automatically attempt a fallback build via Nuitka before declaring the Prime Directive failed, trying the cheapest option first (Tier A: existing environment) before the more expensive one (Tier B: reprovisioned pinned environment).
2. When PyInstaller succeeds normally, offer the user a plain-language, opt-in choice to also build an optimized version -- framed entirely around launch reliability and execution speed, never security or malicious intent (that framing belongs in this document and internal logs only, never in user-facing copy).
3. Reduce false-positive AV quarantine risk where feasible through packaging-technique changes, without adding paid or proprietary third-party dependencies.
4. Preserve the zero-terminal, double-click simplicity for the end user -- all of this stays hidden behind at most one plain-language prompt, and any internal restart between tiers is handled automatically by the tool, never surfaced as a "please run this again" instruction.

## Non-Goals

- **Code signing automation** -- out of scope. Requires a paid certificate ($200-500/yr) and the user's own business/identity info; a separate initiative if pursued at all.
- **Guaranteeing zero AV flags** -- not achievable. Different AV vendors use different heuristics; this feature reduces risk, it does not eliminate it.
- **Replacing PyInstaller as the default backend** -- PyInstaller stays default. It has the broadest package compatibility (numpy, PyQT, etc.) per the competitive research; Nuitka is a fallback/opt-in, not a replacement.
- **Commercial-grade obfuscation/protection** -- that's a different product category (see: paid AV-bypass products in the competitive research). Not the goal here.
- **Non-Windows platforms** -- the repo and Prime Directive are Windows-scoped already; no change to that scope.
- **Guaranteeing single-file distribution for every build path.** Distribution (sharing the output with someone else) is a nice-to-have, not a requirement this feature guarantees -- handled via a plain-language post-run message, not an architecture constraint. Local runs still default to single-file output where possible.

## User Stories

- As a non-technical end user, I want the bootstrapper to keep working even if the standard build hits a launch problem, so I don't have to understand why it failed.
- As a non-technical end user, when the standard build succeeds, I want to be asked in plain language if I'm willing to wait longer for a build that starts up more reliably, so I can choose speed vs. that tradeoff myself.
- As a non-technical end user sharing my app with a friend, I want a single file to send them (whether that's the `.exe` directly or a clearly-named zip), not a folder I have to figure out how to package myself.
- As the maintainer/agent, I want the fallback decision path logged clearly (which tier was used, and why), so diagnostics show exactly what happened.
- As the maintainer, I want the fallback path exercised regularly in CI via simulated failures, so it doesn't silently rot between the rare times it's actually needed in the field.

## Requirements

### Sequencing note (test-first)

Per direction, the failure-simulation tests below should be written **before** the fallback logic they test -- they're the stimulus that proves the state machine catches the right conditions, and expected results get updated once the implementation lands, not the other way around.

### Must-Have (P0)

1. **Build-failure simulation tests, written first.** Add test fixtures that simulate (a) a generic PyInstaller build failure, and (b) a failure where the output file is present, then removed or made inaccessible immediately after creation (this doubles as the stimulus for validating requirement 3 below, since it's the same trigger condition). Write these before the fallback logic itself.
   - Acceptance: Both fixtures exist and fail against the current (pre-fallback) codebase in the expected way, confirmed before any fallback code is written.
   - **Status: SHIPPED 2026-07-20, both fixtures, with a real correctness fix folded in.** While scoping this exact requirement, found that "the current (pre-fallback) codebase" did NOT actually fail in "the expected way" -- a genuine PyInstaller build failure was silently masked (`:die`'s `exit /b` only returns from its own call frame; nothing downstream re-checked the outcome, so `:after_cascade_decision` unconditionally overwrote `~bootstrap.status.json` back to `state=ok` and the process exited 0). Fixed at the root (set `HP_BOOTSTRAP_STATE=error` at the PyInstaller call site, mirroring the pre-existing preflight-failure precedent) before writing the fixtures, so "the expected way" is now a correct, visible failure rather than the bug. Both fixtures ship as `tests/selfapps_pyinstaller_fail.ps1` (`execfail` / `output_vanish` scenarios via new `HP_TEST_FORCE_PYINSTALLER_FAIL`/`HP_TEST_FORCE_OUTPUT_VANISH` test hooks), real/conda-full lanes. See `docs/agent-lessons-learned.md`'s `:die` entry for the full trace. This slice is scoped to requirement 1 only -- no Tier A/B fallback logic exists yet.
2. **Automatic fallback on build failure.** If the PyInstaller build step fails for any reason, automatically attempt a Nuitka build (Tier A first, see requirement 4) before surfacing a failure to the user.
   - Acceptance: Given a PyInstaller build failure of any kind, when the fallback triggers, then Nuitka is attempted and the outcome (success/failure) is clearly logged before any final failure state is surfaced.
   - **Status: SHIPPED 2026-07-20** together with requirements 3 and 4 (all three ship as one slice -- the dispatch and Tier A are inseparable in practice). See requirement 4's status note for the full detail.
3. **Broad file-access-error fallback trigger (per research Finding 2).** Treat any file-access error or missing-output-file condition in the build output directory -- not just generic build failure -- as a fallback trigger, without attempting to distinguish specific causes. This is deliberately not fingerprinted more precisely; that distinction isn't reliably detectable and isn't worth the engineering cost.
   - Acceptance: Given a build that completes but the output file is missing or inaccessible immediately after, when this pattern is detected, then the fallback path is attempted and the event is logged as "output file became inaccessible after build."
   - **Status: SHIPPED 2026-07-20.** All three of requirement 1's converging failure points (forced-fail test hook, real build errorlevel, missing/vanished output) now call the same `:try_nuitka_tier_a` subroutine before declaring final failure -- a single trigger category, exactly as specified.
4. **Tier A fallback: attempt Nuitka in the existing environment first, no reprovisioning.** Attempt Nuitka directly inside the same environment already used for the PyInstaller build -- same Python version, same installed packages. Let Nuitka perform its own internal compiler discovery (it checks for MSVC first and uses it automatically if present; otherwise it attempts its MinGW64 auto-download). Do not build an independent "is MSVC installed" check -- that kind of fragile fingerprinting is exactly what Finding 2 already argued against. Just attempt Tier A and read Nuitka's own success/failure signal.
   - Acceptance: On a machine with MSVC already installed, or where the main environment's Python is already <=3.12, Tier A succeeds with zero reprovisioning and no added environment-setup time beyond the Nuitka install itself.
   - **Status: SHIPPED 2026-07-20.** New `:try_nuitka_tier_a` subroutine (run_setup.bat, called via `call` -- never `goto` -- from inside the existing PyInstaller-build if/else nesting, so it's safe regardless of block depth): installs Nuitka into the current environment (uv or pip, matching `HP_ENV_MODE`), then runs `python -m nuitka --onefile --assume-yes-for-downloads --remove-output --output-dir=dist -o "<env>.exe" <entry>`. No independent compiler probing, exactly as specified -- Nuitka's own success/failure signal (errorlevel + `dist\<env>.exe` existence) is the only thing read. `--assume-yes-for-downloads` is load-bearing: without it, Nuitka can prompt interactively to confirm its own dependency downloads (e.g. MinGW64), which would hang both CI and a real non-interactive double-click run. On success the produced EXE is treated exactly like a PyInstaller one by the rest of the pipeline -- no special-casing needed downstream (the existing EXE smoke-test path doesn't care which tool built `dist\<env>.exe`). **Verified 2026-07-20 against real Windows CI (run 29788624195, `self.exe.build.tiera`, uv lane): the Nuitka CLI flags above are correct as written.** No Windows machine was available in this sandbox to confirm them before shipping, so this was written from documented Nuitka CLI knowledge only; the first real run built `dist\_selftest_nuitka_tiera.exe` via Nuitka 4.1.3 and ran it successfully. The very first CI attempt of this new test DID fail, but the cause was a test bug, not a flags bug: `tests/selfapps_nuitka_tiera.ps1` checked for the literal tilde-prefixed `dist\~selftest_nuitka_tiera.exe` instead of the sanitized `dist\_selftest_nuitka_tiera.exe` ENVNAME actually produces (run_setup.bat replaces every non-alnum/underscore/hyphen character with `_`), and read the app's stdout from the console-redirected bootstrap log instead of the `~run.out.txt` capture file. Fixed to match the established pattern in `selfapps_collect.ps1`/`selfapps_envsmoke.ps1`; the corrected run passed clean. `tests/selfapps_pyinstaller_fail.ps1` (requirement 1's test, real/conda-full, gating) was updated to also force Tier A to fail (`HP_TEST_FORCE_NUITKA_FAIL=1`) so it keeps testing tier EXHAUSTION rather than accidentally being saved by a real fallback success. Tier B (requirement 5) is NOT implemented yet -- a Tier A failure currently falls through to the pre-existing `:die` path unchanged.
5. **Tier B fallback: reprovision a pinned Python 3.12 environment, only if Tier A fails.** If Tier A fails (no usable compiler found, or the Python 3.13+/MinGW64 incompatibility is hit), reuse the already-discovered `requirements.txt` from the main run and provision a second environment pinned to Python 3.12 -- through the **existing provider-tier chain** (UV -> Conda -> venv -> System Python, not hardcoded to uv only) -- installing the same dependencies into it before invoking Nuitka there. Do not re-run pipreqs/heuristic dependency discovery; that work is already captured in the existing `requirements.txt`.
   - **Status: EXPLICITLY DEFERRED (owner instruction, 2026-07-21): "Until I run the bootstrapper myself and have real problems, I don't want to do the reprovision rollback."** Not implemented. Do not start without a fresh, separate go-ahead -- this was deliberately carved out of the broader "go as far as you can" authorization that greenlit requirements 2-4 and 9. A Tier A failure currently falls through to the pre-existing `:die` path unchanged.
   - **Environment cleanliness matters here regardless of restart-vs-in-process.** The main run's `VIRTUAL_ENV` and `PATH` entries point at the 3.13+ environment. If Tier B's provider-chain resolution relies on inherited `PATH`/`VIRTUAL_ENV` rather than an explicit, fully-resolved path to the 3.12 interpreter, it risks silently resolving back to the already-active 3.13+ environment instead of the pinned one -- which would reproduce the exact Tier A failure inside what's supposed to be the fix. Whether Tier B runs as a restart or in-process, it must reference the 3.12 interpreter/environment by explicit resolved path, not by relying on ambient shell state.
   - Acceptance: Tier B only triggers after a real Tier A failure (never speculatively), provisions Python 3.12 with the full dependency set actually installed (not a bare interpreter), is invoked through the standard provider chain rather than a hardcoded tool call, and is verifiably running under 3.12 (not silently resolving back to the main environment's Python version).
6. **Loop avoidance for Tier B (per research Finding 6).** If Tier B is implemented via a restart, the tool must set an internal-only marker (distinct from any user-authored `runtime.txt`) immediately before restarting, and check for that marker on startup. If the marker is already present when a failure occurs, stop with a clear final failure message -- there is no Tier C, and no scenario should loop more than once. If a restart is used, it must be the tool re-invoking itself automatically; the user is never asked to manually re-run anything.
   - Acceptance: A forced double-failure scenario (Tier A and Tier B both fail) terminates in exactly one clean failure message, with no repeated restart attempts, and the marker used for this is verifiably distinct from a user-supplied `runtime.txt`.
7. **Plain-language status messaging, with no safety/security framing.** Output must clearly distinguish: "build failed," "the built file became inaccessible right after creation," and "used a fallback build successfully" -- without implying the original build was unsafe, malicious, or a security risk in any way. Frame everything in terms of launch reliability, not danger. No jargon, no raw exception dumps as the primary message for a non-technical user.
8. **Connectivity check before attempting Tier B specifically.** Tier B's Nuitka build may require downloading a compiler (MinGW64) on first use. Check for network connectivity before attempting Tier B; if offline, skip it and say so plainly rather than hanging or failing with a confusing network error. (Tier A doesn't need this check up front, since it may succeed via an already-installed MSVC with no download at all.)
   - Acceptance: Given no network connectivity, when Tier B would otherwise trigger, then the user sees a clear "this requires an internet connection, which isn't available right now" message instead of a raw timeout or connection error.

### Nice-to-Have (P1)

9. **Post-success opt-in prompt, with copy that leans entirely on speed and reliability, never safety.** After a normal successful PyInstaller build, ask the user if they want an extra build. Per direction, drop any language implying the original build is less "safe" or has anything to do with malicious intent -- the honest claim is about launch reliability and execution speed, not security. Suggested copy direction (not final, agent should feel free to tighten further):
   > "Your app is ready. Want to build an optimized version too? It takes a bit longer to build right now, but it starts up more reliably on Windows and runs faster once it's built."
   Avoid promising a specific speed multiplier for build time or runtime -- that number isn't well established for this comparison. If a performance claim is wanted, the defensible one is: Nuitka's own documentation cites roughly 2-4x faster execution for compute-heavy code, which can be mentioned for scripts that do real work, but should not be generalized to all scripts or promised as a headline number.
   - **Status: SHIPPED 2026-07-21.** New `:offer_optimized_build` subroutine (`run_setup.bat`, called from `:smokerun_ndjson` right after `call :run_postexec_checkpoint exe`), gated on `HP_NUITKA_FALLBACK_USED` unset and `HP_EXE_EXIT` genuinely `"0"`. Uses the exact suggested copy above. Unlike Tier A (free to delete-then-rebuild since the original build already failed), this builds to a distinct temp filename, verifies the new build actually runs (same 30s-cap/`Kill()` pattern as `:run_exe_smokerun`), and only swaps it into `dist\<env>.exe` on confirmed success -- on any failure the original, already-verified EXE is left completely untouched. Reuses `:run_postexec_checkpoint`'s exact CI-safe consent-gate pattern. A reactive-only Visual Studio hint (fires only after a real failure, never proactively) was also added to both this subroutine's and `:try_nuitka_tier_a`'s failure paths, informed by direct research confirming Nuitka auto-detects an installed VS2022 via the registry with no Developer Command Prompt needed. `tests/selfapps_optimized_build.ps1` (uv lane, non-gating, `self.optbuild.offer`) covers all three outcomes: `accept` (real Nuitka build succeeds and swaps in), `forcefail` (deterministic failure leaves the original untouched and still runnable), `decline` (default/CI path shows the prompt but never builds). See `docs/agent-interconnect.md`'s "AV-Safe Build Path requirement 9" section for the full design.
10. **Automatic zip packaging for folder-based deliverables (per research Finding 5).** When the build output is folder-based (e.g., Nuitka `--standalone`), the bootstrapper zips it itself with a distinct, purpose-signaling filename (e.g. `<AppName>_ready_to_share.zip`) rather than asking the user to do it manually. This removes the "which folder is the right one" ambiguity a manually-named duplicate folder would create.
    - Acceptance: A folder-based build produces a zip file with a name that clearly differs from the source folder's name, and the debrief message (requirement 11) references that zip file directly.
11. **Post-run distribution debrief message.** After any successful build, tell the user in plain language how to share the result with someone else, if they want to. For single-file `.exe` output: "just send them this file." For the zip produced by requirement 10: "send them this file" (pointing at the zip, not the folder).
12. **Zip-extraction guard for folder-based deliverables, on the recipient's end.** The launcher inside any zip this project produces should check, at launch, whether it's running from an unextracted zip's temporary view and show a plain-language "click Extract All first" message rather than failing with a confusing missing-file error. Not needed for single-file `.exe` output.
13. **Native-launcher / interpreter-wrapper -- deprioritized, not scheduled.** Per research Finding 3, this remains a P2/future idea, not a v1 or near-term v2 direction. Single-file output should be protected as the default for local runs; a multi-file folder deliverable is a real usability hazard for non-technical users. Revisit only if problems in practice prove severe enough to outweigh that risk.
14. **README/AGENTS.md documentation** of the known launch-reliability limitation, the Tier A/Tier B fallback structure, the Python-3.12-pinning behavior of Tier B specifically (and the existing `pipreqs`-pinning precedent it mirrors), and what the tool does about it, so the behavior isn't a surprise to a technical reader inspecting the repo.

### Future Considerations (P2)

15. Additional alternate backends (e.g., cx_Freeze) if Nuitka proves insufficient for certain dependency sets.
16. Re-evaluate Zig as a Tier B compiler option once its Windows/3.13+ support matures -- not viable today per research Finding 1.

### Explicitly declined (not P2, not on the roadmap)

- **Code signing automation.** Declined -- no certificate exists on the user's side, and acquiring one is out of scope for this project.
- **Telemetry of any kind, including opt-in AV-fallback trigger tracking.** Declined -- the tool stays offline/telemetry-free by design. This was P2 item 10 in Draft v1; removed rather than deferred.

## Success Metrics

**Leading indicators:**
- % of CI runs where the fallback path is deliberately exercised (via the simulation tests from requirement 1) and succeeds at whichever tier is expected.
- % of simulated fallback runs that resolve via Tier A alone (no reprovisioning) vs. needing Tier B -- this tells us in practice how often the cheap path is enough, which the current design can't predict without real data.
- Fallback build time overhead stays bounded and documented (e.g., "adds ~X extra minutes") rather than open-ended, measured separately for Tier A and Tier B, and separately for first-run vs. cached-compiler runs.
- Zero measurable regression in the default (PyInstaller-success) path's speed or footprint.
- Tier B correctly provisions a Python 3.12 environment **with the discovered dependencies actually installed** (not just an empty interpreter), regardless of the main environment's Python version.
- A forced double-failure scenario (both tiers fail) always terminates in exactly one clean stop, never a loop -- verified directly by a test, not just documentation.

**Lagging indicators:**
- Reduction in "app won't open" / "app won't start" type reports, if/when such reports start appearing (none exist yet -- this is the preemptive nature of the work).
- Adoption rate of the opt-in "build optimized version" prompt, once P1 ships, as a signal of whether users value the tradeoff.

## Open Questions

- **(Resolved -- Engineering)** ~~What signal reliably distinguishes a specific failure cause from a generic one?~~ Resolved via research Finding 2: don't try to distinguish; treat any file-access error as one fallback trigger category.
- **(Resolved -- Engineering)** ~~Confirm Nuitka's MinGW64 auto-download path is reliable on a clean machine.~~ Resolved via research Finding 1: it is not reliable on Python 3.13+; mitigated by a two-tier design (try the existing environment first, letting Nuitka find MSVC itself if present; only reprovision a pinned Python 3.12 environment if that fails).
- **(Resolved -- Product)** ~~Does distribution require a single-file deliverable, or is a folder acceptable?~~ Resolved via research Finding 4/5: distribution is a nice-to-have; folder-based output gets zipped automatically by the tool rather than left to the user, with a zip-and-extract-guard treatment on the recipient's end.
- **(Resolved -- Engineering, by repo agent with direct codebase access)** ~~Full process restart with temp-state hand-off, vs. in-process re-invocation of the existing dependency-install routine at a different target Python version, for Tier B specifically.~~ **Use in-process re-invocation, not a full restart.** This repo already has a proven, shipped mechanism for exactly this shape of problem: `:provider_cascade` (the REQ-009/REQ-005.10 provider-cascade-on-warnfix-hard-failure system) re-attempts the dependency-installation phase under a *different* REQ-009 provider tier entirely in-process, via goto-based dispatch within the same running `run_setup.bat` invocation -- no restart, no marker file, no re-launch. It already re-derives `HP_ENV_MODE`, re-resolves the interpreter path, and re-runs dependency installation from `:after_env_mode_selection`, which is designed to be safely re-entrant (see `docs/agent-interconnect.md`'s "Provider cascade execution re-enters env-create" section). Tier B is structurally the same kind of event (a build-time failure that should re-attempt setup under a different, more-capable configuration) and should reuse this pattern rather than introduce a second, restart-based mechanism: pin the target Python explicitly (mirroring how the REQ-009 Tier 5 embed provider already resolves and pins to an *explicit, fully-resolved interpreter path*, never PATH lookup -- see `tools/embed_pyver_check.py` and `:try_embed_fallback`), call the dependency-install routine directly with that path, then invoke Nuitka. This sidesteps Finding 6's entire stale-`PATH`/`VIRTUAL_ENV` risk by construction -- there is no parent-process environment to inherit from if there's no restart, and the codebase already has the discipline of referencing pinned interpreters by explicit resolved path (not `PATH`) baked into its most recent tier (embed). A restart-based design would be *reintroducing* a class of risk (env leakage across a process boundary) this codebase has already identified and designed around once. If in-process re-invocation turns out to be genuinely awkward once someone is actually inside the PyInstaller/Nuitka build code (as opposed to the dependency-install code, which is confirmed re-entrant), that specific friction point -- not this general question -- is the thing worth re-opening.
- **(Resolved -- Product, by repo agent with direct codebase access)** ~~Should Tier B run automatically in unattended/CI contexts, or only when a human is present to answer the P1 opt-in prompt?~~ **Run automatically in both contexts; P0 and P1 share the failure-recovery code path, and only P1's separate "want an optimized build too?" prompt is human-only.** Reasoning: Tier B is a *build-fallback* (recovering from a failed PyInstaller build), not an environment tier that touches a shared/uncontrolled resource the way REQ-009's system-Python tier does -- it provisions a fresh, isolated, pinned environment through the existing provider chain, structurally closer to the embed tier (private, disposable, no consent gate) than to system Python (shared, persistent, REQ-014-gated). This repo's own established rule (`docs/agent-lessons-learned.md`, "Env-var flags are scaffolding, not intended run paths") is that a Prime-Directive-serving fallback must be reachable in the default, no-flag, non-interactive run -- gating Tier B behind a human-present check would make it silently unavailable in CI and in any unattended/scheduled invocation, which is exactly the kind of opt-in-required gate that principle exists to prevent. Requirement 9 (the P1 "want an optimized build too?" prompt) is a *separate* concern -- an elective, human-only upsell offered only *after* a successful primary build -- and should stay human-only/auto-declined-in-CI, matching every other consent-gated prompt in this codebase (REQ-014's system-Python consent, the REQ-018 post-execution checkpoint). Do not conflate the two: P0's Tier A/B is unconditional failure recovery; P1's extra-build offer is an optional enhancement layered on top of an already-successful run.

## Timeline Considerations

No hard deadline -- this is a preemptive risk, not an active incident. Suggested phasing, consistent with "smallest viable change" supervisor philosophy:

- **Phase 1 (P0 only):** Failure-simulation tests written first, then the tiered fallback logic (Tier A -> Tier B), loop-avoidance marker, and plain-language messaging. This is the smallest safe change and the only part with real user-facing downside risk if skipped.
- **Phase 2 (P1):** Opt-in optimized-build prompt + automatic zip packaging + post-run distribution debrief + zip-extraction guard, after Phase 1 has run cleanly in CI for a few cycles.
- **Phase 3 (P2):** Revisit native-launcher/interpreter-wrapper and additional backends based on whether Phase 1/2 actually get used and whether real problems start showing up.

## Related Work (Reference Only)

No project was found that implements this exact tiered PyInstaller->Nuitka fallback pattern -- this appears to be a genuine gap, consistent with the earlier competitive brief's finding. These are adjacent projects worth knowing about, for context and inspiration, not for copying code from:

- **auto-py-to-exe** -- https://github.com/brentvollebregt/auto-py-to-exe -- a GUI wrapper around PyInstaller. Notably, it already ships a `--build-directory-override` flag whose documented purpose is letting a user "whitelist a folder to stop your antivirus from removing files" -- real, independent confirmation that build-directory AV interference is a known, common-enough problem that other tools have already built workarounds for.
- **PyOxidizer** -- https://github.com/indygreg/PyOxidizer -- takes a structurally different approach: statically links Python and loads modules directly from memory rather than extracting to a temp directory at runtime, which is architecturally similar in spirit to what makes Nuitka's compiled output less AV-prone than PyInstaller's self-extracting pattern. Worth knowing about, but the project has had a slow release cadence in recent years -- not suggesting adoption, just noting the architectural precedent for "avoid the extract-to-temp-dir pattern entirely."
- **Briefcase (BeeWare)** -- https://github.com/beeware/briefcase -- produces native installers (e.g., proper `.msi` on Windows) rather than a single self-extracting `.exe`. Different tradeoff than this project's goals (an installer is a heavier, more "official" artifact than a single portable file), but relevant as another example of sidestepping the self-extracting-archive pattern that trips AV heuristics.
- **PyInstaller issue #6754** -- https://github.com/pyinstaller/pyinstaller/issues/6754 -- a real, long-running GitHub issue thread of developers hitting exactly this problem in production and comparing workarounds (including Nuitka and py2exe), useful as a live reference for how other teams describe and troubleshoot the same failure mode this PRD is designing around.

## Confidence Assessment

**Yellow -- proceed with Phase 1, but do not treat Phase 1 as a solved problem.**

- The Tier A/Tier B restructure is a real improvement over v3: it avoids paying the reprovisioning cost on every fallback trigger, leverages Nuitka's own documented MSVC auto-detection instead of building fragile detection logic ourselves, and is grounded in the same citation base as before (Nuitka's own docs on MSVC-by-default and the MinGW64/3.13+ restriction).
- Still yellow, not green, for the same core reason as v3: the Python-3.12-pinning limitation inside Tier B is real and permanent for machines without MSVC -- scripts using 3.13+-only syntax won't benefit from that tier. That ceiling should be stated plainly, not discovered by a user.
- The loop-avoidance design (single internal marker, exactly one Tier B attempt, no Tier C) is sound in principle but untested until the failure-simulation tests (requirement 1) actually exercise the double-failure case -- that's precisely why those tests are sequenced first.
- Whether Tier B uses a full restart or in-process re-invocation remains genuinely unknown without seeing the actual codebase structure -- flagged, not guessed at. **Resolved above** by a repo agent with direct access to the codebase: in-process re-invocation via the existing `:provider_cascade` pattern, not a restart.
- **New this pass:** the stale-`PATH`/`VIRTUAL_ENV` risk (requirement 5, Finding 6) is a real gap that wasn't specified in v4 and has now been added -- worth calling out because it's the kind of failure mode that would only surface in testing, not in a design review, which is exactly why it's a reason to stay cautious rather than declare done. **Note:** the in-process resolution above sidesteps this risk by construction rather than requiring it be defended against at restart time.
- Auto-zip and the copy rewrite are low technical risk -- mostly straightforward file operations and wording.

**On this pass's third-party review specifically:** its duplicate-requirement claim was checked directly against the current document and is false -- requirements 3 and 7 are distinct; that duplication existed in an earlier draft and was already corrected. Its Green/GO verdict is not adopted here -- the underlying design didn't newly resolve the 3.12 ceiling or the untested-loop-avoidance concern that kept this Yellow last pass, and it missed the environment-leakage gap entirely (which came from the same conversation, not from independent review). Its one genuinely valuable contribution -- the `PATH`/`VIRTUAL_ENV` leakage risk -- has been incorporated above. A third party's confidence label isn't a substitute for this document's own verdict, and in this case the two disagree.

---

## Notes from Claude (repo agent review, 2026-07-11)

This section was added by a Claude Code session with direct access to the live `Python_vs_Windows` repository, at the owner's request, after reading the PRD in full. It covers: overall assessment of the document, the two open questions resolved inline above, and -- at the owner's explicit and strongly-worded request -- a detailed treatment of a philosophical tension the PRD itself raises but deliberately scopes narrowly (Finding 1's "scoped philosophy resolution, not a global tradeoff").

### Overall assessment of the PRD

This is an unusually well-constructed PRD for a preemptive (not-yet-observed) risk: it cites primary sources (Nuitka's own docs and open issues, not secondhand summaries), it correctly self-corrected two real design mistakes across drafts (the v2 bare-3.12-interpreter mistake in Finding 1, and the always-reprovision cost in the earlier Tier A/B split), and it explicitly documents *why* it disagrees with a third-party Green/GO recommendation rather than either blindly adopting or silently ignoring it -- that's the right way to handle conflicting outside input, and this repo's own conventions (see `docs/agent-closed-backlog.md`'s "Known Findings" pattern of recording rejected alternatives with reasoning) already value exactly that kind of documented disagreement. The Yellow confidence rating is justified and should not be waved through to Green without the loop-avoidance tests (requirement 1) actually existing and passing against a real double-failure simulation.

One structural observation, not a defect: this PRD is large relative to how this repo actually ships work. `CLAUDE.md`'s Iteration Contract calls for "exactly ONE missing feature slice per loop," and even "Phase 1 (P0 only)" here bundles seven requirements spanning a new build backend, a two-tier reprovisioning decision, loop-avoidance state, environment-cleanliness guarantees, and a connectivity check. That's not a criticism of the PRD's scoping -- P0 genuinely can't be split much smaller without becoming incoherent -- it's a note for whoever picks this up: budget it as several loops, not one, and expect the "smallest viable change" framing in Timeline Considerations to mean *smallest viable **phase***, not a single sitting.

### Priority recommendation: way later, not now or soon

The Problem Statement is honest that "this risk has not yet caused a real reported failure" -- this is preemptive hardening against a well-documented *industry* problem (the Related Work section's citations are real and relevant), not a fix for anything this repo's own users have hit. Weighed against the size of Phase 1 alone (a second build backend, a two-tier fallback decision, new loop-avoidance state, new environment-cleanliness guarantees) and this repo's own stated discipline about not building ahead of a concrete, observed need (see the recently-rejected winget-as-a-tier and NuGet-as-a-fallback-host ideas in `docs/agent-closed-backlog.md`'s Known Findings -- both rejected specifically for being solutions to gaps that weren't actually biting anyone yet), my recommendation is: **keep this as a ready-to-execute, well-specified backlog item; do not schedule it now or soon.** Revisit when either (a) a real AV-quarantine report actually comes in from a user, or (b) there's a slow period with bandwidth for a genuine multi-loop initiative and nothing higher-value competing for it. This matches the PRD's own "no hard deadline... preemptive risk, not an active incident" framing -- I'm not disagreeing with the document, just making the "way later" framing explicit since the owner asked for a priority call.

### On generalizing the Python-version-pin pattern beyond Tier B (owner's explicit request)

The owner read Finding 1's "scoped philosophy resolution, not a global tradeoff" paragraph and reacted with strong, explicit hesitation about ever extending that pattern -- pinning to an older Python version -- beyond Tier B's narrow, well-justified case. The owner asked me to convey this hesitation forcefully enough that a future reader treats it as a real warning, not a footnote, and to state a worst-case scenario plainly. Here it is, in full.

**The specific idea under consideration (not proposed by this PRD, but raised in conversation around it):** should the *entire* bootstrapper's default Python-version selection (REQ-004: "let the selected provider pick latest, no hard-coded fallback") deliberately lag behind the newest CPython release by a minor version or two -- mirroring how conda-forge and, to a lesser extent, uv's own package ecosystem naturally take some time to catch up after a new Python release -- rather than always chasing the newest release on day one? The reasoning in favor is intuitive: a beginner's script is more likely to actually work if the interpreter it lands on isn't the version the wider package ecosystem hasn't finished supporting yet, and Tier B's own 3.12 pin is a live example of exactly that problem class (Nuitka's MinGW64 compiler doesn't support 3.13+ yet).

**Why I recommend against generalizing this, stated plainly:**

1. **This repo has no update mechanism for already-distributed copies, which makes any new pin's staleness risk categorically worse than it looks.** `run_setup.bat` is explicitly self-contained and is distributed by being copied -- emailed, downloaded once, dropped into a project folder -- with **zero telemetry** (declined outright in this very PRD's Non-Goals) and **no auto-update mechanism** anywhere in the architecture. Once a copy of the bootstrapper is sitting in a user's folder, it runs exactly the logic it shipped with, forever, until that specific user manually goes and gets a newer copy -- which, for the tool's actual target audience (beginners who were handed a `.py` file and don't know what Python is), realistically never happens. This is a fundamentally different distribution model from almost anything else that ships version pins: a pip package gets a fresh resolve on every install; a SaaS tool gets redeployed centrally; even most CLI tools have a "check for updates" nag. This one has none of that. **A hardcoded "always stay one or two versions behind latest" policy baked into the bootstrapper today doesn't just risk being stale later -- it is *guaranteed* to still be running its exact today's-logic on every copy already in the wild, indefinitely, regardless of whether the reason for the policy ever stops being true.**

2. **The worst-case scenario, stated concretely, exactly as the owner asked:** suppose this policy ships. Two years from now, the ecosystem-lag problem it was designed around has substantially resolved -- conda-forge and PyPI wheel coverage for new Python releases lands within weeks now instead of months, say, because tooling across the ecosystem has generally improved. The *central* repository's `CLAUDE.md` gets its quarterly maintenance pass, someone notices the lag policy is no longer earning its keep, and bumps or removes it in `main`. **None of that helps a single copy of `run_setup.bat` that is already sitting in someone's project folder from eighteen months ago.** That copy keeps capping itself at "latest minus however-many," on a machine whose owner has no idea the cap exists, for a reason that stopped being true long before they ever ran it a second time. The tool's own stated purpose -- "getting the code to run takes priority over preserving outdated constraints" -- would be defeated by the tool's own infrastructure: an artificial version ceiling nobody chose, that nobody can see, enforced by logic that was already obsolete when they downloaded it. That is a materially worse failure mode than the one it would have been trying to prevent (an occasional day-one ecosystem-lag hiccup, which -- see point 4 below -- the existing architecture already degrades out of reasonably gracefully).

3. **It directly contradicts an existing, deliberate design principle in this same codebase, not just a vague ideal.** REQ-004 is explicit: *"let the selected provider pick latest (no hard-coded fallback)."* That phrase -- "no hard-coded fallback" -- was clearly a considered decision, not an oversight (the surrounding `Closed Backlog` entry for "Python version detection Tier 3 write-back" describes deliberately *removing* a previous `python<3.13` hard-coded cap in favor of this policy). A blanket "lag by N" default would be reintroducing the exact thing that was removed, generalized from one Python release to a permanent policy.

4. **The existing architecture already degrades reasonably gracefully against ecosystem lag, without a new pin.** This is worth being concrete about, because it changes the cost/benefit picture: conda's own solver won't offer a Python version conda-forge hasn't finished building packages for in the first place -- it's self-limiting by construction, not something this bootstrapper has to defend against. Where a genuine gap could still show up (uv acquiring a brand-new CPython release before some specific package has a wheel for it, or a package's own C-extension build lagging) is *exactly* the shape of failure the warnfix/provider-cascade system already exists to catch and recover from -- a failed install is a failed install, whether the cause is a missing translation-table mapping or an ecosystem-lag gap, and the existing repair-and-retry loop doesn't need to know which. A new global pin would be solving a problem this bootstrapper already has a general-purpose mechanism for, at the cost of a permanent, un-updatable ceiling.

5. **The maintenance-burden point the owner raised deserves to be named directly, not just implied:** every pin this repo carries (`pipreqs==0.4.13` today, Tier B's `3.12` if this PRD ships) is a promise that someone will keep periodically re-checking whether it's still justified -- and this repo already has real infrastructure for that (the quarterly "Periodic Maintenance Checks" cadence in `CLAUDE.md`, backed by a scheduled trigger). But as point 1 spells out, that infrastructure only ever refreshes what's in `main`. It has no reach into copies already distributed. **The more pins this project accumulates, the larger the gap grows between "what the current repo believes is correct" and "what every already-distributed copy is actually still doing" -- and that gap is permanent and unobservable from this side, since there's no telemetry to even measure how many stale copies are out there or how stale they've gotten.** This isn't a reason to never pin anything (Tier B's 3.12 pin is well-justified, narrow, and cites a specific, checkable upstream fact rather than a vibe) -- it's a reason to treat every *new* pin request with real scrutiny, and to be specifically suspicious of any pin proposed as a general precaution ("just in case," "to be safe") rather than as the fix for one, cited, currently-true constraint.

**What this means concretely for anyone extending this PRD:**

- **Keep Tier B's 3.12 pin exactly as scoped** -- it fixes a real, specific, currently-true, citable upstream limitation (Nuitka+MinGW64 doesn't support 3.13+), it's isolated to a narrow fallback branch nobody hits unless PyInstaller already failed, and it doesn't touch the main bootstrap path's Python selection at all. This is the *good* kind of pin per point 5 above, and does not need to change.
- **Do not generalize this into a bootstrapper-wide "stay behind latest" default.** If ecosystem-lag pain on brand-new Python releases becomes a real, observed problem (not a hypothetical), the fix that fits this repo's actual constraints is almost certainly *not* a new hard-coded version ceiling -- it's more likely to be: better warnfix/cascade coverage for the specific failure signatures ecosystem-lag produces, or a documentation/messaging improvement ("this Python version is very new; some packages may not support it yet" as a plain-language WARN, matching the tone of existing REQ-016 post-flight guidance), neither of which carries the permanent, un-updatable staleness risk a hard version cap does.
- **If a pin is ever proposed anywhere in this codebase going forward,** the bar should be: does it fix one specific, currently-true, checkable upstream fact (like Tier B's 3.12 pin, like the existing `pipreqs==0.4.13` pin) -- or is it a general precaution against a class of problem the existing repair/cascade infrastructure might already handle well enough? The former is proportionate. The latter is how a tool with no update mechanism slowly fills up with silent, permanent, unmeasurable technical debt distributed across every copy anyone ever downloaded.

See also: `CLAUDE.md`'s Active Backlog, which now links back to this document, and a new companion entry in `CLAUDE.md`'s Periodic Maintenance Checks describing a lightweight "next-pin probe" concept for keeping the *existing* pins (`pipreqs`, and Tier B's `3.12` if it ships) honest over time within the repo itself -- which is the correctly-scoped version of "check whether a pin is still needed," as opposed to a new pin that would itself need the same treatment forever.
