Files
PDFToMD/docs/superpowers/plans/2026-05-12-offline-installer.md
T
2026-05-14 10:16:59 +09:00

23 KiB

Offline Windows Installer Implementation Plan

Status: Abandoned at the user's request on 2026-05-13 before implementation began. This file is retained as historical planning context only. Do not execute this plan unless the user explicitly reopens offline installer work.

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build an offline Windows installer that installs the existing pdf2md CLI/UI runtime on another Windows x64 PC without internet access.

Architecture: Build a large installer payload on an internet-connected build PC, then create the target PC .venv locally from bundled wheels during installation. Keep conversion behavior unchanged and keep the UI as a launcher over the installed project-owned pdf2md CLI.

Tech Stack: Python 3.12, uv, pip wheelhouse/download workflow, PyInstaller, PowerShell, Inno Setup, MinerU 3.1.0, CUDA PyTorch 2.6.0+cu126, optional Node.js/MathJax.


File Structure

  • docs/Sprints/SPRINT17CONTRACT.md: sprint contract, scope, acceptance criteria, and hard failure criteria.
  • packaging/offline/build-offline-payload.ps1: connected build-PC script that stages all offline files under dist/offline-installer/.
  • packaging/offline/verify-offline-payload.ps1: build-PC and target-PC script that validates payload-manifest.json and hashes.
  • packaging/offline/install-runtime.ps1: target-PC installer script that hash-verifies the payload, creates .venv, installs from local wheels, configures local models, and runs doctor.
  • packaging/offline/repair-runtime.ps1: target-PC repair script that recreates .venv from the retained wheelhouse.
  • packaging/offline/run-doctor.ps1: shortcut target for post-install diagnostics.
  • packaging/offline/Pdf2MdOffline.iss: Inno Setup installer script.
  • packaging/offline/requirements-runtime-cu126.txt: pinned offline runtime requirement set for Windows x64 CUDA 12.6 wheels.
  • packaging/offline/README.md: build and install instructions.
  • packaging/offline/THIRD_PARTY_NOTICES.md: redistribution notes and license links for bundled payload families.
  • src/pdf2md/packaging_manifest.py: optional small helper for deterministic manifest/hash generation.
  • src/pdf2md_ui/runner.py: installed runtime command resolution and child environment updates.
  • src/pdf2md_ui/app.py: installed runtime project-root default only if needed.
  • tests/test_offline_packaging.py: fast tests for manifest, script safety, and installer script contents with fake payloads.
  • tests/test_ui_runner.py: fast tests for installed .venv and bundled uv --offline command resolution.
  • .gitignore: ignore generated payload, wheelhouse, models, and installer outputs.
  • README.md and docs/V1RELEASECHECKLIST.md: user-facing build/release documentation.
  • PLAN.md, PROGRESS.md, docs/WORKARCHIVE.md: coordination and handoff.

Task 1: Packaging Manifest And Ignore Policy

Files:

  • Create: tests/test_offline_packaging.py

  • Create: src/pdf2md/packaging_manifest.py

  • Modify: .gitignore

  • Step 1: Write the failing manifest tests

from pathlib import Path

from pdf2md.packaging_manifest import build_payload_manifest


def test_build_payload_manifest_records_hash_size_and_source(tmp_path: Path) -> None:
    payload = tmp_path / "payload"
    payload.mkdir()
    wheel = payload / "wheelhouse" / "example-1.0-py3-none-any.whl"
    wheel.parent.mkdir()
    wheel.write_bytes(b"wheel-bytes")

    manifest = build_payload_manifest(
        payload,
        sources={"wheelhouse/example-1.0-py3-none-any.whl": "local test wheel"},
    )

    assert manifest["files"] == [
        {
            "path": "wheelhouse/example-1.0-py3-none-any.whl",
            "size": 11,
            "sha256": "9ceb18f15662bb87e54af2f5953c0484d2ef76f5444d87913360b9ef87d7296d",
            "source": "local test wheel",
        }
    ]


