Design doc (read in full first): docs/design/view_controller_decomposition.md — §2.1 ProcessRunner, §3.3 items 4 and 5, §4 slice 8. The spike's line numbers are stale (it measured the view model at 2,926 lines; it is 3,720 today). Line references in this issue are current as of 2026-09-10.
Goal
Single-document execution moves out of DocumentRedactionViewModel into a ProcessRunner. This is the direct PythonKit call boundary and the single highest-consequence piece in either file: a regression here changes what actually gets redacted. It lands after every other service extraction and gets its own review pass.
Dependencies
Scope
Move: processDocument(_:destination:) (1175), logAdvancedSettingsSnapshot (1285), applyAdvancedSettingsEnvironment() (1298), applyMetadataSettingsEnvironment(_:context:) (1309), processDocumentWithPythonKit(...) (1384), awaitPythonOutcome(_:runner:) (1634), and the metadata counterpart scrubDocumentMetadataOnly(_:destination:) (972) and generateMetadataReport(for:runner:destination:) (658). The runner takes a DocumentItem, destination, RedactionSettings, the runner provider and the preflight closures, and returns a result (outcome + output paths) that the view model applies to item status; it does not mutate published state.
The environment-variable order must be preserved exactly and is pinned by the goldens: clearCancellationRequest → MARCUT_LOG_PATH → MARCUT_ADVANCED_* → MARCUT_METADATA_* → model readiness (LLM only) → MARCUT_SCRUB_REPORT_PATH → runner; and for scrub: MARCUT_METADATA_* → MARCUT_METADATA_ONLY → runner, METADATA_ONLY unset on return. The defer { unsetenv("MARCUT_SCRUB_REPORT_PATH") } inside the detached completion task (1530) must keep the same lifetime.
Extraction rules (design doc §3.3)
- Move code only. No logic edits beyond what Swift access rules mechanically require (a method becoming
internal on the new type instead of private on the old one) and call-site updates to go through the collaborator.
- Reviewer diffs each moved body against its old body with
git diff --color-moved to confirm nothing rode along.
- Characterization tests run unmodified except for setup/construction. An assertion that needs to change to keep passing means the extraction changed behaviour and blocks the PR.
- If a bug is found mid-extraction, land the fix as a separate PR before or after. Never combine.
Out of scope
Automation
afk — rung 0. The design doc's "manual end-to-end smoke run" (§3.3 item 5) is replaced here by the bridge-call goldens: the marcut CLI does not go through the view model, so a CLI run proves nothing about this slice, while the goldens pin every argument and env var the view model hands to Python. State this in the PR.
Acceptance criteria
- Golden harness diffs to zero across all scenarios, before and after.
- The Python suite passes (several env vars this code sets are read by
pipeline.py).
git diff --color-moved shows moves only; the reviewer confirms the env-var ordering line by line.
- Both suites pass; SwiftFormat lint clean.
Required verification
swift build --package-path src/swift/MarcutApp
swift test --package-path src/swift/MarcutApp
source .venv/bin/activate && PYTHONPATH=src/python python3 -m pytest -q
swiftformat --lint src/swift/MarcutApp/Sources src/swift/MarcutApp/Tests
swift test --package-path src/swift/MarcutApp --filter RedactionCharacterizationTests run before and after the change: zero golden diffs, no golden file modified in the PR.
Notes
Land as its own PR; do not combine with other issues in this set. Do not reopen design-spike #24. Before committing, update the affected docs/INDEX.md lines and the design doc's status in the same commit (repo docs-sync rule).
Design doc (read in full first):
docs/design/view_controller_decomposition.md— §2.1ProcessRunner, §3.3 items 4 and 5, §4 slice 8. The spike's line numbers are stale (it measured the view model at 2,926 lines; it is 3,720 today). Line references in this issue are current as of 2026-09-10.Goal
Single-document execution moves out of
DocumentRedactionViewModelinto aProcessRunner. This is the direct PythonKit call boundary and the single highest-consequence piece in either file: a regression here changes what actually gets redacted. It lands after every other service extraction and gets its own review pass.Dependencies
applyOutputArtifactsalready extracted)Scope
Move:
processDocument(_:destination:)(1175),logAdvancedSettingsSnapshot(1285),applyAdvancedSettingsEnvironment()(1298),applyMetadataSettingsEnvironment(_:context:)(1309),processDocumentWithPythonKit(...)(1384),awaitPythonOutcome(_:runner:)(1634), and the metadata counterpartscrubDocumentMetadataOnly(_:destination:)(972) andgenerateMetadataReport(for:runner:destination:)(658). The runner takes aDocumentItem, destination,RedactionSettings, the runner provider and the preflight closures, and returns a result (outcome + output paths) that the view model applies to item status; it does not mutate published state.The environment-variable order must be preserved exactly and is pinned by the goldens:
clearCancellationRequest→MARCUT_LOG_PATH→MARCUT_ADVANCED_*→MARCUT_METADATA_*→ model readiness (LLM only) →MARCUT_SCRUB_REPORT_PATH→ runner; and for scrub:MARCUT_METADATA_*→MARCUT_METADATA_ONLY→ runner,METADATA_ONLYunset on return. Thedefer { unsetenv("MARCUT_SCRUB_REPORT_PATH") }inside the detached completion task (1530) must keep the same lifetime.Extraction rules (design doc §3.3)
internalon the new type instead ofprivateon the old one) and call-site updates to go through the collaborator.git diff --color-movedto confirm nothing rode along.Out of scope
processingTasks,activeAttemptTokens(View decomposition — Slice 9: extract BatchCoordinator and close out docs #111).runEnhancedOllamaWithProgress.Automation
afk— rung 0. The design doc's "manual end-to-end smoke run" (§3.3 item 5) is replaced here by the bridge-call goldens: themarcutCLI does not go through the view model, so a CLI run proves nothing about this slice, while the goldens pin every argument and env var the view model hands to Python. State this in the PR.Acceptance criteria
pipeline.py).git diff --color-movedshows moves only; the reviewer confirms the env-var ordering line by line.Required verification
swift build --package-path src/swift/MarcutAppswift test --package-path src/swift/MarcutAppsource .venv/bin/activate && PYTHONPATH=src/python python3 -m pytest -qswiftformat --lint src/swift/MarcutApp/Sources src/swift/MarcutApp/Testsswift test --package-path src/swift/MarcutApp --filter RedactionCharacterizationTestsrun before and after the change: zero golden diffs, no golden file modified in the PR.Notes
Land as its own PR; do not combine with other issues in this set. Do not reopen design-spike #24. Before committing, update the affected
docs/INDEX.mdlines and the design doc's status in the same commit (repo docs-sync rule).