modify pdftomd

This commit is contained in:
김경종
2026-05-14 10:16:59 +09:00
parent 2232b51fc9
commit dc11880140
69 changed files with 7784 additions and 1150 deletions
@@ -0,0 +1,683 @@
# 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**
```python
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:
```powershell
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**
```python
"""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`:
```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:
```powershell
uv run pytest tests/test_offline_packaging.py -q
git diff --check
```
Expected: tests PASS; diff check has no whitespace errors.
- [ ] **Step 6: Commit**
```powershell
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**
```python
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:
```powershell
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**
```text
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:
```powershell
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`:
```powershell
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:
```powershell
uv run pytest tests/test_offline_packaging.py -q
git diff --check
```
Expected: PASS.
- [ ] **Step 8: Commit**
```powershell
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**
```python
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:
```powershell
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:
```powershell
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:
```powershell
uv run pytest tests/test_offline_packaging.py -q
git diff --check
```
Expected: PASS.
- [ ] **Step 6: Commit**
```powershell
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**
```python
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:
```powershell
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:
```powershell
uv run pytest tests/test_ui_runner.py -q
```
Expected: PASS.
- [ ] **Step 6: Commit**
```powershell
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**
```python
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:
```powershell
uv run pytest tests/test_offline_packaging.py -q
```
Expected: FAIL because the Inno script does not exist.
- [ ] **Step 3: Create the Inno script**
```ini
[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:
```powershell
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:
```powershell
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**
```powershell
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:
```markdown
## 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`:
```markdown
### 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:
```powershell
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:
```powershell
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**
```powershell
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.