def test_build_payload_manifest_uses_forward_slash_relative_paths(tmp_path: Path) -> None:
    payload = tmp_path / "payload"
    nested = payload / "models" / "mineru" / "model.bin"
    nested.parent.mkdir(parents=True)
    nested.write_bytes(b"model")

    manifest = build_payload_manifest(payload, sources={})

    assert manifest["files"][0]["path"] == "models/mineru/model.bin"
  • Step 2: Run the manifest tests to verify failure

Run:

uv run pytest tests/test_offline_packaging.py -q

Expected: FAIL because pdf2md.packaging_manifest does not exist.

  • Step 3: Implement the minimal manifest helper
"""Offline installer payload manifest helpers."""

from __future__ import annotations

import hashlib
from pathlib import Path
from typing import Mapping, TypedDict


class ManifestFile(TypedDict):
    path: str
    size: int
    sha256: str
    source: str


class PayloadManifest(TypedDict):
    files: list[ManifestFile]


def build_payload_manifest(payload_root: str | Path, *, sources: Mapping[str, str]) -> PayloadManifest:
    root = Path(payload_root)
    files: list[ManifestFile] = []
    for path in sorted(candidate for candidate in root.rglob("*") if candidate.is_file()):
        relative = path.relative_to(root).as_posix()
        files.append(
            {
                "path": relative,
                "size": path.stat().st_size,
                "sha256": _sha256(path),
                "source": sources.get(relative, "unknown"),
            }
        )
    return {"files": files}


def _sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()
  • Step 4: Add generated payload ignores

Append to .gitignore:

dist/
packaging/offline/_payload/
packaging/offline/_wheelhouse/
packaging/offline/_models/
*.issig
*.exe.tmp

If dist/ is already ignored implicitly by an existing entry, keep one clear dist/ entry and avoid duplicates.

  • Step 5: Run tests

Run:

uv run pytest tests/test_offline_packaging.py -q
git diff --check

Expected: tests PASS; diff check has no whitespace errors.

  • Step 6: Commit
git add .gitignore src\pdf2md\packaging_manifest.py tests\test_offline_packaging.py
git commit -m "feat: add offline payload manifest helper"

Task 2: Offline Payload Builder

Files:

  • Create: packaging/offline/build-offline-payload.ps1

  • Create: packaging/offline/verify-offline-payload.ps1

  • Create: packaging/offline/requirements-runtime-cu126.txt

  • Create: packaging/offline/README.md

  • Modify: tests/test_offline_packaging.py

  • Step 1: Write tests for builder safety

from pathlib import Path


def test_payload_builder_excludes_development_and_sample_paths() -> None:
    script = Path("packaging/offline/build-offline-payload.ps1").read_text(encoding="utf-8")

    assert ".git" in script
    assert ".venv" in script
    assert "samples" in script
    assert "outputs" in script
    assert "Copy-Item -Recurse -Force" in script


def test_runtime_requirements_pin_core_gpu_stack() -> None:
    requirements = Path("packaging/offline/requirements-runtime-cu126.txt").read_text(encoding="utf-8")

    assert "torch==2.6.0" in requirements
    assert "torchvision==0.21.0" in requirements
    assert "mineru[core]==3.1.0" in requirements
    assert "pypdf" in requirements
  • Step 2: Run tests to verify failure

Run:

uv run pytest tests/test_offline_packaging.py -q

Expected: FAIL because the packaging files do not exist.

  • Step 3: Create the pinned requirements file
convert-pdf-to-md==0.1.0
pypdf>=6.10.2,<7
torch==2.6.0
torchvision==0.21.0
mineru[core]==3.1.0
  • Step 4: Create the payload builder skeleton

The script must accept explicit input paths and fail when required payload pieces are missing:

param(
    [string]$Configuration = "Release",
    [string]$PythonInstaller,
    [string]$UvExe,
    [string]$MinerUModelSource,
    [string]$NodeRoot = "",
    [string]$OutputRoot = "dist\offline-installer"
)

$ErrorActionPreference = "Stop"
$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
$StageRoot = Join-Path $RepoRoot $OutputRoot
$AppRoot = Join-Path $StageRoot "app"
$RuntimeRoot = Join-Path $StageRoot "runtime"
$PayloadRoot = Join-Path $StageRoot "payload"

