# 01 — Lifecycle: bootstrap, AppContext, current_context

The single canonical entry point that wires identity +
observability + crash reporter + excepthooks transactionally.
Replaces the five-call sequence with one call. This doc
also covers the `mgf.common.typing` Protocols (consumers meet
them through the lifecycle path).

> **Read first:** [`library_common.md`](library_common.md)
> §"Three integration shapes" — pick app vs library vs test
> before reading the API surface here.

---

## Motivation

Before , integrating the library required five distinct
calls in the right order: `configure() → setup_logging() →
setup_otel() → install_excepthook() → install_threading_excepthook()`.
Each was independently failable; partial success left the
process in an inconsistent state. Consumers copy-pasted the
boilerplate at every entry point.

`bootstrap()` is the closed-box answer: one call, transactional,
with LIFO rollback on failure. The wiring steps + their order
are the library's concern; the consumer's concern is "is my
identity set, and am I ready to do work?"

---

## Goal

A consumer's main entry point becomes one line:

```python
with mgf.common.bootstrap() as ctx:
    do_work()
```

Identity flows through `current_context()`. Logs land in the
right place. OTel spans (if enabled) carry the right
`service.name`. Crash reports identify the right app. Cleanup
on exit unwinds every wired step LIFO.

---

## Quick start — app entry point

```python
# myapp/__main__.py
import mgf.common

def main() -> int:
    # Identity auto-derives from the launching package's
    # distribution metadata when run as `python -m myapp` or
    # via a console-script entry that preserves __spec__.
    with mgf.common.bootstrap() as ctx:
        run_app_logic(ctx)
        return 0

if __name__ == "__main__":
    raise SystemExit(main())
```

Pass identity explicitly when the app isn't launchable as
`python -m`:

```python
with mgf.common.bootstrap(
    app_name="myapp",
    app_version="1.0.0",
) as ctx:
    ...
```

Tune observability via `MgfSettings` (factory shortcuts cover
the common shapes — see [`04_settings.md`](04_settings.md)):

```python
from mgf.common.settings import MgfSettings, OtelSettings

with mgf.common.bootstrap(
    settings=MgfSettings.production(
        otel=OtelSettings(
            enabled=True,
            endpoint="http://otel-collector:4318",
            sample_rate=0.05,
        ),
    ),
) as ctx:
    ...
```

---

## Public API

