# Invariants, Failure Modes & Safe-Change Rules

> The closing synthesis: the hard rules you must not break and the failure modes they prevent. Dependency direction is one-way down the stack; cross-context calls go through DataHub topics or typed events, never `#include`. `AuthManager::session()` is the canonical source for Fincept credentials — `SettingsRepository` is a fallback copy only. SecureStorage is SQLite + AES-256-GCM keyed off `machineUniqueId` and requires `Database` open first; platform keychains are intentionally unused. Schema migrations under `storage/sqlite/migrations/` are forward-only. Screens are lazy-instantiated via `DockScreenRouter` factories. Use this page as a checklist before merging: which layer am I in, what state do I own, and which invariant am I at risk of violating?

- Repository: Fincept-Corporation/FinceptTerminal
- GitHub: https://github.com/Fincept-Corporation/FinceptTerminal
- Human wiki: https://grok-wiki.com/public/wiki/fincept-corporation-finceptterminal-b8b64fbc871f
- Complete Markdown: https://grok-wiki.com/public/wiki/fincept-corporation-finceptterminal-b8b64fbc871f/llms-full.txt

## Source Files

- `docs/ARCHITECTURE.md`
- `docs/CONTRIBUTING.md`
- `docs/CPP_CONTRIBUTOR_GUIDE.md`
- `docs/PYTHON_CONTRIBUTOR_GUIDE.md`
- `fincept-qt/src/auth`
- `fincept-qt/src/storage`
- `fincept-qt/src/core`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
- [docs/CPP_CONTRIBUTOR_GUIDE.md](docs/CPP_CONTRIBUTOR_GUIDE.md)
- [fincept-qt/src/auth/AuthManager.h](fincept-qt/src/auth/AuthManager.h)
- [fincept-qt/src/auth/AuthManager.cpp](fincept-qt/src/auth/AuthManager.cpp)
- [fincept-qt/src/storage/secure/SecureStorage.h](fincept-qt/src/storage/secure/SecureStorage.h)
- [fincept-qt/src/storage/secure/SecureStorage.cpp](fincept-qt/src/storage/secure/SecureStorage.cpp)
- [fincept-qt/src/storage/sqlite/Database.h](fincept-qt/src/storage/sqlite/Database.h)
- [fincept-qt/src/storage/sqlite/migrations/MigrationRunner.h](fincept-qt/src/storage/sqlite/migrations/MigrationRunner.h)
- [fincept-qt/src/storage/sqlite/migrations/MigrationRunner.cpp](fincept-qt/src/storage/sqlite/migrations/MigrationRunner.cpp)
- [fincept-qt/src/storage/sqlite/migrations/v029_secure_credentials.cpp](fincept-qt/src/storage/sqlite/migrations/v029_secure_credentials.cpp)
- [fincept-qt/src/app/DockScreenRouter.h](fincept-qt/src/app/DockScreenRouter.h)
- [fincept-qt/src/app/DockScreenRouter.cpp](fincept-qt/src/app/DockScreenRouter.cpp)
- [fincept-qt/src/app/DockScreenRouter_Materialize.cpp](fincept-qt/src/app/DockScreenRouter_Materialize.cpp)
- [fincept-qt/src/app/WindowFrame_Setup.cpp](fincept-qt/src/app/WindowFrame_Setup.cpp)
- [fincept-qt/src/datahub/DataHub.h](fincept-qt/src/datahub/DataHub.h)
</details>

# Invariants, Failure Modes & Safe-Change Rules

This is the closing synthesis page: the hard rules that hold the modular monolith together, the concrete failure modes they prevent, and a pre-merge checklist you can run against any patch. Each rule is stated together with what the code actually does today, where it lives, and what would break if you broke it. Use the table at the end as a sanity gate before opening a pull request.

Fincept Terminal is a Qt6/C++20 desktop binary with 54 lazy screens, ~50 services, 16 broker adapters, and an in-process DataHub. The invariants below are what keep that scale from collapsing into a tangle: clear dependency direction, one canonical source per piece of mutable state, and lifecycle preconditions that subsystems explicitly assert rather than hope for.

