Session closeout — 12 Sep 2026

2026-09-12 22:07 · 2 deliverable(s) · auto-closeout

GO-PACKET-prereqs-2026-09-12.md CONTEXT.md

GO-PACKET-prereqs-2026-09-12.md

11,977 bytes · modified 2026-09-12 21:46

GO Packet — Scanbot L1-L4 Prerequisites

Date: 2026-09-12. Target box: Nano, path /Users/SeanVargas/AI_OS. Nothing below has been run; this packet only documents what each step would do so Sean can approve or hold each one with a single word. Source read: SPEC.md, build/BUILD-NOTES.md, build/requirements.txt, build/scanbot/deps.py, build/scanbot/l1_redact/ocr.py, build/scanbot/l1_redact/detect.py, build/scanbot/contracts.py, build/scanbot/cli.py, and ops/scripts/vision_ocr.swift.

All four runtime-data paths below live under ~/Library/Application Support/AIOS/.... That tree is outside iCloud Drive's synced Desktop/Documents/Mobile Documents locations and outside the AIOS repo that Syncthing watches, matching SPEC §3's own runtime-root proposal.

Step 1 — Runtime venv + pip install

Disk: creates /Users/SeanVargas/Library/Application Support/AIOS/scanbot-venv/ (a Python virtualenv, several hundred MB, dominated by spaCy/thinc/numpy). Touches nothing in the repo or in requirements.txt itself.

Commands:

python3 -m venv "/Users/SeanVargas/Library/Application Support/AIOS/scanbot-venv"
VENV="/Users/SeanVargas/Library/Application Support/AIOS/scanbot-venv"
"$VENV/bin/pip" install --upgrade pip
"$VENV/bin/pip" install -r /Users/SeanVargas/AI_OS/ops/scripts/scanbot/requirements.txt

(If build/scanbot/ has not yet been promoted, point -r at the packet's build/requirements.txt instead — the venv itself doesn't care which copy it reads.)

Eight pinned packages: pymupdf==1.24.14 (imports as fitz, PDF render/rebuild), Pillow==11.0.0 (raster ops), pillow-heif==0.20.0 (HEIC decode, registers a Pillow opener), pytesseract==0.3.13 (Tesseract wrapper), presidio-analyzer==2.2.355 (NER), spacy==3.8.2 (NLP engine under Presidio), imap-tools==1.7.4 (Proton Bridge IMAP), osxphotos==0.68.4 (Photos inventory).

