Packaged API boots outside the repository root (OGUR-66)¶
Verdict: IN-MVP-1 · Estimate: 3 half-days · Status: DELIVERED — shipped in PR #240, released in v1.1.0 Serves gate: §1 MVP-1 — "follow an authenticated link": no link exists unless the API deploys, and §8 records it cannot boot from a wheel Scope contract: §2 item 2 — "One deployment, one URL, serving both packs"; §2 Checkpoints 1 — "packaged API boots outside the repository root" (due Aug 10) Date: 2026-08-10
Why this is in scope¶
This is a named §8 blocker on the deployment gate (OGUR-59, due Aug 11), and the scope contract's own checkpoint schedule lists "packaged API boots outside the repository root" as an Aug 10 deliverable. The wheel-boot proof from a temporary directory is the correct acceptance shape — §8 notes the defect is invisible when running from repo root, so any check that runs from repo root proves nothing. The stated scope restriction ("strictly to packaging") is consistent with §2's forecast warning that "any new feature, broad cleanup" misses Aug 13; hold the implementer to it.
Boundary: packaging, not pipeline correctness. If moving
run_drug_briefing_pipeline tempts a refactor of the briefing pipeline itself,
that is §3 territory — the move exists only so the import at
ogur/api/routes/briefings.py:14 resolves from an installed wheel.
proposal: Move run_drug_briefing_pipeline into the ogur/ package, drop runtime archived_data dependence, and prove a built wheel boots create_app() from a temp directory — packaging only.
verdict: IN-MVP-1
milestone: MVP-1
scope_citation: '§2 item 2 "One deployment, one URL, serving both packs"; §2 Checkpoints 1 "packaged API boots outside the repository root"; §8 "API cannot boot from a wheel… Invisible today because uvicorn from repo root works" (ogur/api/routes/briefings.py:14; pyproject.toml:60)'
user_evidence: null
reversal_condition: 'A founder §9 cut or §6.1 amendment removing the one-URL deployment gate from MVP-1 — nothing short of that; this is a named §8 blocker on a §2 line'
disposition: SPEC
What exists / what's new¶
Exactly one from scripts. import inside ogur/:
ogur/api/routes/briefings.py:14 → run_drug_briefing_pipeline, used as a
BackgroundTasks target at briefings.py:61. Routers import inside
create_app() (ogur/api/app.py:22) and app.py:32 runs app = create_app()
at module level, so the failure fires on import ogur.api.app.
archived_data runtime reads reachable from create_app(): only
ogur/config.py:33-35 — the _REPO_ROOT-derived default for
explore_report_pack_path (lazy per-request read; fails as 503
ReportPackUnavailable via report_pack.py:95-128, never blocks boot). In a
wheel, _REPO_ROOT resolves into site-packages, so the default is already
dead in the deployed case. EXPLORE_REPORT_PACK_PATH already overrides.
ogur/engine/discovery_modality/cache.py:60 also derives from _REPO_ROOT but
is not read at boot — out of scope.
| What | Where | Reusable as-is? |
|---|---|---|
| The pipeline function itself | scripts/briefing/generate_drug_briefing.py:38 |
yes — pure move, zero logic change |
| Multi-pack env override + 503 fail-closed | ogur/api/report_pack.py:76-80, 95-128 |
yes — the archived_data fix is deleting a default, not building config |
| Sibling landscape pipeline (placement precedent) | ogur/engine/pipeline.py::run_briefing_pipeline |
yes |
| Existing tests of the function | tests/unit/engine/test_drug_briefing.py:16,98,150,171 |
needs import-path change + conftest.py:80 patch string |
| Genuinely new | Where |
|---|---|
ogur/engine/drug_briefing.py (moved code) |
new module in existing engine package |
| Wheel-boot smoke | scripts/smoke/smoke_wheel_boot.sh |
explore_report_pack_path default change |
ogur/config.py:33-35 |
Wheel ships ogur/ only (pyproject.toml:59-60); pack JSONs stay deployment
artifacts configured via EXPLORE_REPORT_PACK_PATH, not package data.
Architectural seam & collisions¶
Move within the existing engine seam — scripts/briefing/generate_drug_briefing.py:38-136
imports only ogur.engine.* and ogur.store.* and mirrors
pipeline.py::run_briefing_pipeline; a sibling module ogur/engine/drug_briefing.py
keeps the diff reviewable. No new seam, no ADR.
Thin wrapper: scripts/briefing/generate_drug_briefing.py keeps its __main__
block (argv parsing, usage text) and module-level logging.basicConfig (must NOT
move into ogur/), re-exporting run_drug_briefing_pipeline for back-compat
(scripts/briefing/analyze_drug.py:85, scripts/smoke/smoke_drug_briefing.sh).
Patch-where-used ripple: tests/conftest.py:80 patch string
scripts.briefing.generate_drug_briefing.get_session →
ogur.engine.drug_briefing.get_session, else the in-memory-SQLite fixture
silently stops covering the moved copy.
Collisions (checked gh pr list, git worktree list): no open PR touches
briefings.py or report_pack.py. Stale April drafts (#82, #106, #66, #63,
30, #18, #129) overlap generate_drug_briefing.py / config.py /¶
pyproject.toml additively only. No sequencing blocker.
Surface contract¶
No surface impact. No route, page, component, or schema change. Invariants:
- Route paths and status codes unchanged (
/briefing/{id}/drug/{name}404s,POST .../generate202). ogur/api/schemas.pyuntouched.- §2.3 pack byte-identity unbroken — the config default change must not change
which pack is served;
meta.source_digestscompare is the evidence.
No new claims displayed → no new <SourceChip> / <ConfidenceBadge> (the
legitimate no-provenance case).
Acceptance criteria¶
| # | Criterion | How it's checked | Layer |
|---|---|---|---|
| 1 | Zero scripts imports under ogur/ |
rg -n 'from scripts\.\|^import scripts' ogur/ exits 1 |
grep in smoke |
| 2 | Built wheel imports the app off-repo-root | smoke_wheel_boot.sh: uv build → fresh temp venv → pip install dist/ogur-*.whl → cd "$TMPDIR" → python -c "from ogur.api.app import app" exits 0 |
e2e smoke |
| 3 | Negative control — smoke env cannot lie | Same venv/cwd: python -c "import scripts" exits non-zero |
e2e smoke |
| 4 | Booted app serves /health 200 |
Same env: TestClient(app) → GET /health → 200 |
e2e smoke |
| 5 | Missing pack → 503, never 500, never blocks boot | Same env, env -u EXPLORE_REPORT_PACK_PATH: report route → 503 ReportPackUnavailable; /health still 200; new default must not resolve into site-packages |
e2e smoke |
| 6 | Env override still wins | In-repo with EXPLORE_REPORT_PACK_PATH set: report route → 200; existing explore-report tests green unmodified |
unit |
| 7 | §2.3 byte-identity | tests/unit/api/test_report_pack_integrity.py::test_source_digests_match_the_artifacts_on_disk passes unmodified |
unit |
| 8 | Re-export is a move, not a copy | New test: wrapper import is ogur.engine.drug_briefing.run_drug_briefing_pipeline (identity, not equality) |
unit |
| 9 | Pipeline tests follow the move | test_drug_briefing.py:16 imports new module; conftest.py:80 patch string updated; full suite green at ~1,932 baseline |
unit |
| 10 | Route surface unchanged | app.openapi() paths+status-codes diff before/after is empty; git diff --stat ogur/api/schemas.py empty |
unit + grep |
| 11 | Lint gate | make lint (never make fmt) |
lint |
Regression gap named by qa: nothing exercises trigger_drug_briefing's task
target (briefings.py:56-62). Add one test patching
ogur.api.routes.briefings.run_drug_briefing_pipeline (patch-where-used) and
asserting the 202 path schedules it.
False positives¶
| Looks like success | Actually is | Discriminator |
|---|---|---|
| Wheel smoke green from repo root | cwd puts repo on sys.path; wheel completeness never tested |
Criterion 2's cd "$TMPDIR" + criterion 3's import scripts must-fail probe |
Green via pip install -e . / reused venv |
.pth points back at the source tree |
Smoke installs dist/*.whl into a venv it creates; criterion 3 re-verifies |
| Report route 200 off-root "proving" the default fix | EXPLORE_REPORT_PACK_PATH leaked from .env/shell (known repo trap) |
Criterion 5 runs with env -u and asserts 503, not 200 |
| Re-export test green after copy-paste "move" | Two divergeable copies | Criterion 8 asserts is; criterion 1 asserts no scripts import remains |
/health 200 read as "boot works" |
Lazy-import regression hiding behind an unhit route | Criterion 5 exercises the report route, the path reading the changed default |
| Full suite green read as "trigger endpoint survived" | No test exercises the task target | The new patch-where-used test above |
Human judgement required¶
| Question | Who decides | Against what |
|---|---|---|
Is smoke_wheel_boot.sh wired into CI or left a manual pre-deploy gate? |
Founder / tech-lead | OGUR-59; §2 checkpoint 1 implies it runs at least once before Aug 11 |
Off-root default for explore_report_pack_path: empty (fail-closed 503) vs repo-root probe that keeps make dev working? |
Tech-lead | §8 deployment shape (packs baked into image, path via env); criterion 5 must hold either way |
Open questions¶
None blocking. Status READY.