## 1. Dependency direction is one-way down the stack

The architecture is layered, and the layering is enforced by convention today and (target) by per-module CMake libraries tomorrow.

```text
Presentation  ─►  Application  ─►  Data Plane  ─►  Adapters  ─►  Infrastructure  ─►  Platform
   (screens)      (bounded ctx)     (DataHub)      (broker/MCP)   (Database/Auth)     (Qt6)
```

Rules:

- **Never reverse the arrow.** Infrastructure has no business knowledge — `Database` does not know what a "watchlist" is.
- **Adapters are leaves.** Two services may share an adapter; an adapter may not call a service.
- **Cross-context calls go through DataHub topics or typed events, never `#include`.** A Markets screen does not include a Trading header; both subscribe to topics on the hub.

The target CMake layout makes this enforceable mechanically: each layer becomes a library target that may only link against the layers below it. The current monolithic `CMakeLists.txt` (~3,300 LOC) still permits violations, which is exactly why they are tagged in the refactor plan rather than silently absent.

Failure mode if broken: circular dependency creeps in (a service `#include`s a screen header for a struct), build slows, and the layer collapses into one giant module that can't be tested in isolation.

Sources: [docs/ARCHITECTURE.md:72-78](docs/ARCHITECTURE.md), [docs/ARCHITECTURE.md:303-324](docs/ARCHITECTURE.md), [fincept-qt/src/datahub/DataHub.h:39-58](fincept-qt/src/datahub/DataHub.h)

## 2. `AuthManager::session()` is the canonical source of fincept credentials

`AuthManager` is the in-memory singleton that owns the live `SessionData` for the logged-in user. Every consumer — UI navigation chrome, MCP tools, broker chat services — reads tokens through `AuthManager::instance().session()`.

```cpp
// fincept-qt/src/auth/AuthManager.h:14-17
class AuthManager : public QObject {
    static AuthManager& instance();
    const SessionData& session() const { return session_; }
```

`SettingsRepository` holds a **fallback persistence copy** of the same JSON-serialised session blob (key `fincept_session`). `SecureStorage` additionally persists the durable `api_key` so it survives SQLite corruption or migration. Neither is a substitute for the live session struct; both are reload sources for `AuthManager::load_session()`.

| Source | Role | Lifetime |
|---|---|---|
| `AuthManager::session()` | Canonical, in-memory | Process lifetime |
| `SettingsRepository.get("fincept_session")` | Fallback JSON for cold start | Persistent (plaintext SQLite) |
| `SecureStorage.retrieve("api_key")` | Durable encrypted recovery of api_key | Persistent (AES-256-GCM) |

Invariant: **never cache fincept credentials anywhere else.** Capturing `session.api_key` into a member variable means the next `logout()` does not invalidate your copy, and the next `refresh_user_data()` leaves you stale.

Failure mode if broken: a screen or service authenticates with a revoked token after logout, the user appears logged in despite being signed out, and `SessionGuard`'s 401 auto-logout never fires for the stale caller.

Sources: [fincept-qt/src/auth/AuthManager.h:14-34](fincept-qt/src/auth/AuthManager.h), [fincept-qt/src/auth/AuthManager.cpp:69-101](fincept-qt/src/auth/AuthManager.cpp), [fincept-qt/src/auth/SessionGuard.cpp:13-46](fincept-qt/src/auth/SessionGuard.cpp), [docs/ARCHITECTURE.md:197](docs/ARCHITECTURE.md), [docs/ARCHITECTURE.md:445](docs/ARCHITECTURE.md)

## 3. SecureStorage = SQLite + AES-256-GCM keyed off `machineUniqueId`

`SecureStorage` is a SQLite-backed credential store. Values are encrypted with AES-256-GCM using a 256-bit key derived once per process from `QSysInfo::machineUniqueId()`, the OS product type, and a fixed app salt, hashed with SHA-256.