if (-not (Test-Path $PythonInstaller)) { throw "Missing Python installer: $PythonInstaller" }
if (-not (Test-Path $UvExe)) { throw "Missing uv.exe: $UvExe" }
if (-not (Test-Path $MinerUModelSource)) { throw "Missing MinerU model source: $MinerUModelSource" }
if (-not (Test-Path (Join-Path $RepoRoot "dist\pdf2md-ui.exe"))) { throw "Missing UI exe. Build dist\pdf2md-ui.exe first." }

Remove-Item -LiteralPath $StageRoot -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $AppRoot,$RuntimeRoot,$PayloadRoot | Out-Null

$Excluded = @(".git", ".venv", "samples", "outputs", "dist", "build", "node_modules", ".pytest_cache", "__pycache__")

Copy-Item -Recurse -Force (Join-Path $RepoRoot "src") (Join-Path $RuntimeRoot "src")
Copy-Item -Force (Join-Path $RepoRoot "pyproject.toml") (Join-Path $RuntimeRoot "pyproject.toml")
Copy-Item -Force (Join-Path $RepoRoot "uv.lock") (Join-Path $RuntimeRoot "uv.lock")
Copy-Item -Force (Join-Path $RepoRoot "README.md") (Join-Path $RuntimeRoot "README.md")
Copy-Item -Force (Join-Path $RepoRoot "dist\pdf2md-ui.exe") (Join-Path $AppRoot "pdf2md-ui.exe")
New-Item -ItemType Directory -Path (Join-Path $PayloadRoot "python"),(Join-Path $PayloadRoot "uv") | Out-Null
Copy-Item -Force $PythonInstaller (Join-Path $PayloadRoot "python\python-3.12-amd64.exe")
Copy-Item -Force $UvExe (Join-Path $PayloadRoot "uv\uv.exe")
Copy-Item -Recurse -Force $MinerUModelSource (Join-Path $PayloadRoot "models")

if ($NodeRoot -and (Test-Path $NodeRoot)) {
    Copy-Item -Recurse -Force $NodeRoot (Join-Path $PayloadRoot "node")
}

Write-Host "Offline installer stage created at $StageRoot"
Write-Host "Use pip download on the connected build PC to fill payload\wheelhouse before compiling the installer."
  • Step 5: Document the connected wheelhouse build command

Add to packaging/offline/README.md:

uv build --wheel
Copy-Item dist\convert_pdf_to_md-0.1.0-py3-none-any.whl dist\offline-installer\payload\wheelhouse\
py -3.12 -m pip download -d dist\offline-installer\payload\wheelhouse -r packaging\offline\requirements-runtime-cu126.txt --find-links dist\offline-installer\payload\wheelhouse --extra-index-url https://download.pytorch.org/whl/cu126
  • Step 6: Add the payload verifier

verify-offline-payload.ps1 must read payload\payload-manifest.json, recompute SHA-256 for each listed file, and fail when a file is missing or changed.

  • Step 7: Run tests

Run:

uv run pytest tests/test_offline_packaging.py -q
git diff --check

Expected: PASS.

  • Step 8: Commit
git add packaging\offline\build-offline-payload.ps1 packaging\offline\verify-offline-payload.ps1 packaging\offline\requirements-runtime-cu126.txt packaging\offline\README.md tests\test_offline_packaging.py
git commit -m "feat: plan offline payload builder"

Task 3: Target Runtime Install And Repair Scripts

Files:

  • Create: packaging/offline/install-runtime.ps1

  • Create: packaging/offline/repair-runtime.ps1

  • Create: packaging/offline/run-doctor.ps1

  • Modify: tests/test_offline_packaging.py

  • Step 1: Write script safety tests

from pathlib import Path


def test_install_runtime_uses_only_local_package_sources() -> None:
    script = Path("packaging/offline/install-runtime.ps1").read_text(encoding="utf-8")

    assert "--no-index" in script
    assert "--find-links" in script
    assert "UV_OFFLINE" in script
    assert "https://" not in script
    assert "http://" not in script


def test_install_runtime_does_not_silently_overwrite_mineru_config() -> None:
    script = Path("packaging/offline/install-runtime.ps1").read_text(encoding="utf-8")

    assert "mineru.json" in script
    assert "Backup" in script
    assert "Silent" in script
    assert "throw" in script
  • Step 2: Run tests to verify failure