| Name | What it does |
|---|---|
| [`bootstrap()`](../../PUBLIC_API.md#mgfcommon) | The single canonical entry. Auto-derives identity. Returns `AppContext`. Raises `BootstrapError` on partial failure (LIFO-rolled-back). |
| [`AppContext`](../../PUBLIC_API.md#mgfcommon) | Per-tenant identity + settings. Two construction modes: bootstrap-owned (set by `bootstrap()`) and scoped (direct `AppContext(...)` for multi-tenancy). |
| [`current_context()`](../../PUBLIC_API.md#mgfcommon) | Active `AppContext` or `None`. Resolution: contextvar first (scoped sub-contexts), then process-global (bootstrap), then `None`. ~30ns. |
| [`app_name()`](../../PUBLIC_API.md#mgfcommon) / [`app_version()`](../../PUBLIC_API.md#mgfcommon) | Read identity. Same resolution as `current_context()`. |
| [`bootstrap_for_test()`](../../PUBLIC_API.md#mgfcommon) | Test-bootstrap with `MgfSettings.testing()` defaults. Optional `capture_logs=True` / `capture_spans=True` flags. |
| [`UnconfiguredWarning`](../../PUBLIC_API.md#mgfcommon) | `UserWarning` subclass; fires when identity is read before bootstrap. |
| [`configure()`](../../PUBLIC_API.md#mgfcommon) | -era legacy identity setter. **Use `bootstrap()` instead.** |
| [`BootstrapError`](../../PUBLIC_API.md#mgfcommonexceptions) | Subclass of `OperationError`. Carries `.failed_step`. |

Code: [`src/mgf/common/_lifecycle.py`](../../src/mgf/common/_lifecycle.py),
[`src/mgf/common/_test_helpers.py`](../../src/mgf/common/_test_helpers.py).

---

## Patterns

### Multi-tenancy (scoped sub-contexts)

The 5% library-in-library case. `AppContext("name", "version")`
constructed directly (no `_wiring`) pushes onto the per-task
contextvar on `__enter__`, pops on `__exit__`.

```python
with mgf.common.bootstrap(app_name="myapp") as outer:
    do_main_work()  # current_context() → outer

    with mgf.common.AppContext("plugin", "2.0"):
        run_plugin_code()  # current_context() → "plugin"

    # After scope: routes back to outer
    do_more_work()
```

Settings inheritance is explicit: scoped contexts get fresh
`MgfSettings()` defaults. To inherit, pass
`settings=current_context().settings`.

### Test bootstrap with capture

```python
import pytest
from mgf.common import bootstrap_for_test, AppContext

@pytest.fixture
def app_ctx_with_capture() -> AppContext:
    with bootstrap_for_test(
        capture_logs=True, capture_spans=True,
    ) as ctx:
        yield ctx

def test_emits_expected_log(app_ctx_with_capture: AppContext) -> None:
    do_thing_that_logs()
    messages = [r.getMessage() for r in app_ctx_with_capture.captured_logs]
    assert "expected message" in messages
```

`capture_spans=True` requires an SDK `TracerProvider` already
active (call `setup_otel(enabled=True)` first OR install one in
a session-scoped fixture). The helper does NOT auto-install one
— OTel's `set_tracer_provider` is one-shot per process and
auto-installing here would pollute other tests' fixtures.

---

## `mgf.common.typing` — the four Protocols

Consumers meet these through the lifecycle: function signatures
that accept "anything that can be cancelled" / "anything that
reports progress" use these instead of concrete classes. All
four are `runtime_checkable`.

| Protocol | Methods | Concrete impls |
|---|---|---|
| [`CancellationProtocol`](../../PUBLIC_API.md#mgfcommontyping) | `cancel()`, `is_cancelled()` | `CancellationToken` (sync), `AsyncCancellationToken` (asyncio) |
| [`ReporterProtocol`](../../PUBLIC_API.md#mgfcommontyping) | `report(message, current=0, total=0)`, `is_cancelled()` | `NullReporter`, `RecordingReporter` |
| [`LogSinkProtocol`](../../PUBLIC_API.md#mgfcommontyping) | `append_info()`, `append_error()` | (consumer-side; e.g., GUI widgets) |
| [`VaultProtocol`](../../PUBLIC_API.md#mgfcommontyping) | `get`, `set`, `delete`, `list_keys`, `backend` | See [`07_vault.md`](07_vault.md). |

```python
from mgf.common.typing import CancellationProtocol

def long_running(cancel: CancellationProtocol) -> None:
    for item in work_items():
        cancel.raise_if_cancelled() if hasattr(cancel, "raise_if_cancelled") else None
        process(item)
```

---

## Links

- **Public API:** [`PUBLIC_API.md` `mgf.common` section](../../PUBLIC_API.md#mgfcommon)
- **Engineering standards:**
  [`DESIGN_PRINCIPLES.md`](../standards/DESIGN_PRINCIPLES.md) for
  DP-09 (cancellation), DP-11 (async ownership)
- **Examples:** [`examples/06_full_app/`](../../examples/06_full_app/)
  — bootstrap in a real shape
- **Reference consumer:**
  [`example_app/src/example_app/cli.py`](../../example_app/src/example_app/cli.py)
  — bootstrap + custom subcommand integration
- **Multi-tenancy contract:** [`multi_tenancy.md`](multi_tenancy.md)
  — what scoped `AppContext` supports, what it explicitly
  doesn't, and the public-API stability promise
- **Code:**
  [`src/mgf/common/_lifecycle.py`](../../src/mgf/common/_lifecycle.py),
  [`src/mgf/common/_test_helpers.py`](../../src/mgf/common/_test_helpers.py),
  [`src/mgf/common/typing/__init__.py`](../../src/mgf/common/typing/__init__.py)