```cpp
// fincept-qt/src/storage/secure/SecureStorage.cpp:57-67
seed.append(QSysInfo::machineUniqueId());
seed.append('\x1f');
seed.append(QSysInfo::productType().toUtf8());
seed.append('\x1f');
seed.append(kAppSalt);
cached = QCryptographicHash::hash(seed, QCryptographicHash::Sha256);
```

Rows live in the `secure_credentials` table (created by migration v029) with columns `key | ciphertext | iv | tag | updated_at`. Each row carries its own random 96-bit IV and 128-bit GCM authentication tag; tag mismatch on `retrieve()` returns `"Not found"` rather than corrupted plaintext, so the recovery path (re-prompt PIN / re-login) kicks in cleanly.

### Hard invariants

1. **`Database::open()` must run first.** `store`, `retrieve`, and `remove` all begin with `if (!db.is_open()) return err("Database not open");`. Calling `SecureStorage::instance().retrieve(...)` from a static initializer or from `main()` before bootstrap will silently fail.
2. **Platform keychains are intentionally unused.** v029 explicitly replaced the old per-OS backends (Windows Credential Manager, macOS Keychain via `SecItem`, Linux libsecret/QSettings). Do not reintroduce them — the AES-256-GCM design is the chosen threat model, and a stale comment in `AuthManager::save_session()` mentioning "DPAPI / Keychain" describes intent that the v029 migration replaced.
3. **Copying the SQLite file to another machine yields unreadable ciphertext, by design.** This is the desired behaviour, not a bug.
4. **A process running as the same user on the same machine can decrypt the rows.** This matches the previous Linux fallback's threat model; do not rely on SecureStorage for cross-user isolation.

| What it protects against | What it does not |
|---|---|
| Casual `grep` of the on-disk SQLite file | Another process as the same user on the same machine |
| Profile copy moved to a different machine | Forensic tools that dump kernel memory |
| Bit-flips / partial writes (GCM tag fails) | Targeted malware that re-derives the key |

Failure mode if broken: calling `SecureStorage` before `Database::open()` returns an error that is easy to swallow, leaving the api_key absent from the session — the user appears logged out at startup despite having a valid SQLite session row. Reintroducing platform keychains would also drift the credential set across two stores and break the "one canonical encrypted location" assumption.

Sources: [fincept-qt/src/storage/secure/SecureStorage.h:6-43](fincept-qt/src/storage/secure/SecureStorage.h), [fincept-qt/src/storage/secure/SecureStorage.cpp:18-67](fincept-qt/src/storage/secure/SecureStorage.cpp), [fincept-qt/src/storage/secure/SecureStorage.cpp:195-281](fincept-qt/src/storage/secure/SecureStorage.cpp), [fincept-qt/src/storage/sqlite/migrations/v029_secure_credentials.cpp:1-50](fincept-qt/src/storage/sqlite/migrations/v029_secure_credentials.cpp), [docs/ARCHITECTURE.md:192](docs/ARCHITECTURE.md)

## 4. Schema migrations under `storage/sqlite/migrations/` are forward-only

The migration runner is a versioned, transactional, append-only ladder. There is no `down()` function on `Migration`, no `rollback()` entrypoint on `MigrationRunner`, and no downgrade path anywhere in the registry.

```cpp
// fincept-qt/src/storage/sqlite/migrations/MigrationRunner.h:14-37
struct Migration {
    int version;
    QString name;
    std::function<Result<void>(QSqlDatabase&)> apply;   // no `revert`
};
```

The current ladder is v001 → v031 (31 migrations, including `v029_secure_credentials`). Each migration:

