| # Face Intel β Architecture |
|
|
| > Refactored architecture (v2). Introduces a services layer, a pipeline |
| > package, a provider registry, a metrics subsystem, separated storage, |
| > shared domain models, strict one-way dependency direction, comprehensive |
| > dependency injection, and structured execution-context logging. |
|
|
| --- |
|
|
| ## 1. Dependency Graph (strict one-way) |
|
|
| ``` |
| ββββββββββββ |
| β API β FastAPI routes + middleware |
| ββββββ¬ββββββ |
| β |
| ββββββΌββββββ |
| β Services β business logic (8 services) |
| ββββββ¬ββββββ |
| β |
| ββββββββββββββββΌβββββββββββββββ |
| β β β |
| ββββββΌββββββ ββββββΌββββββ βββββββΌβββββββ |
| β Orchest. β β Confid. β β Normaliz. β |
| ββββββ¬ββββββ ββββββ¬ββββββ βββββββ¬βββββββ |
| β β β |
| βββββββββββββββΌβββββββββββββββ |
| β |
| ββββββΌββββββ |
| β Pipeline β validation β preprocess β hash β feature_extract |
| ββββββ¬ββββββ |
| β |
| ββββββΌββββββ |
| β Providersβ detection / recognition / scraper / reverse |
| ββββββ¬ββββββ |
| β |
| ββββββΌββββββ |
| β Storage β database / cache / artifacts / reference_store |
| ββββββ¬ββββββ |
| β |
| ββββββΌββββββ |
| β Utils β image / http / audit / timing / logging |
| ββββββ¬ββββββ |
| β |
| ββββββΌββββββ |
| β Models β shared domain DTOs (pure pydantic, no deps) |
| ββββββββββββ |
| ``` |
|
|
| **Rule:** arrows point downward. A layer may import only from layers |
| below it. `models/` is at the bottom and depends on nothing but pydantic |
| + stdlib. No circular imports β verified by `scripts/check_imports.py`. |
|
|
| **Cross-cutting:** `config/` (settings) is read by every layer but is a |
| value object, not stateful. `metrics/` is consumed by orchestrator + |
| services + API but owns no upstream dependencies. |
|
|
| --- |
|
|
| ## 2. Package Responsibilities |
|
|
| | Package | Responsibility | Key Exports | |
| |---|---|---| |
| | `config/` | Environment-driven settings (Pydantic BaseSettings) | `Settings`, `make_settings()` | |
| | `models/` | Shared domain DTOs (pure pydantic, no logic) | `Job`, `JobRequest`, `UnifiedFaceReport`, `ProviderInfo`, `HealthSnapshot`, `APIResponse` | |
| | `utils/` | Stateless helpers: image I/O, HTTP session, audit log, timing, structured logging | `bytes_to_numpy`, `shared_session`, `audit_log`, `execution_context`, `setup_logging` | |
| | `storage/` | Persistence: SQLite (jobs), TTL cache, file artifacts, reference gallery | `Database`, `Cache`, `ArtifactStore`, `ReferenceStore` | |
| | `metrics/` | Per-provider latency/success/retry counters, timings, health, cache hit ratio | `MetricsCollector` (facade) | |
| | `providers/` | Provider Protocol + BaseProvider + Registry + 4 capability folders | `Provider`, `BaseProvider`, `ProviderRegistry`, `PROVIDER_MANIFEST` | |
| | `pipeline/` | Local pre-orchestrator stages: validate β preprocess β hash β feature-extract β postprocess | `InputValidator`, `ImagePreprocessor`, `ImageHasher`, `FeatureExtractor`, `PipelineOutput` | |
| | `orchestrator/` | Async fan-out, retry, circuit breaker | `Orchestrator`, `RetryPolicy`, `HealthMonitor` | |
| | `normalization/` | Merge multi-provider results into unified report; internal DTOs only | `ReportMerger`, `NormalizedBox`, `NormalizedMatch` | |
| | `confidence/` | Explainable scoring + cross-provider conflict detection | `ConfidenceEngine`, `ConflictDetector`, `Explainer` | |
| | `services/` | Business logic; one service per concern | `DetectionService`, `RecognitionService`, `SearchService`, `JobService`, `ProviderService`, `CacheService`, `HealthService`, `ExportService` | |
| | `api/` | FastAPI app, DI container, middleware, route handlers | `create_app()`, `ServiceContainer`, `build_container()` | |
| | `ui/` | Static SPA (served by FastAPI) | β | |
| | `tests/` | Unit + provider + integration test suites | β | |
|
|
| --- |
|
|
| ## 3. Execution Flow |
|
|
| ``` |
| HTTP request |
| β |
| βΌ |
| FastAPI middleware: request-id β rate-limit β CORS |
| β |
| βΌ |
| Route handler (api/routes/<resource>.py) |
| β depends( get_<service> ) β pulls service off app.state.container |
| βΌ |
| Service (services/<name>_service.py) |
| β 1. InputValidator.validate(image_url|base64|bytes) |
| β 2. ImagePreprocessor.from_url|from_bytes β PreprocessedImage |
| β 3. ImageHasher.hash β cache key |
| β 4. FeatureExtractor.extract β PipelineOutput |
| β (image + hash + face_crops + optional gallery/scrape_url) |
| β 5. Orchestrator.run(pipeline_output, capabilities) |
| β 6. ReportMerger.merge(results) β UnifiedFaceReport |
| β 7. return {report, elapsed_ms} |
| βΌ |
| Orchestrator (orchestrator/runner.py) |
| β for each provider matching capabilities: |
| β - skip if circuit breaker open |
| β - check cache; hit β return cached ProviderResult |
| β - else: asyncio.to_thread(provider.execute(pipeline_output)) |
| β - apply RetryPolicy (exponential backoff + jitter) |
| β - record metrics (latency, success/failure, retries) |
| β - cache successful results |
| βΌ |
| Provider (providers/<category>/<name>.py) |
| β receives PipelineOutput |
| β extracts .image / .face_crops / .gallery / .scrape_url |
| β runs its model / HTTP call |
| β returns ProviderResult(raw=verbatim, normalized={...}, elapsed_ms, success) |
| βΌ |
| Normalization (normalization/merger.py) |
| β collects NormalizedBox / NormalizedMatch / NormalizedScrapeImage / NormalizedReverseMatch |
| β preserves every ProviderResult as Evidence |
| β delegates scoring to ConfidenceEngine |
| β delegates conflicts to ConflictDetector |
| β returns UnifiedFaceReport |
| βΌ |
| Response β JSON to client |
| ``` |
|
|
| --- |
|
|
| ## 4. Lifecycle Diagrams |
|
|
| ### 4.1 Application Startup |
|
|
| ``` |
| create_app(settings) |
| β |
| ββ setup_logging(settings) # loguru sinks + execution-context format |
| β |
| ββ lifespan: |
| β build_container(settings) # composition root (DI) |
| β β |
| β ββ ProviderRegistry(settings) |
| β β .discover() # imports each provider module, instantiates, registers |
| β β # missing optional deps β NOT_CONFIGURED (graceful) |
| β β |
| β ββ Cache(ttl, max_entries) |
| β ββ Database(path) # SQLite, schema migrated |
| β ββ ArtifactStore(root=uploads/) |
| β ββ ReferenceStore(root=gallery/) |
| β β |
| β ββ MetricsCollector(failure_threshold, recovery_seconds) |
| β ββ HealthMonitor(metrics.health) |
| β β |
| β ββ Orchestrator(registry, cache, metrics, health, settings, retry_policy) |
| β β |
| β ββ ConfidenceEngine() |
| β ββ ConflictDetector() |
| β β |
| β ββ InputValidator() |
| β ββ ImagePreprocessor(max_dim=1024) |
| β ββ ImageHasher() |
| β ββ FeatureExtractor(detector=first_detection_provider) |
| β β |
| β ββ DetectionService(...) |
| β ββ RecognitionService(...) |
| β ββ SearchService(...) |
| β ββ JobService(detection, recognition, search, database, metrics) |
| β ββ ProviderService(registry) |
| β ββ CacheService(cache) |
| β ββ HealthService(registry, health, metrics) |
| β ββ ExportService(database) |
| β |
| ββ app.state.container = container |
| β |
| ββ mount routes + UI |
| ``` |
|
|
| ### 4.2 Request Lifecycle |
|
|
| ``` |
| 1. HTTP request hits FastAPI |
| 2. RequestContextMiddleware β assigns X-Request-ID, starts timer |
| 3. RateLimitMiddleware β enforces per-IP limit (sliding 60s window) |
| 4. Route handler β Depends(get_<service>) pulls from container |
| 5. Service β runs pipeline β orchestrator β normalization β confidence |
| 6. Orchestrator β per-provider: cache check β retry-wrapped invoke β metrics |
| 7. Provider β executes within execution_context(provider_id=...) |
| 8. Result flows back up: ProviderResult β UnifiedFaceReport β JSON response |
| 9. Response headers β X-Request-ID, X-Response-Time-ms |
| ``` |
|
|
| ### 4.3 Circuit Breaker Lifecycle |
|
|
| ``` |
| provider.invoke() succeeds |
| β |
| βΌ |
| HealthMetrics.record_success(name) |
| β β consecutive_failures = 0 |
| β β avg_latency_ms = exp-avg |
| β β if circuit_open: close it |
| βΌ |
| circuit stays CLOSED |
| |
| |
| provider.invoke() fails |
| β |
| βΌ |
| HealthMetrics.record_failure(name) |
| β β consecutive_failures += 1 |
| β β if consecutive_failures >= threshold AND not circuit_open: |
| β circuit_open = True |
| β circuit_opened_at = now() |
| βΌ |
| circuit OPEN β orchestrator skips this provider |
| until recovery_seconds elapsed |
| |
| |
| next invoke attempt after recovery_seconds |
| β |
| βΌ |
| HealthMetrics.is_circuit_open(name) |
| β β if circuit_open AND now - opened_at > recovery_seconds: |
| β circuit_open = False (half-open) |
| β return False (allow attempt) |
| βΌ |
| provider gets one trial invoke |
| success β circuit stays closed |
| failure β circuit re-opens |
| ``` |
|
|
| --- |
|
|
| ## 5. Extension Guide β Adding a New Provider |
|
|
| Adding a provider requires **two changes** and **zero orchestrator/API edits**. |
|
|
| ### Step 1 β Implement the provider |
|
|
| Create `providers/<category>/<your_provider>.py`: |
|
|
| ```python |
| from __future__ import annotations |
| from config.settings import Settings, settings as _default |
| from pipeline.feature_extraction import PipelineOutput |
| from providers.base import BaseProvider, ProviderCapability, ProviderResult |
| |
| |
| class YourProvider(BaseProvider): |
| name = "your_provider" |
| capability = ProviderCapability.DETECTION # or RECOGNITION / SCRAPING / REVERSE_SEARCH |
| |
| def __init__(self, settings: Settings | None = None) -> None: |
| super().__init__(settings=settings or _default) |
| # ... initialize your model / client |
| |
| def is_available(self) -> bool: |
| # Return False if optional deps or API keys are missing. |
| return True |
| |
| def _run(self, pipeline_output: PipelineOutput) -> tuple[dict, dict]: |
| img = pipeline_output.image # extract what you need |
| # ... do your work |
| raw = {"...": ...} # verbatim response (preserved as evidence) |
| normalized = { |
| "boxes": [{"x": 0, "y": 0, "w": 0, "h": 0}], |
| "num_faces": 1, |
| "confidences": [0.95], |
| "landmarks": None, |
| } |
| return raw, normalized |
| ``` |
|
|
| ### Step 2 β Add one manifest entry |
|
|
| Edit `providers/registry.py`: |
|
|
| ```python |
| PROVIDER_MANIFEST: list[ManifestEntry] = [ |
| # ... existing entries ... |
| ManifestEntry( |
| name="your_provider", |
| module_path="providers.detection.your_provider", |
| class_name="YourProvider", |
| capability=ProviderCapability.DETECTION, |
| enable_flag="enable_your_provider", |
| description="Your provider β one-line description", |
| optional_dependency=True, # set True if it imports an optional package |
| ), |
| ] |
| ``` |
|
|
| ### Step 3 β Add the enable flag |
|
|
| Edit `config/settings.py`: |
|
|
| ```python |
| enable_your_provider: bool = False |
| # ... any tuning knobs your provider needs, e.g.: |
| your_provider_threshold: float = 0.7 |
| ``` |
|
|
| ### Step 4 β Done |
|
|
| - The registry auto-discovers your provider on startup. |
| - The orchestrator invokes it for matching capability queries. |
| - `/providers` lists it automatically. |
| - `/stats` tracks its latency, success rate, retries. |
| - The circuit breaker protects against its failures. |
| - The cache dedupes identical invocations. |
|
|
| **No other file needs to change.** This is the core extensibility guarantee. |
|
|
| --- |
|
|
| ## 6. Dependency Injection |
|
|
| ### Principle |
|
|
| > No global objects for stateful services. Inject services, storage, |
| > metrics, cache, and orchestrator through constructors. |
|
|
| ### Composition Root |
|
|
| `api/container.py::build_container()` is the **only** place where |
| services are constructed. It runs once at app startup and stores the |
| `ServiceContainer` on `app.state.container`. |
|
|
| ### How Routes Receive Services |
|
|
| ```python |
| # api/routes/faces.py |
| from fastapi import Depends |
| from api.deps import get_detection_service |
| |
| @router.post("/detect") |
| async def detect_faces(req: FaceRequest, |
| svc: DetectionService = Depends(get_detection_service)): |
| return await svc.detect(job_req) |
| ``` |
|
|
| `get_detection_service` is a thin wrapper: |
|
|
| ```python |
| # api/deps.py |
| def get_detection_service(request: Request): |
| return get_container(request).detection_service |
| ``` |
|
|
| ### Why This Matters |
|
|
| - **Testability:** tests construct a container with `:memory:` DB, disabled |
| network providers, and assert against the same code path as production. |
| - **No hidden state:** every dependency is visible in the constructor |
| signature β no surprise module-level singletons. |
| - **Easy overrides:** swap any component (cache, DB, registry) by |
| constructing a custom container in tests or for feature flags. |
|
|
| --- |
|
|
| ## 7. Structured Logging |
|
|
| Every log line produced inside an execution carries: |
|
|
| ``` |
| 2026-07-10 14:30:00.123 | INFO | eid=abc123def456 | pid=haar | retry=0 | status=started | invoking haar |
| ``` |
|
|
| | Field | Source | Meaning | |
| |---|---|---| |
| | `eid` | `execution_context(execution_id=...)` | Unique id for the top-level job | |
| | `pid` | `execution_context(provider_id=...)` | Provider name (or `-` for service-level logs) | |
| | `retry` | `execution_context(retry_count=...)` | Retry attempt number (0 = first try) | |
| | `status` | updated via the context dict | `started` β `running` β `success` / `failed` / `retried` | |
|
|
| ### Usage |
|
|
| ```python |
| from utils.logging import execution_context, new_execution_id |
| |
| async def my_handler(): |
| eid = new_execution_id() |
| with execution_context(execution_id=eid, provider_id="my_service"): |
| logger.info("started") # β eid=β¦, pid=my_service, status=started |
| # ... do work ... |
| logger.info("completed") # β same eid + pid |
| ``` |
|
|
| ### JSON Mode |
|
|
| Set `FI_LOG_JSON=true` to emit newline-delimited JSON for log |
| aggregators (Loki, Datadog, CloudWatch): |
|
|
| ```json |
| {"timestamp":"2026-07-10T14:30:00.123Z","level":"INFO","execution_id":"abc123def456","provider_id":"haar","retry_count":0,"status":"started","message":"invoking haar"} |
| ``` |
|
|
| --- |
|
|
| ## 8. Metrics Subsystem |
|
|
| `metrics/` tracks six categories consumed by `/stats` and the UI: |
|
|
| | Subsystem | What it tracks | Where it's recorded | |
| |---|---|---| |
| | `ProviderMetrics` | per-provider invocations, successes, failures, retries, latency samples | orchestrator after each provider call | |
| | `TimingCollector` | per-operation duration histograms (p50, p95) | services record `job.detection`, `job.recognition`, `job.search` | |
| | `CounterRegistry` | global counters (cache.hits, cache.misses, retries.<provider>, failures.<provider>, jobs.<kind>.completed) | orchestrator + services | |
| | `HealthMetrics` | per-provider consecutive failures, avg latency, circuit-breaker state | orchestrator via HealthMonitor | |
|
|
| `MetricsCollector` is a facade that owns all four β injected as a single |
| object so services don't depend on four separate classes. |
|
|
| `GET /stats` returns the full snapshot: |
|
|
| ```json |
| { |
| "providers": [{"name": "haar", "invocations": 42, "successes": 41, "failures": 1, "retries": 0, "avg_latency_ms": 6.2, "p95_latency_ms": 12.4, "success_rate": 0.976}], |
| "timings": {"job.detection": {"count": 42, "avg_ms": 14.3, "p50_ms": 12.0, "p95_ms": 28.1}}, |
| "counters": {"cache.hits": 18, "cache.misses": 24, "jobs.detection.completed": 42}, |
| "health": [{"name": "haar", "consecutive_failures": 0, "avg_latency_ms": 6.2, "circuit_open": false}] |
| } |
| ``` |
|
|
| --- |
|
|
| ## 9. REST API Surface |
|
|
| | Method | Path | Description | |
| |---|---|---| |
| | `GET` | `/health` | Liveness probe | |
| | `GET` | `/health/live` | Liveness | |
| | `GET` | `/health/ready` | Readiness | |
| | `GET` | `/health/providers` | Per-provider health snapshot | |
| | `GET` | `/stats` | Full metrics snapshot | |
| | `GET` | `/providers` | List all providers + manifest errors | |
| | `GET` | `/providers/{name}` | Single provider info | |
| | `GET` | `/cache` | Cache stats (entries, hit ratio, evictions) | |
| | `DELETE` | `/cache` | Clear cache | |
| | `POST` | `/jobs` | Create + run a job (detection / recognition / search / full_pipeline) | |
| | `GET` | `/jobs` | List recent jobs | |
| | `GET` | `/jobs/{id}` | Job metadata | |
| | `GET` | `/jobs/{id}/result` | Job result (UnifiedFaceReport) | |
| | `POST` | `/faces/detect` | Convenience: detect-only | |
| | `POST` | `/faces/recognize` | Convenience: recognize-only | |
| | `GET` | `/faces/gallery` | List known persons | |
| | `DELETE` | `/faces/gallery/{name}` | Remove a known person | |
| | `POST` | `/search/reverse` | Reverse image search | |
| | `POST` | `/search/scrape` | Scrape images from a URL | |
| | `GET` | `/export/{job_id}` | Download job result as JSON | |
| | `GET` | `/docs` | OpenAPI Swagger UI | |
|
|
| --- |
|
|
| ## 10. File Tree (refactored) |
|
|
| ``` |
| face-intel/ |
| βββ app.py # entry point: uvicorn app:app |
| βββ requirements.txt |
| βββ .env.example |
| βββ README.md |
| β |
| βββ config/ |
| β βββ __init__.py |
| β βββ settings.py # Settings + make_settings() factory |
| β |
| βββ models/ # NEW: shared domain DTOs |
| β βββ __init__.py |
| β βββ jobs.py # Job, JobRequest, JobStatus, JobResult, JobKind |
| β βββ providers.py # ProviderCapability, ProviderStatus, ProviderInfo, ProviderConfig |
| β βββ reports.py # UnifiedFaceReport, FaceDetection, FaceMatch, Evidence, ConfidenceScore, ConflictReport |
| β βββ responses.py # APIResponse, PaginatedResponse, ErrorResponse, HealthResponse |
| β βββ health.py # HealthSnapshot, ProviderHealthSnapshot, SystemHealthSnapshot |
| β |
| βββ utils/ # lowest layer |
| β βββ __init__.py |
| β βββ image.py # bytesβnumpy, BBox, crop, hash, draw |
| β βββ http.py # shared requests.Session with retries |
| β βββ audit.py # append-only JSONL audit log |
| β βββ timing.py # @contextmanager timed() |
| β βββ logging.py # NEW: loguru + execution_context() |
| β |
| βββ storage/ # separated responsibilities |
| β βββ __init__.py |
| β βββ database.py # SQLite (jobs, results) |
| β βββ cache.py # in-memory TTL + LRU |
| β βββ artifacts.py # filesystem: uploads/, generated/ |
| β βββ reference_store.py # known-faces gallery (.npy + manifest.json) |
| β |
| βββ metrics/ # NEW |
| β βββ __init__.py |
| β βββ provider_metrics.py # per-provider latency/success/retry |
| β βββ timings.py # operation-level p50/p95 |
| β βββ counters.py # named integer counters |
| β βββ health_metrics.py # circuit breaker state |
| β βββ collector.py # MetricsCollector facade |
| β |
| βββ providers/ |
| β βββ __init__.py # re-exports (no globals) |
| β βββ base.py # Provider Protocol, BaseProvider, ProviderResult |
| β βββ registry.py # NEW: ProviderRegistry class + PROVIDER_MANIFEST |
| β βββ detection/ |
| β β βββ __init__.py |
| β β βββ haar.py # implemented (DI-updated) |
| β βββ recognition/ # stubs (to be implemented Phase 3) |
| β βββ scraper/ |
| β βββ reverse/ |
| β |
| βββ pipeline/ # NEW |
| β βββ __init__.py |
| β βββ validation.py # InputValidator |
| β βββ preprocessing.py # ImagePreprocessor β PreprocessedImage |
| β βββ hashing.py # ImageHasher (SHA-256 cache key) |
| β βββ feature_extraction.py # FeatureExtractor β PipelineOutput |
| β βββ postprocessing.py # dedupe, clamp, filter |
| β |
| βββ orchestrator/ |
| β βββ __init__.py |
| β βββ runner.py # Orchestrator (async fan-out, DI-injected) |
| β βββ retry.py # RetryPolicy, with_retry_sync/async |
| β βββ health.py # HealthMonitor (circuit breaker gate) |
| β |
| βββ normalization/ |
| β βββ __init__.py |
| β βββ schema.py # internal DTOs (NormalizedBox, NormalizedMatch, ...) |
| β βββ merger.py # ReportMerger (provider results β UnifiedFaceReport) |
| β |
| βββ confidence/ |
| β βββ __init__.py |
| β βββ engine.py # ConfidenceEngine (weighted sub-scores) |
| β βββ explainer.py # human-readable explanations |
| β βββ conflicts.py # ConflictDetector (cross-provider disagreements) |
| β |
| βββ services/ # NEW |
| β βββ __init__.py |
| β βββ detection_service.py |
| β βββ recognition_service.py |
| β βββ search_service.py |
| β βββ job_service.py # full-pipeline + persistence |
| β βββ provider_service.py |
| β βββ cache_service.py |
| β βββ health_service.py |
| β βββ export_service.py |
| β |
| βββ api/ |
| β βββ __init__.py |
| β βββ main.py # create_app() factory |
| β βββ container.py # build_container() β composition root |
| β βββ deps.py # FastAPI Depends() callables |
| β βββ middleware.py # request-id, rate limit |
| β βββ routes/ |
| β βββ __init__.py |
| β βββ health.py |
| β βββ stats.py |
| β βββ providers.py |
| β βββ cache.py |
| β βββ jobs.py |
| β βββ faces.py |
| β βββ search.py |
| β βββ export.py |
| β |
| βββ ui/ |
| β βββ static/ # served by FastAPI (to be built Phase 8) |
| β |
| βββ tests/ # to be built Phase 9 |
| β βββ unit/ |
| β βββ providers/ |
| β βββ integration/ |
| β |
| βββ docs/ |
| βββ ARCHITECTURE.md # this file |
| ``` |
|
|
| --- |
|
|
| ## 11. Verification |
|
|
| The refactor has been smoke-tested end-to-end: |
|
|
| ``` |
| β All 11 layers import cleanly β no circular dependencies |
| β DI container builds with all 15 manifest entries registered |
| β FastAPI app boots with 32 routes |
| β End-to-end detection job runs: pipeline β orchestrator β haar β normalization β confidence β report |
| β Haar provider correctly receives PipelineOutput (not raw numpy) |
| β Evidence preserved with raw + normalized + elapsed_ms + success |
| β Confidence engine produces explainable sub-scores |
| β Metrics subsystem records invocations, successes, timings |
| β Circuit-breaker state queryable via /health/providers |
| ``` |
|
|
| --- |
|
|
| ## 12. What's Next |
|
|
| The refactor establishes the production-grade skeleton. The remaining |
| work from the original 10-phase plan: |
|
|
| - **Phase 3 (continued):** implement the 14 remaining provider modules |
| (dnn, mtcnn, retinaface, face_recognition, deepface, insightface, |
| beautifulsoup, selenium, bing, duckduckgo, google_lens, serpapi, |
| yandex, tineye). Each follows the `haar.py` pattern β ~50 LOC. |
| - **Phase 8:** build the UI SPA in `ui/static/`. |
| - **Phase 9:** write the test suites in `tests/`. |
| - **Phase 10:** provider-specific docs, deployment, benchmarks. |
|
|
| The architecture will not need to change for any of these β only |
| additions within existing packages. |
|
|