Run:

uv run pytest tests/test_offline_packaging.py -q

Expected: FAIL because scripts do not exist.

  • Step 3: Implement install-runtime.ps1

The script must:

param(
    [string]$InstallRoot = "$env:LOCALAPPDATA\Programs\ConvertPDFToMD",
    [switch]$Silent
)

$ErrorActionPreference = "Stop"
$PayloadRoot = Join-Path $InstallRoot "payload"
$RuntimeRoot = Join-Path $InstallRoot "runtime"
$VenvPython = Join-Path $RuntimeRoot ".venv\Scripts\python.exe"
$VenvPdf2Md = Join-Path $RuntimeRoot ".venv\Scripts\pdf2md.exe"
$UvExe = Join-Path $PayloadRoot "uv\uv.exe"
$Wheelhouse = Join-Path $PayloadRoot "wheelhouse"
$Requirements = Join-Path $PayloadRoot "requirements-runtime-cu126.txt"
$LogRoot = Join-Path $InstallRoot "logs"

New-Item -ItemType Directory -Path $LogRoot -Force | Out-Null
$env:UV_OFFLINE = "1"
$env:MINERU_MODEL_SOURCE = "local"

if (-not (Test-Path $UvExe)) { throw "Missing bundled uv.exe: $UvExe" }
if (-not (Test-Path $Wheelhouse)) { throw "Missing wheelhouse: $Wheelhouse" }
if (-not (Test-Path $Requirements)) { throw "Missing requirements: $Requirements" }

& $UvExe venv (Join-Path $RuntimeRoot ".venv") --python 3.12
if ($LASTEXITCODE -ne 0) { throw "uv venv failed with exit code $LASTEXITCODE" }

& $UvExe pip install --python $VenvPython --no-index --find-links $Wheelhouse -r $Requirements
if ($LASTEXITCODE -ne 0) { throw "offline package install failed with exit code $LASTEXITCODE" }

& $UvExe pip check --python $VenvPython
if ($LASTEXITCODE -ne 0) { throw "uv pip check failed with exit code $LASTEXITCODE" }

$MinerUConfig = Join-Path $env:USERPROFILE "mineru.json"
if (Test-Path $MinerUConfig) {
    if ($Silent) { throw "Existing mineru.json requires interactive confirmation: $MinerUConfig" }
    $Backup = "$MinerUConfig.pdf2md-backup-$(Get-Date -Format yyyyMMddHHmmss)"
    Copy-Item -Force $MinerUConfig $Backup
}

& $VenvPdf2Md doctor *> (Join-Path $LogRoot "doctor-after-install.txt")
if ($LASTEXITCODE -ne 0) { throw "pdf2md doctor failed with exit code $LASTEXITCODE" }
  • Step 4: Implement repair and doctor scripts

repair-runtime.ps1 reruns install-runtime.ps1 for an existing install root. run-doctor.ps1 runs the installed .venv\Scripts\pdf2md.exe doctor and writes logs\doctor-latest.txt.

  • Step 5: Run tests

Run:

uv run pytest tests/test_offline_packaging.py -q
git diff --check

Expected: PASS.

  • Step 6: Commit
git add packaging\offline\install-runtime.ps1 packaging\offline\repair-runtime.ps1 packaging\offline\run-doctor.ps1 tests\test_offline_packaging.py
git commit -m "feat: add offline runtime install scripts"

Task 4: UI Installed Runtime Resolution

Files:

  • Modify: src/pdf2md_ui/runner.py

  • Modify: src/pdf2md_ui/app.py only if needed

  • Modify: tests/test_ui_runner.py

  • Step 1: Add failing runner tests

from pathlib import Path

from pdf2md_ui.runner import resolve_cli_command


def test_resolve_prefers_project_venv_pdf2md(tmp_path: Path) -> None:
    root = tmp_path / "runtime"
    scripts = root / ".venv" / "Scripts"
    scripts.mkdir(parents=True)
    (root / "pyproject.toml").write_text("[project]\nname='x'\n", encoding="utf-8")
    pdf2md = scripts / "pdf2md.exe"
    pdf2md.write_text("", encoding="utf-8")

    resolved = resolve_cli_command(project_root=root, which=lambda name: None)

    assert resolved.args_prefix == (str(pdf2md),)
    assert resolved.cwd is None
    assert resolved.source == "venv"