1. Registers itself by calling `register_migration_v0NN()` before `Database::open()` (explicit calls exist so MSVC's linker does not strip the translation units).
2. Runs inside `BEGIN IMMEDIATE` / `COMMIT`. On failure the runner issues `ROLLBACK` and returns an error — the schema_version row is only written after success.

### Rules for adding migrations

| Rule | Why |
|---|---|
| New migration gets a new highest number `vNNN_short_name.cpp` | Re-numbering breaks every installed copy that already recorded the old number |
| Use `CREATE TABLE IF NOT EXISTS` and `ALTER TABLE … ADD COLUMN`, never `DROP` | Forward-only means a column you add today may have data tomorrow; you cannot undo it |
| Call `register_migration_vNNN()` from the runner's explicit list | Static-init alone is unreliable under MSVC link-time GC |
| Do not edit an applied migration | Already-upgraded installs will not re-run it, so the edit only affects fresh installs and silently bifurcates schema |

Failure mode if broken: editing v015 in place leaves existing users on the old shape forever (their `schema_version` already records 15 as applied) while new users get the edited version — silent schema divergence that surfaces as random query errors weeks later.

Sources: [fincept-qt/src/storage/sqlite/migrations/MigrationRunner.h:14-82](fincept-qt/src/storage/sqlite/migrations/MigrationRunner.h), [fincept-qt/src/storage/sqlite/migrations/MigrationRunner.cpp:31-80](fincept-qt/src/storage/sqlite/migrations/MigrationRunner.cpp), [fincept-qt/src/storage/sqlite/migrations/v029_secure_credentials.cpp:1-50](fincept-qt/src/storage/sqlite/migrations/v029_secure_credentials.cpp)

## 5. Screens are lazy-instantiated via `DockScreenRouter` factories

Screens never exist at startup. `WindowFrame_Setup.cpp` registers a `std::function<QWidget*()>` factory for each screen id; `DockScreenRouter` calls the factory the first time anyone navigates to that id.

```cpp
// fincept-qt/src/app/DockScreenRouter.h:30-47
using ScreenFactory = std::function<QWidget*()>;
void register_factory(const QString& id, ScreenFactory factory);
```

```cpp
// fincept-qt/src/app/WindowFrame_Setup.cpp:224-234
dock_router_->register_factory("dashboard", []() { return new screens::DashboardScreen; });
dock_router_->register_factory("markets",   []() { return new screens::MarketsScreen;   });
```

### Why this matters

- A 54-screen workspace would otherwise instantiate ~54 `QWidget` trees at first window open, each potentially loading charts, fonts, and Python helpers. Lazy factories defer that cost until the user actually opens the screen.
- `ensure_all_registered()` pre-creates `CDockWidget` placeholders (not full screens) so ADS's `restoreState()` can rebind by `objectName` — restoration does not force eager screen construction.
- `duplicate_panel()` only works for factory-registered screens; eager `register_screen()` reuses one widget instance and cannot be tabbed twice. New code should use `register_factory`, not `register_screen`.

### State ownership rule

A screen that needs its UI state to survive restart implements `IStatefulScreen` and routes save/restore through `ScreenStateManager`. A screen must not own caches; cached data belongs in `CacheManager` via DataHub. A `QHash<QString, ...>` field in a screen is only permitted as (a) a live-feed dispatch table cleared on hide, or (b) a view/index over data already owned by DataHub.

Failure mode if broken: registering screens eagerly returns the multi-window memory profile back to the 100-250 MB-per-window regime called out in the known-weaknesses table; bypassing `IStatefulScreen` means workspace restore leaves the user staring at empty tabs after a restart.

Sources: [fincept-qt/src/app/DockScreenRouter.h:29-96](fincept-qt/src/app/DockScreenRouter.h), [fincept-qt/src/app/DockScreenRouter.cpp:206-233](fincept-qt/src/app/DockScreenRouter.cpp), [fincept-qt/src/app/DockScreenRouter_Materialize.cpp:46-95](fincept-qt/src/app/DockScreenRouter_Materialize.cpp), [fincept-qt/src/app/WindowFrame_Setup.cpp:224-234](fincept-qt/src/app/WindowFrame_Setup.cpp), [docs/ARCHITECTURE.md:432-447](docs/ARCHITECTURE.md)

## 6. Cross-cutting failure modes worth memorising

These are the easy traps — each one is a single line of code that compiles cleanly and breaks at runtime.

| Trap | What goes wrong | Where the rule lives |
|---|---|---|
| Synchronous DB query on the UI thread | Frame stutter; per-thread cloned connection never created | [Database.h:33-37](fincept-qt/src/storage/sqlite/Database.h) |
| `BrokerHttp` called from the UI thread | Internal `QEventLoop` re-enters the UI event loop; reentrancy hazards | [docs/ARCHITECTURE.md:228-229](docs/ARCHITECTURE.md) |
| `qApp->setStyleSheet(...)` from a widget's own event handler | Wayland crash; theme changes must dispatch via `Qt::QueuedConnection` | [docs/ARCHITECTURE.md:443](docs/ARCHITECTURE.md) |
| Screen calling `HttpClient::instance()` directly | Bypasses caching, dedup, and DataHub fan-out | [docs/ARCHITECTURE.md:439](docs/ARCHITECTURE.md) |
| Data service exposing `QWidget*` | Couples data plane to UI; only UI-coordinator services may | [docs/ARCHITECTURE.md:240](docs/ARCHITECTURE.md) |
| Hardcoded broker enums in `if`-trees | Defeats `BrokerEnumMap<T>` table-driven dispatch | [docs/ARCHITECTURE.md:441](docs/ARCHITECTURE.md) |
| Adding a new `::instance()` singleton | Worsens the 40+ singleton tax; use the DI container | [docs/ARCHITECTURE.md:447](docs/ARCHITECTURE.md) |
| Unmanaged `*_API_KEY` env var in `PythonRunner` | Leaks via `/proc/<pid>/environ`; whitelist is intentional | [docs/ARCHITECTURE.md:299](docs/ARCHITECTURE.md) |

## 7. Pre-merge checklist

Run this against any patch before requesting review.

```text
┌─ LAYER ──────────────────────────────────────────────────────────────┐
│ Which layer does my code live in (Presentation / Application /        │
│ Data Plane / Adapters / Infrastructure)?                              │
│ Does any new #include reach upward? If yes, refactor through DataHub  │
│ or a typed event before merging.                                      │
└───────────────────────────────────────────────────────────────────────┘
┌─ STATE OWNERSHIP ────────────────────────────────────────────────────┐
│ Am I touching fincept credentials? Read via AuthManager::session();   │
│ never cache a copy.                                                   │
│ Am I touching schema? Add v032_*.cpp; never edit an applied file.     │
│ Am I touching SecureStorage? Confirm Database::open() runs first.     │
│ Am I caching data in a screen? Move it to CacheManager via DataHub.   │
└───────────────────────────────────────────────────────────────────────┘
┌─ LIFECYCLE ──────────────────────────────────────────────────────────┐
│ New screen → register_factory (lazy), not register_screen (eager).    │
│ State to survive restart → implement IStatefulScreen.                 │
│ Async work → co_await via QCoro (new code) or signals (existing).     │
│ Long DB / HTTP / Python call → not on the UI thread.                  │
└───────────────────────────────────────────────────────────────────────┘
┌─ SECURITY ───────────────────────────────────────────────────────────┐
│ New credential to persist → SecureStorage, not SettingsRepository.    │
│ New API key env var for Python → add to the whitelist explicitly.     │
│ No platform keychain reintroduction.                                  │
└───────────────────────────────────────────────────────────────────────┘
```

## Summary

The page-level rule is simple: every layer in Fincept Terminal has one canonical owner of state and one canonical way to be called. `AuthManager::session()` owns the live fincept session; `SecureStorage` owns encrypted credentials and refuses to run before `Database::open()`; the migration ladder owns the schema and only moves forward; `DockScreenRouter` owns screen lifetime via factories. Violations of these invariants compile cleanly and fail at runtime in ways that are hard to debug, which is why the architecture doc lists them as anti-patterns and why this page exists as a checklist. When in doubt, ask which layer you are in, which canonical owner of state you should be calling, and whether your patch would survive being merged behind someone else's that already broke one of the rules above.

Sources: [docs/ARCHITECTURE.md:438-448](docs/ARCHITECTURE.md), [fincept-qt/src/auth/AuthManager.h:14-34](fincept-qt/src/auth/AuthManager.h), [fincept-qt/src/storage/secure/SecureStorage.cpp:190-281](fincept-qt/src/storage/secure/SecureStorage.cpp)