Compiler/Apple-framework flags: this was verified on bare python3.14.6, new enough that wheels for pymupdf, Pillow, and especially spacy (plus thinc/blis, which link Apple's Accelerate framework for BLAS on arm64) may not exist yet for cp314-macosx_arm64 — a missing wheel means a from-source build needing Xcode Command Line Tools. pillow-heif and pytesseract need Homebrew's libheif and tesseract respectively (not pip-installable; both stay non-functional until those brew installs happen separately). osxphotos is pure Python but pulls PyObjC bindings (Quartz, CoreServices) that bridge to Apple frameworks, usually as wheels. imap-tools and presidio-analyzer are pure Python.

Expected output: "$VENV/bin/pip" list shows the eight pins plus their transitive deps; db-init --root <root> (dry-run) still returns the same JSON as on bare python3, now with every deps.require() path importable.

Rollback: rm -rf "/Users/SeanVargas/Library/Application Support/AIOS/scanbot-venv" — deletes the venv only, nothing else changes.

Risk: a cp314 wheel gap on pymupdf/spacy turns a five-minute install into a stalled or failed from-source compile.

Step 2 — spaCy model for Presidio

detect.py line 39 hard-codes model='en_core_web_lg' and checks spacy.util.is_package(self.model) before use; nothing else is accepted. This is a ~590 MB network download, per requirements.txt's own comment, and is deliberately not triggered by any code path — DependencyMissing('presidio_model','local_model_missing') fires instead of ever calling the download helper.

Disk: adds an en_core_web_lg package (~590 MB installed) inside the venv's site-packages/, nowhere else.

Commands:

"$VENV/bin/python3" -m spacy download en_core_web_lg
"$VENV/bin/python3" -c "import spacy; print(spacy.util.is_package('en_core_web_lg'))"

Offline alternative: on a networked machine, fetch the wheel matching spaCy 3.8.x (e.g. en_core_web_lg-3.8.0-py3-none-any.whl from the explosion/spacy-models GitHub releases), carry the file over by hand, then "$VENV/bin/pip" install /path/to/en_core_web_lg-3.8.0-py3-none-any.whl — a local wheel install registers the same importable package name and satisfies is_package() with no network call on Nano itself.

Expected output: the is_package check prints True; scanbot.cli detect dry-run JSON flips "presidio": false to "presidio": true.

Rollback: "$VENV/bin/pip" uninstall -y en_core_web_lg.

Risk: an interrupted 590 MB transfer can leave a half-installed package that still answers is_package() incorrectly, so verify with the one-liner above before trusting it.

Step 3 — Box-emitting Vision helper (spec only; no Swift written)

Today's ops/scripts/vision_ocr.swift (1.4 KB, compiled to the 67 KB vision_ocr binary) runs VNRecognizeTextRequest at .accurate and prints observations.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n") — plain text, one line per Vision observation, nothing else. ocr.py's vision_words() (lines 39-58) is how the adapter tells today's text-only output from tomorrow's box-emitting output: it always attempts json.loads(result.stdout); against today's binary that raises a JSONDecodeError (a ValueError subclass), which the adapter catches and turns into DependencyMissing('vision_boxes','helper_text_only'), then falls back to Tesseract. That fallback path keeps working unchanged until the helper is actually replaced, so this step is reversible by design.

The JSON the adapter requires (validate_words, lines 27-36, and the Word/OCRPage dataclasses in contracts.py): one object on stdout, nothing else —

{"schema_version": 1, "coordinate_space": "display_pixels",
 "words": [{"text": str, "box": [x0,y0,x1,y1], "block": int, "line": int, "order": int, "confidence": float}]}

box must be top-left-origin, y-down pixel coordinates within [0,width]×[0,height] of the exact PNG the helper was handed — the same convention as image.width/image.height on the Python side. confidence in [0,1]. block/line/order only drive reconstruct()'s sort-and-join (newline between differing (block,line), space within); block can stay 0 and line can be a simple top-to-bottom index over Vision's observations.

Swift changes needed, as a spec: (1) for word-level boxes, split each observation's recognized string on whitespace and call VNRecognizedText.boundingBox(for:) on each word's Range<String.Index> instead of only the line-level box; skip a word whose lookup throws rather than fabricate one. (2) Vision returns normalized, bottom-left-origin quads (0...1, y-up); convert each word's min/max corners to pixels with x0=minXwidth, x1=maxXwidth, y0=(1-maxY)height, y1=(1-minY)height, clamp into bounds, drop non-positive results. (3) apply the observation's own confidence to every word split from it — Vision has no sub-line confidence. (4) no rotation handling needed in Swift: render_bytes() already bakes PDF /Rotate and EXIF orientation into the raster before the helper sees it. (5) print only the JSON object to stdout, diagnostics to stderr, keep today's exit codes — ocr.py treats any nonzero exit as vision_failed regardless of message text.

Disk: none from approving this block alone — it's a design, not a build. Once implemented, it replaces ops/scripts/vision_ocr and vision_ocr.swift (and the mirrored copy under ~/aios/ops/scripts/), which sit inside the Syncthing-watched repo; that's fine because this tool is stateless code with no PII, unlike the runtime-data paths above. Per this agent's own ops/ edit gate, that swap needs AIOS_PROTECTED_OK=1 and a dated .bak-2026-09-12 copy of both files before overwrite.

Commands (for the later implementation, not run now): back up first — cp ops/scripts/vision_ocr ops/scripts/vision_ocr.bak-2026-09-12 and same for the .swift source; compile with swiftc -O ops/scripts/vision_ocr.swift -o ops/scripts/vision_ocr; smoke-test with ./ops/scripts/vision_ocr sample.png | python3 -m json.tool.

Expected output: the smoke test above parses as valid JSON with schema_version:1, coordinate_space:"display_pixels", and a non-empty words list whose boxes fall inside the image; ocr.py's vision_words() then succeeds instead of raising DependencyMissing.

Rollback: copy the .bak-2026-09-12 files back over the live names.

Risk: a subtly wrong bottom-left-to-top-left flip still passes validate_words's range checks while burning the wrong pixels, so SPEC §5's mandatory human visual review is what actually catches it, not the schema check.

Step 4 — Pilot per SPEC §7.5

Corpus, synthetic-first, bounded: ~30 synthetic fixtures reusing SPEC §7.1's categories (scanned/native PDFs, hidden text, metadata/attachments, 90°/270° rotation, split-line phones, multiline names, faint/blank/unreadable pages, HEIC orientation, faces/signatures) — fabricated PII only, so a bug never leaks anything real. Once those pass: 10 real Proton messages (read-only, existing 50 MiB/100 MiB caps) and 15 hand-picked Photos assets covering all seven L3 categories including "uncertain." 55 items total, all hand-selected, no bulk pulls.

Commands, dry-run first (default, no flag) then --execute, from the venv:

ROOT="/Users/SeanVargas/Library/Application Support/AIOS/scanbot"
"$VENV/bin/python3" -m scanbot.cli --root "$ROOT" db-init
"$VENV/bin/python3" -m scanbot.cli --root "$ROOT" --vision-helper /Users/SeanVargas/AI_OS/ops/scripts/vision_ocr \
  redact <fixture> --mime application/pdf --asset-id <id> --out /tmp/candidate-<id>.json
"$VENV/bin/python3" -m scanbot.cli --root "$ROOT" verify /tmp/candidate-<id>.json --mime application/pdf --out /tmp/verify-<id>.json
"$VENV/bin/python3" -m scanbot.cli --root "$ROOT" proton-ingest --account pilot --folder INBOX \
  --trust-configured --username <addr> --password-env SCANBOT_PROTON_PW --limit 10
"$VENV/bin/python3" -m scanbot.cli --root "$ROOT" photos-inventory --library-id pilot --limit 15

Repeat each with --execute after the dry-run JSON confirms engine/config availability (presidio: true, vision helper configured, sync_excluded passed).

Metrics: per-page failures = count of OCRPage.status != 'succeeded', read from OCR/verify JSON. Detection misses = planted PII absent from DetectionSet on synthetic fixtures, plus reviewer-found residual PII on real output. Triage accuracy = correct TriageResult.category / total against hand-labeled ground truth for the 15 Photos assets. Latency = wall-clock via time around each CLI call (or events-table timestamps), reported p50/p95 per stage. Peak memory = /usr/bin/time -l <cmd> on macOS, reading "maximum resident set size," max across the run.

Expected output: a metrics table plus a reviewer sign-off on every rendered redacted page; zero unresolved leaks in the sample is the acceptance bar.

Rollback: rm -rf "$ROOT" then re-run db-init — this only removes local derived state; Proton ingestion is read-only against Bridge and Photos inventory never writes to the Photos library, so no source data is ever at risk.

Risk: real PII exists briefly in local OCR/detection JSON before verification runs, so every intermediate file must land inside $ROOT (never /tmp, never the repo) for the whole pilot.

Summary table

StepGO wordEst. minutesBlocks on
1. Venv + pip installGO-110-30none
2. spaCy model downloadGO-25-15GO-1
3. Vision helper JSON-box spec/swapGO-345-90none
4. Pilot (SPEC §7.5)GO-4120-180GO-1, GO-2, GO-3

Step 4 needs all three prior GOs: BUILD-NOTES flags the venv as "a hard prerequisite for any pilot" (without Presidio+spaCy, detect always returns needs_review and nothing ever reaches candidate), and the box-emitting Vision helper is BUILD-NOTES' own listed prerequisite before "pilot per SPEC §7.5."

CONTEXT.md

8,530 bytes · modified 2026-09-12 21:43

ops/scripts/scanbot — CONTEXT.md

Provenance

Built 2026-09-12 on Nano; not yet promoted, not deployed, no venv, no packages installed, no network calls made.

Builders: OpenAI Codex (codex exec, high reasoning effort) built core (config/contracts/db/deps), L1, and L2, then hit its usage-limit wall.

Claude (receiver session) built L3, L4, scanbot/cli.py, the full test suite, and requirements.txt.

Tests: 212 tests, 1 skipped (needs pymupdf + Pillow), 0 failures, on bare python3 with zero third-party packages installed.

Bug fixed 1: db.py put_asset — 7 placeholders bound against the 6-column assets table; every insert would have failed.

Bug fixed 2: l1_redact/release.py approve_visual_review — 11 placeholders against the 10-column approvals table; human approval could never be recorded. Bug fixed 3: l2_proton/ingest.py validate_filename did not reject dot-leading or >255-byte filenames.

Inputs

Working (this run) — the promoted package, its tests, and its own build log:

scanbot/cli.py, scanbot/config.py, scanbot/contracts.py, scanbot/db.py, scanbot/deps.py; scanbot/l1_redact/{ocr,patterns,detect,redact,verify,release}.py; scanbot/l2_proton/ingest.py; scanbot/l3_photos/{inventory,triage}.py; scanbot/l4_actions/{orchestrate,vcard,calendar_draft,listing_draft}.py; scanbot/migrations/001_initial.sql; requirements.txt; tests/test_*.py (11 files, 212 tests); BUILD-NOTES.md (build log, bugs fixed, known SPEC gaps, next steps).

Reference (every run) — read-only, does not move with this promotion:

comms/outputs/2026-09-09-scanbot-rebuild/SPEC.md — the approved specification; §3 storage layout, §4 contracts/ledger schema, §5 per-lane behavior and hard limits, §7 test plan, §8 build sequence. comms/outputs/2026-09-09-scanbot-rebuild/CODEX-BRIEF.md — original brief, only if build rationale/history is needed.

Do NOT load — superseded or historical, not authority for current behavior:

comms/outputs/2026-09-09-scanbot-rebuild/RESEARCH-{icloud,priorart,proton,scanbot-audit}.md — pre-SPEC research; SPEC §1 already corrected and overrides them (e.g. the disproven stored-Live-Text/detected_text assumption). comms/outputs/2026-09-09-scanbot-rebuild/codex-build-L1L4.log, codex-run.log, BUILD-PROMPT-L1L4.txt, CODEX-PROMPT-2026-09-09.txt — raw build transcripts and prompts (147–204 KB); history, not spec. reference/scanbot_pdf_redact.py, and the pre-existing scanbot_pdf_redact.py / scanbot_run_pipeline.sh / scanbot_batch_redact_extract.py / scanbot_pii_audit.py already in this directory — the retired tool and its callers (SPEC §6). Its raster-and-rebuild mechanics are already ported into scanbot/l1_redact/redact.py; the old file is kept only for md5 provenance, never a maintenance reference or import target.

Process

1. Tests, bare python3, nothing installed:

   cd ops/scripts/scanbot
   python3 -m unittest discover -s tests
   python3 -m compileall -q scanbot && echo COMPILE_OK

Expect Ran 212 tests ... OK (skipped=1) then COMPILE_OK. The one skip is PixelStackTests (needs pymupdf + Pillow, gated in step 3). Any other skip, failure, or import of a third-party module means the deps.py isolation contract broke.

2. CLI dry-run smoke. Every subcommand in scanbot/cli.py defaults to dry-run; --execute is required to act. Use a real, non-symlinked scratch root — plain mktemp -d on macOS resolves under /var, a symlink that config.py's guard correctly rejects: ROOT=$(mktemp -d /private/tmp/scanbot-smoke.XXXXXX).

SubcommandDry-run default
db-initReports would_initialize; creates nothing.
db-statusReports row counts, skips reconciliation.
ocrReports which OCR engines are configured; does not read the file.
detectReports page count and Presidio availability; runs no detection.
redactReports max_passes (hard limit: 5) and dependency availability; touches nothing.
verifyReports the candidate's recorded hash only; does not reopen the file.
release-checkReports eligibility and dispatch_available (hard limit: always false — no dispatch code path exists in this build).
proton-ingestValidates settings and reports sync_exclusion_confirmed/imap_tools; opens no socket. Live ingest is loopback-only (127.0.0.1:1143 default, certificate trust required, never CERT_NONE) and caps at 50 MiB per attachment / 100 MiB per message.
photos-inventorySkips reconciliation; reads no photo bytes.
photos-triageReports vlm: not_configured; does not read the image.
draft-vcard / draft-calendar / draft-listingReport line/field/review summaries; write no file even if --out is given.
action-stageReports whether the idempotency key is already staged; inserts nothing.
worker-run-oncePasses dry_run=True into the orchestrator; leases and executes no job.

Caveat: db-status, release-check, photos-inventory, action-stage, and worker-run-once construct the Ledger unconditionally, which creates the runtime root and state/scanbot.sqlite3 even under the dry-run default — only the stage-specific action is gated by --execute. Always pass --root "$ROOT" for these five during smoke-testing, never the default path.

3. Gated prerequisites — each NEEDS SEAN GO; none done yet:

a. NEEDS SEAN GO — project venv: python3 -m venv .venv && .venv/bin/pip install -r requirements.txt, plus brew install libheif tesseract. Project venv only, never system-wide or the AIOS runtime python.

b. NEEDS SEAN GO — spaCy model (not in requirements.txt, ~590 MB, network): python -m spacy download en_core_web_lg. Code never invokes this itself; without it detect() reports needs_review and redact() never yields a candidate.

c. NEEDS SEAN GO — box-emitting Apple Vision helper. ops/scripts/vision_ocr currently emits plain text only; the adapter raises DependencyMissing(vision_boxes, helper_text_only) and falls back to Tesseract. Vision OCR cannot support redaction until a box-emitting helper (~1.4 KB .swift source) is built and passed via --vision-helper.

Outputs

One runtime root (default ~/Library/Application Support/AIOS/scanbot/, override with --root), outside the repo, the Photos library, and any sync/cloud root — L2 and L3 both refuse to write until sync_exclusion_confirmed is set. All directories owner-only (0700/0600). Per SPEC §3:

Review reports: no rendered review UI exists yet — approve_visual_review records reviewer, reviewed pages, and destination into approvals, but renders no pixels (BUILD-NOTES gap 6). Until that UI is built, "review output" is the verifications/approvals rows in state/scanbot.sqlite3, read via scanbot db-status --execute --root <root> or a direct query — never a separate PII-bearing report file.

Human check

Before merging this promotion PR:

cd ops/scripts/scanbot && python3 -m unittest discover -s tests && python3 -m compileall -q scanbot && echo COMPILE_OK

Expect Ran 212 tests in <1s, then OK (skipped=1), then COMPILE_OK — with zero third-party packages installed. Any failure, any extra skip, or any import error blocks the merge; that clean-on-bare-python3 result is the entire point of deps.py. A pilot per SPEC §7.5 is a separate, later check that additionally needs all three GOs in Process step 3.