def test_resolve_uses_bundled_uv_offline_when_no_venv_command(tmp_path: Path) -> None:
    root = tmp_path / "runtime"
    root.mkdir()
    (root / "pyproject.toml").write_text("[project]\nname='x'\n", encoding="utf-8")
    uv = tmp_path / "payload" / "uv" / "uv.exe"
    uv.parent.mkdir(parents=True)
    uv.write_text("", encoding="utf-8")

    resolved = resolve_cli_command(project_root=root, bundled_uv=uv, which=lambda name: None)

    assert resolved.args_prefix == (str(uv), "run", "--offline", "pdf2md")
    assert resolved.cwd == root
    assert resolved.source == "bundled-uv"
  • Step 2: Run tests to verify failure

Run:

uv run pytest tests/test_ui_runner.py -q

Expected: FAIL because the runner does not yet support installed .venv or bundled uv resolution.

  • Step 3: Implement minimal runner changes

Add bundled_uv as an optional keyword to resolve_cli_command, check <project_root>\.venv\Scripts\pdf2md.exe after configured command and before PATH, and use bundled uv run --offline pdf2md before system uv.

  • Step 4: Add child environment tests

Add a test that build_child_environment(project_root=runtime_root) prepends .venv\Scripts and payload\node when those folders exist, while preserving MINERU_MODEL_SOURCE=custom if the user already set it.

  • Step 5: Run tests

Run:

uv run pytest tests/test_ui_runner.py -q

Expected: PASS.

  • Step 6: Commit
git add src\pdf2md_ui\runner.py src\pdf2md_ui\app.py tests\test_ui_runner.py
git commit -m "feat: resolve installed offline runtime from UI"

Task 5: Inno Setup Script

Files:

  • Create: packaging/offline/Pdf2MdOffline.iss

  • Modify: tests/test_offline_packaging.py

  • Step 1: Add Inno script tests

from pathlib import Path


def test_inno_script_installs_payload_and_shortcuts() -> None:
    script = Path("packaging/offline/Pdf2MdOffline.iss").read_text(encoding="utf-8")

    assert "DefaultDirName={localappdata}\\Programs\\ConvertPDFToMD" in script
    assert "payload\\*" in script
    assert "app\\*" in script
    assert "runtime\\*" in script
    assert "pdf2md-ui.exe" in script
    assert "install-runtime.ps1" in script
    assert "PDF2MD Doctor" in script
    assert "Repair PDF2MD Runtime" in script


def test_inno_script_excludes_development_artifacts() -> None:
    script = Path("packaging/offline/Pdf2MdOffline.iss").read_text(encoding="utf-8")

    assert "samples" not in script
    assert "outputs" not in script
    assert ".venv" not in script
  • Step 2: Run tests to verify failure

Run:

uv run pytest tests/test_offline_packaging.py -q

Expected: FAIL because the Inno script does not exist.

  • Step 3: Create the Inno script
[Setup]
AppId={{PDF2MD-OFFLINE-INSTALLER}}
AppName=ConvertPDFToMD
AppVersion=0.1.0
DefaultDirName={localappdata}\Programs\ConvertPDFToMD
DefaultGroupName=ConvertPDFToMD
OutputDir=..\..\dist
OutputBaseFilename=Pdf2MdOfflineSetup-0.1.0
Compression=lzma2
SolidCompression=yes
PrivilegesRequired=lowest

[Files]
Source: "..\..\dist\offline-installer\payload\*"; DestDir: "{app}\payload"; Flags: recursesubdirs createallsubdirs
Source: "..\..\dist\offline-installer\app\*"; DestDir: "{app}\app"; Flags: recursesubdirs createallsubdirs
Source: "..\..\dist\offline-installer\runtime\*"; DestDir: "{app}\runtime"; Flags: recursesubdirs createallsubdirs
Source: "install-runtime.ps1"; DestDir: "{app}\scripts"
Source: "repair-runtime.ps1"; DestDir: "{app}\scripts"
Source: "run-doctor.ps1"; DestDir: "{app}\scripts"

[Icons]
Name: "{group}\ConvertPDFToMD"; Filename: "{app}\app\pdf2md-ui.exe"; WorkingDir: "{app}\runtime"
Name: "{group}\PDF2MD Doctor"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\scripts\run-doctor.ps1"""; WorkingDir: "{app}"
Name: "{group}\Repair PDF2MD Runtime"; Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\scripts\repair-runtime.ps1"""; WorkingDir: "{app}"

[Run]
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -File ""{app}\scripts\install-runtime.ps1"" -InstallRoot ""{app}"""; StatusMsg: "Installing offline pdf2md runtime..."; Flags: runhidden
  • Step 4: Run tests

Run:

uv run pytest tests/test_offline_packaging.py -q
git diff --check

Expected: PASS.

  • Step 5: Compile with Inno Setup on a build PC

Run:

ISCC.exe packaging\offline\Pdf2MdOffline.iss

Expected: exit code 0 and dist\Pdf2MdOfflineSetup-0.1.0.exe exists. Do not commit the generated exe.

  • Step 6: Commit
git add packaging\offline\Pdf2MdOffline.iss tests\test_offline_packaging.py
git commit -m "feat: add offline installer script"

Task 6: Documentation, Verification, And Handoff

Files:

  • Modify: README.md

  • Modify: docs/V1RELEASECHECKLIST.md

  • Modify: docs/Sprints/SPRINT17CONTRACT.md

  • Modify: PLAN.md

  • Modify: PROGRESS.md

  • Modify: docs/WORKARCHIVE.md

  • Step 1: Document build and install flow

Add a README section with:

## Offline Windows Installer

The offline installer is built on an internet-connected Windows x64 build PC, then copied to a target Windows x64 PC with networking disabled. The target installer creates a fresh `.venv` from bundled wheels; it does not copy the development `.venv`.
  • Step 2: Document verification gates

Add to docs/V1RELEASECHECKLIST.md:

### Offline Installer Gate

- Build `dist\pdf2md-ui.exe`.
- Stage the offline payload.
- Verify payload hashes.
- Compile the Inno Setup installer.
- Install on a clean Windows x64 VM with networking disabled.
- Run `pdf2md doctor` from the installed `.venv`.
- Run one optional local conversion only when a local test PDF is available and generated outputs remain ignored.
  • Step 3: Run final fast tests

Run:

uv run pytest tests/test_offline_packaging.py tests/test_ui_runner.py
uv run pytest
git diff --check

Expected: PASS, except pre-existing documented optional skips.

  • Step 4: Run packaging smoke on build PC

Run:

uv run --group ui-build pyinstaller --clean --onefile --windowed --name pdf2md-ui src\pdf2md_ui\app.py
$pythonInstaller = "C:\BuildCache\python-3.12-amd64.exe"
$uvExe = "C:\BuildCache\uv.exe"
$mineruModels = "C:\BuildCache\mineru-models"
powershell -ExecutionPolicy Bypass -File packaging\offline\build-offline-payload.ps1 -Configuration Release -PythonInstaller $pythonInstaller -UvExe $uvExe -MinerUModelSource $mineruModels
ISCC.exe packaging\offline\Pdf2MdOffline.iss

Expected: installer exe exists under dist\; generated files remain untracked.

  • Step 5: Update coordination docs

Record changed files, verification output, generated installer path, payload size, and residual risks in PROGRESS.md. Move final implementation evidence and offline VM smoke results to docs/WORKARCHIVE.md.

  • Step 6: Commit final docs
git add README.md docs\V1RELEASECHECKLIST.md docs\Sprints\SPRINT17CONTRACT.md PLAN.md PROGRESS.md docs\WORKARCHIVE.md
git commit -m "docs: record offline installer release gate"

Execution Notes

  • Do not commit payload contents, wheels, model files, Python installers, Node binaries, generated installer exe files, samples/, or outputs/.
  • Keep runtime conversion strict-local. Setup-time payload creation may use internet only on the build PC.
  • Treat license/model redistribution review as a release gate before sharing the installer outside the current personal environment.