# TraderAlign — Single Source of Truth Audit (Phase 1)

| | |
|---|---|
| **Status** | Complete — this is a research/audit document, not a changelog. No code was changed as part of producing this report (except the separate, explicitly-authorized Phase 3 dead-file relocation, documented in its own section at the bottom). |
| **Scope** | Every place in the codebase (`public/`, `api/`, `src/`, `config/`, `cron/`, root) that independently computes, derives, or re-derives a fact the system already knows or can derive centrally — balance, drawdown, breach/pass, scores, classifications, commentary stats, and more. |
| **Created** | 2026-08-07 |
| **Origin** | Kevin's explicit directive, issued immediately after the multi-session balance/breach/pass "single source of truth" incident (see CLAUDE.md's "Single Source of Truth Rule (Core Account Metrics)" and the matching dated entry at the top of "Currently Being Worked On"). Governing rule: *"No page, file, or component may compute a value independently if that value represents a fact the system already knows or can derive centrally."* |
| **Method** | Seven parallel research passes across the entire codebase, each covering a category of computed fact, followed by direct personal verification (reading the actual source, not just trusting agent summaries) of every claim flagged as a confirmed live disagreement. |

> **Read this document before touching any computation logic anywhere in TraderAlign.** It is the factual foundation for the Phase 2 architecture proposal and the Phase 4 migration plan — both of which cite specific line numbers and specific disagreements documented here. Do not re-derive this audit from scratch in a future session; update this document if new findings emerge instead.

---

## Executive Summary

The recently-fixed balance bug was not an isolated defect. It was the visible symptom of a real, systemic pattern: **the same fact is independently computed in multiple places across nearly every category examined**, and in several cases the independent copies use genuinely different formulas, different defaults, or different underlying data — not just the same formula written twice.

Across seven research passes and direct verification, this audit found:

- **29 confirmed or strongly-suspected disagreements** where two or more live code paths would produce different answers for the same real account/trade/scenario — not theoretical risk, but demonstrable from the code as written.
- **The single most severe finding**: `public/mt5-resolve.php`'s trade-creation feature completely bypasses the app's sole lifecycle mutator — a real, reachable, intended workflow inserts closed trades with only uncapped `broker_pnl` set, never computes `challenge_pnl`, and never updates the account's balance at all. This isn't a wrong number shown somewhere; it's a permanent, silent gap in an account's real data, created by using a real feature exactly as designed.
- **A close second**: `api/trades/validate.php` — the backend endpoint behind the Calculator's live "Account Status" panel and "Confirm Trade" popup — computes its own account balance (violating the P&L Rule: uses uncapped `broker_pnl`/`projected_pnl`, never `challenge_pnl`, for prop-firm accounts) and its own breach/daily-loss/profit-target thresholds via a flatter, non-phase-aware rules cascade than the canonical `AnalyticsEngine::getChallengeMetrics()`. Confirmed directly: if an account's `rules` JSON only carries phase-specific keys (the documented normal case for real two/three-step accounts), `validate.php`'s `$maxDdPct` resolves to **0**, which **silently disables breach detection entirely** in the Calculator — a trader could see zero breach warning while placing a trade that Drafts/Dashboard would immediately mark as breached on save. Independently found by two separate research passes.
- **`api/mt5/reconcile.php` has the same class of bypass as the top finding**, on the trade-matching path rather than trade-creation.
- **Five independent implementations of the same rules-JSON threshold-resolution cascade** exist (`getChallengeMetrics()`, `rnPhaseTargets()`, `validate.php`, calculator-source.html's `_getPFContext()`, `api/account/state.php`, `AccountPicker.php`'s `apChallengeRulesSummary()`) — with a concrete, demonstrable failure case: an account's real declared Phase 2 profit target shows correctly on Dashboard/Drafts/Reports and literally "Not specified" on Trading Center's own Account Home for the identical account.
- **A function already exists in the codebase, explicitly built and documented as "the" single source of truth for account balance, with zero real callers** — a prior attempt at solving exactly this problem, currently inert, worth reviewing rather than duplicating.
- **Three independent "Consistency Score" formulas** and **three independent "checklist adherence" statistics** exist, each legitimately labeled with the same or similar name but computing genuinely different things.
- **Two previously-undocumented, currently-live balance bugs** were found that are not mentioned anywhere in CLAUDE.md's existing history: Trading Center's Account Home (raw `trading_accounts.current_balance` shown for prop-firm accounts, not phase-aware) and Colex's "total balance" chat answer (same class of bug).
- **What is already correctly centralized and should not be re-architected**: the Four-Stage engine (`computeFourStageRaw()`/`getFourStagePeriodSummary()`), `sp_classify_trade` as the sole write-path for trade classification, `JourneySegmentation::partitionAccountJourney()` for phase-bucketing, and (as of the 2026-08-07 fix) `getChallengeMetrics()['phase_balance']` for the two highest-stakes consumers (`dashboard.php`'s `resolve_trade_id`, `drafts.php`'s pre-check).

The pattern confirms Kevin's own diagnosis exactly: this is not N unrelated bugs, it is one architectural gap (no enforced single computation layer) producing N symptoms, and new symptoms will keep surfacing at increasing cost as the codebase grows, until a real single-source-of-truth layer exists and is the *only* way to get these values.

---

## 1. Account Balance

| # | Site | What it computes | Data source | Status |
|---|---|---|---|---|
| 1a | `AnalyticsEngine::getChallengeMetrics()` (`api/analytics/engine.php`), field `phase_balance` | Phase-reset-aware current balance: fresh `starting_balance` + sum of only the current phase's own trades' `effective_pnl`, via `JourneySegmentation::partitionAccountJourney()` with an `array_key_last()` safety check | `trading_accounts.starting_balance` + trades table, partitioned | **Canonical**, fixed 2026-08-07. Correctly consumed by `dashboard.php`'s `resolve_trade_id` and `drafts.php`'s live pre-check. |
| 1b | `AnalyticsEngine::getAccountMetrics()` | Whole-account, never-phase-resetting current balance | `trading_accounts.current_balance` directly | Correct **only** for personal/live accounts or whole-ledger purposes — by design, not phase-aware. Do not use for prop-firm boundary math. |
| 1c | `api/account/state.php` (Calculator's balance-display endpoint) | Independently reimplements the same formula as 1a | `challenge_accounts.account_size` + trades table, own `JourneySegmentation` call | Duplicate inline implementation. Formula-identical to 1a as of the 2026-08-07 fix, but architecturally a separate copy with no shared function. |
| 1d | `public/journal.php` → `rnBuildPhaseCardFromRows()` + `rnBuildJourney()` | Per-segment running balance: `accountSize + Σ(segment's own rows' effective_pnl)` | `challenge_accounts.account_size` + trades, own `JourneySegmentation` call | Duplicate inline implementation. Correct in effect, separate copy. |
| 1e | `public/journal.php` → `renderRnFundedCard()`, `renderRnCoachingSection()` | `$currentBalance = $phase['account_size'] + $phase['total_profit']` — re-derives balance a *third* time from already-built phase-card data | In-memory, from 1d | Duplicate inline arithmetic (trivial, but still not a shared call). |
| 1f | `src/TradingArchiveEngine.php` → `gatherPropSegments()`, `successStory()`, `gatherLiveSegments()`/`liveAccountSnapshot()` | Segment balance/growth, same shape as 1d | `challenge_accounts.account_size` + trades, own `JourneySegmentation` call | Duplicate inline implementation, formula-consistent with 1a/1d as of the 2026-08-06/07 revert-and-fix. |
| 1g | **`public/trading-center.php` → `tcRenderPropAccountSummary()` (line 648)** | Displays "Current Balance" for a **prop-firm** account | `$account['current_balance']` — raw, from `AccountPicker::apFetchAccounts()`'s `SELECT ta.current_balance` | **CONFIRMED BUG, previously undocumented.** Directly verified: `number_format((float)$account['current_balance'], 2)` — no phase-awareness, no call to `getChallengeMetrics()`. Same bug class as the original Journal/Calculator/Archive incident, never fixed here. |
| 1h | `public/trading-center.php` → `tcRenderLiveAccountSummary()` | Displays "Current Balance" for a live/personal account | Same raw field | Correct — live accounts have no phase concept, this field is right. |
| 1i | **`api/trades/validate.php` (Calculator's live trade-confirmation endpoint, lines 138-153)** | `$realCurrentBalance = $startingBalance + SUM(COALESCE(broker_pnl, projected_pnl, 0))` across **every closed trade ever**, no phase partitioning | `trading_accounts.starting_balance` + raw trade sum, **never `challenge_pnl`** | **CONFIRMED BUG, previously undocumented, most severe finding of this audit.** Directly verified via direct file read. Two independent defects: not phase-scoped, AND a direct P&L Rule violation (uses uncapped ledger P&L even for prop-firm accounts). See §9 for the breach-detection consequence. |
| 1j | `api/colex/chat.php` → `generateGeneralResponse()` | Sums `$acc['current_balance']` across all of a user's accounts for "You have N account(s) with $X total balance" | `trading_accounts.current_balance`, raw, summed | **CONFIRMED BUG, previously undocumented.** Same class as 1g/1i — raw whole-ledger value for prop-firm accounts. |
| 1k | `api/analytics/career.php` (line 44) | `SUM(current_balance - starting_balance) AS net_pnl` | Raw SQL on `trading_accounts`, bypasses trades table and P&L Rule entirely | Wrong formula, but **confirmed unreachable** — no frontend caller anywhere in the codebase. Dead-but-wrong; flag if ever wired up. |
| 1l | `api/analytics/portfolio.php` | Aggregates `getAccountMetrics()`'s whole-ledger balance (1b), not `getChallengeMetrics()['phase_balance']` (1a) | Calls the shared engine, but the wrong field for prop-firm | Correctly delegates, but to the wrong canonical field for this purpose. **Confirmed unreachable** — no frontend caller. |
| 1m | `api/analytics/account.php` | Reads `(float)$account['current_balance']` raw | `trading_accounts.current_balance` | Confirmed unreachable (per CLAUDE.md, re-confirmed this pass). Not a live bug. |

---

## 2. Drawdown (daily, max, current, % of account)

| # | Site | What it computes | Status |
|---|---|---|---|
| 2a | `getAccountMetrics()`/`computeMetricsFromTrades()` | Peak-equity-relative drawdown over the whole account's equity curve, all phases blended | Canonical for "how far below all-time peak" — a genuinely different concept from phase-boundary drawdown, not itself wrong. |
| 2b | `getChallengeMetrics()`'s `overall_drawdown`/`daily_drawdown` | Phase-boundary drawdown: `max(0, startingBalance − phase_balance)`, clamped at zero | **Canonical** for breach/pass boundary math. Reused correctly by `dashboard.php`, `drafts.php`, `reports.php`'s Boundary Analysis/Accounts Needing Attention, `journal.php`. |
| 2c | `api/account/state.php` (line 111-112) | `(startingBalance − currentBalance) / startingBalance × 100` — **no `max(0,...)` clamp**, can go negative when in profit | Independent formula. Real, if minor, divergence from 2b's clamped-at-zero display: an in-profit account shows a *negative* drawdown% here vs. a flat `0%` via the canonical path. |
| 2d | `api/trades/validate.php` (lines 230-238, 287-294) | Own drawdown %, own breach level, from a **flat, non-phase-cascading** `rules['max_dd_pct']` | **Confirmed bug** — see §1i and §9. |
| 2e | `TradingArchiveEngine.php` — `segmentMetrics()`, `successStory()`/`accountRuleRow()`, `liveAccountSnapshot()` | Multiple independent peak-to-trough / peak-equity-relative drawdown walks over already-segmented rows | Independent inline computations, not calls to `getAccountMetrics()`/`getChallengeMetrics()`. Architecturally necessary today (no per-historical-segment API exists), but N separate re-implementations of the same walk. |
| 2f | `api/colex/chat.php` → `generateDrawdownResponse()` | Blends `getAccountMetrics()`'s whole-account-lifetime drawdown with `getChallengeMetrics()`'s phase-boundary daily drawdown in one response | Correctly calls shared engines, but blends two different scopes in one chat answer — not a formula bug, but a real source of an answer that doesn't cleanly match what Journal/Reports show. |
| 2g | `reports.php` → `getAccountMetricsForPeriod()` | Merges all-time + period-scoped `getAccountMetrics()` calls; drawdown/peak-equity fields taken from the all-time call by explicit design | Correctly delegates to the shared engine for its documented purpose. |

**CONFIRMED DISAGREEMENT — Trading Archive vs. AnalyticsEngine on live-account "current drawdown %"**: `TradingArchiveEngine::liveAccountSnapshot()` reads the *stored* `trading_accounts.peak_equity`/`current_balance` columns directly (which reflect deposits/withdrawals); `AnalyticsEngine::getAccountMetrics()` reconstructs the equity curve purely from `starting_balance + Σ(trade P&L)` (blind to deposits/withdrawals). For any live account with deposit/withdrawal history, these two formulas will produce different "current drawdown %" figures for the same account, shown on different pages (Dashboard/Reports vs. Trading Archive's Lessons From Success).

---

## 3. Daily Loss Limit / Remaining Daily Loss Budget

| # | Site | Formula | Status |
|---|---|---|---|
| 3a | `getChallengeMetrics()` | `dailyLossLimit = startingBalance(phase, fixed) × (maxDailyLossPct/100)` | **Canonical.** Correctly consumed by `drafts.php`, `journal.php`. |
| 3b | `api/trades/validate.php` (line 245) | `dailyLossLimit = $realCurrentBalance(running, wrong) × (dailyDdPct/100)` | **CONFIRMED DISAGREEMENT.** Two structural differences from 3a: base is the running (and wrongly-computed, per §1i) current balance rather than the fixed phase starting balance, and `dailyDdPct` is flat-only (default 0, not 5). These two formulas will disagree on virtually every account with any daily P&L, even setting the balance bug aside. |

---

## 4. Breach Detection

| Site | Description | Status |
|---|---|---|
| `getChallengeMetrics()` | **Canonical.** `isBreached = excessLossAfterBreach > 0`; `breachLevel = startingBalance × (1 − maxOverallLossPct/100)`, phase-cascading threshold resolution. | Correct. |
| `dashboard.php`'s `resolve_trade_id` | Reads canonical `breach_level_amount`/`phase_balance`, caps `challenge_pnl`, writes `lifecycle_state='breached'` only on explicit trader confirmation. The sole DB-writing breach mutator. | Correct, canonical thresholds. |
| `drafts.php` | PHP-side, embeds `getChallengeMetrics()`'s canonical fields as hidden JS fields (`livebalance`/`breachlevel`/`targetbalance`/`dailyremaining`) | Correct, no independent client math (fixed 2026-08-07). |
| **`api/trades/validate.php` (lines 283-294)** | **INDEPENDENT breach detection, does not call `getChallengeMetrics()` at all.** `$maxDdPct = $rules['max_dd_pct'] ?? 0` — flat key only, no phase fallback, default **0** (not 10). `$breachWarning = ($realCurrentBalance − $riskPerPosition) <= $breachLevel`. | **CONFIRMED, most severe finding — directly verified by reading the file.** See §9. |
| `calculator-source.html` (live PF panel breach banner, ~line 5599) | Client-side JS re-derives `isAlreadyBreached` from `validate.php`'s already-divergent fields | Adds a display layer on top of an already-wrong upstream number; not a new threshold source itself. |
| `calculator-source.html` → `renderPropFirmFallback()` (~line 5848) | **A fourth, fully offline breach check**, used only when `validate.php` is unreachable — computed entirely from stale snapshot fields baked into the trade/URL object at Calculator load time | Independent, offline, uses yet another data path (stale snapshots, not live balance). |
| `calculator-source.html` → `openBreachPopup()` (~line 4226) | Displays its own `breachLevel = startBal × (1 − maxDDPct/100)` from `api/account/state.php`'s flat-only `max_dd_pct` field | Independent recomputation, flat (non-phase) rules read. |
| `journal.php`'s `rnDeriveTerminalStatus()`, `reports.php`'s Boundary Analysis, `TradingArchiveEngine.php`'s breach-cause bucketing | Read the already-persisted `lifecycle_state='breached'` / `breached_at` | State readers, not independent detection — safe. |

**§9 — The single most severe confirmed finding of this audit.** If an account's `rules` JSON carries only phase-specific keys (`phase_2_max_loss`, not a flat `max_dd_pct`) — which CLAUDE.md's own history documents as the normal, real pattern for two/three-step accounts (*"phase_N_target_pct... is the key every real two_step/three_step account's rules JSON actually uses"*) — then `validate.php`'s `$maxDdPct` resolves to **0**, and the `if ($maxDdPct > 0 && ...)` guard means **breach detection is silently disabled entirely** in the Calculator's live panel and Confirm-Trade popup, while `getChallengeMetrics()` (used by Drafts/Dashboard at save time) would still correctly compute and enforce the real threshold. A trader can see **zero breach warning while placing a trade in the Calculator**, then have that same trade **immediately marked breached the moment they confirm it in Drafts** — the two systems structurally cannot agree for any account using per-phase-only rules.

---

## 5. Pass/Fail Progress ("% complete" / "distance to pass")

| Site | Formula | Caps at 100%? |
|---|---|---|
| `getChallengeMetrics()` (canonical) | `min(100, max(0, currentProfit/profitNeeded × 100))` | **Yes.** |
| `journal.php`'s `rnBuildPhaseCardFromRows()` (Active Journeys phase cards) | `max(0.0, totalProfit/profitNeeded × 100)` | **No — can exceed 100%** for a passed phase with excess profit. |
| `journal.php`'s `rnGoalProgress()` (trader-set Growth Target) | Capped explicitly | Yes. |
| `api/trades/validate.php` + Calculator fallback | Bar width clamped client-side; underlying % text not always clamped | Inconsistent. |

**CONFIRMED DISAGREEMENT**: for an account that has passed its phase with real excess profit, Active Journeys' phase-progress bar/number can show over 100% while every other progress display in the app (Goal section, canonical `getChallengeMetrics()`) caps at 100%.

---

## 6. Phase Transition & Rule-Threshold Resolution (`challenge_accounts.rules` cascade)

This is the single largest concentration of confirmed drift in the entire audit. **Six independent implementations** of "which `rules[...]` key wins for this phase, and what's the default if none match" were found:

1. **`AnalyticsEngine::getChallengeMetrics()`** — CANONICAL. Phase-specific key first, then flat, then hardcoded defaults (8/5/10 phase 1, 5/5/10 phase 2/3, 0/5/10 funded).
2. **`journal.php`'s `rnPhaseTargets()`** — hand-duplicated copy, verified (by a prior session) to match #1 for `profit_target_pct`/`max_dd_pct`. **Does not resolve `daily_dd_pct` at all.**
3. **`api/trades/validate.php` (lines 214-227)** — materially simpler: `max_dd_pct`/`daily_dd_pct` are **flat-key-only**, no phase fallback at all, default **0** (not 10/5). Profit target checks phase-specific correctly but never falls back to flat `profit_target_pct`.
4. **`calculator-source.html`'s `_getPFContext()`/`_phaseRuleVals()`** — fully client-side, reads raw unprocessed rules JSON forwarded verbatim via URL query params from `strategies.php`. Flat-only for max/daily DD; returns `null` (no defaults) for anything missing. Every locally-recomputed drawdown/breach figure downstream in the Calculator ultimately traces back to this URL-param source, not a live fetch — meaning a stale/bookmarked/reused Calculator URL across a phase promotion would silently use outdated rule percentages.
5. **`api/account/state.php` (lines 126-128)** — flat-only for max/daily DD (never phase-aware, unlike this same file's own correctly-fixed balance computation); `profit_target_pct` checks the flat key **before** phase-specific (backwards from every other cascade), and when it falls back only ever checks `phase_1_target_pct` — never phase 2/3, even for an account actually in phase 2/3.
6. **`src/AccountPicker.php`'s `apChallengeRulesSummary()`** (Trading Center's "Challenge Rules" card) — flat-key-only for Max/Daily Drawdown, **contradicting its own doc-comment**, which claims parity with `rnPhaseTargets()`'s cascade. That claim is only true for Profit Target.

Two additional variants (`TradingArchiveEngine.php`'s `realDeclaredMaxDdPct()` and `failureDailyLossPct()`) correctly match #1/#2's cascade *order* but deliberately return `null` instead of a fabricated default — an intentional, documented divergence in default-handling philosophy for statistical-reporting purposes, not a bug.

**Confirmed live consequences:**
- **Trading Center's "Challenge Rules" card can show a different Max/Daily Drawdown than Drafts/Journal use for the same account's current phase**, if the firm declared per-phase limits (cascade #6 vs #1).
- **`api/account/state.php`'s displayed rule thresholds are not phase-aware**, unlike its own balance computation — a Phase-2 account with a different Phase-1 target will show the wrong figure in `openBreachPopup()`.
- **`dashboard.php`'s `passed_phase` branch reads `challenge_accounts.current_stage` directly** (line ~325-329), the exact stale-column pattern `getChallengeMetrics()` was fixed to stop trusting on 2026-08-07 — but the sibling `passed_phase` write logic, 125 lines later in the *same handler*, was not given the same fix. If `current_stage` is stale at the moment a trader confirms "Passed Phase" (a confirmed-possible live drift case per CLAUDE.md), this branch can write the wrong `_complete` state, or silently no-op (`$stageCompleteMap` has no `'funded'` entry).
- **`validate.php`'s phase-completion banner** (`$phaseCompleted`) uses its own divergent progress % (built on cascade #3's defaults) — plausible for the Calculator to never show "phase completed" for an account whose rules lack the exact keys it checks, even though the account has genuinely passed per canonical math (suspected, not independently reproduced against a specific live account this pass).

---

## 7. Growth Target ("Current Goal")

`journal.php`'s `rnGoalProgress()` is correctly isolated — a trader-set target on `account_goals.target_balance`, a different concept from prop-firm rule thresholds, capped at 100%, no disagreement possible with `getChallengeMetrics()`. The "remaining ÷ avg win = wins needed" formula shape is independently reimplemented (not shared) between the Goal section and the phase-card progress display, but for two genuinely different targets (goal vs. phase profit target) — not a bug, just an un-shared formula shape worth noting for the migration plan.

---

## 8. Discipline / Consistency / Execution-Quality Scores

| # | Site | Formula | Reads `trade_classification`? |
|---|---|---|---|
| 8a | `EdgeHealthEngine::getScore()` / `getPeriodFactors()` | `scoreConsistency()` = avg(CV(risk%), CV(win-R), CV(loss-R)), mapped to 100 − min(1,avgCv)×100 | **No** — never touches classification at all. |
| 8b | `TradingArchiveEngine::historicalConsistency()` | `0.5 × (100−min(100,CV(risk%)×100)) + 0.5 × disciplineRate` | **Yes** — disciplineRate = % of `disciplined_win`/`controlled_loss`/`breakeven` trades. |
| 8c | `api/colex/chat.php`'s `generateDisciplineResponse()` | Raw (non-normalized) standard deviation of risk%, fixed absolute thresholds (<0.5 excellent, <1.5 good, else poor) | No — calls neither shared engine. |

**CONFIRMED — three genuinely different "Consistency"-labeled scores exist**, shown on different pages (Reports/Journal/Trader Profile via 8a; Trading Archive via 8b; Colex chat via 8c), computed from different inputs, with no cross-calling. 8a/8b are documented in CLAUDE.md as *deliberately* different metrics (all-time vs. period, and a genuinely different formula for Trading Archive) — but 8c is undisclosed and unrelated to either; a trader could see "excellent" from Colex chat and a middling score on Reports for the same trades, with no way to reconcile the two since they aren't the same statistic. (Confirmed via agent research; `EdgeHealthEngine::getPeriodFactors()`'s dual All-Time/Last-30-Days windows within Journal's Active Journeys are correctly the *same* formula at two windows, not a bug — verified.)

---

## 9. Tilt / Discipline Classification

**Confirmed sole write path**: `sp_classify_trade` (stored procedure) is the only place `trades.trade_classification` is ever written — grepped the whole repo for `SET trade_classification`, found zero PHP writers. This part of the architecture is genuinely, correctly centralized already.

**CONFIRMED DISAGREEMENT — Colex's two tilt/revenge signals disagree with each other, live, on the same product surface.** `api/colex/chat.php`'s `generatePatternsResponse()` infers "revenge trading or tilt" purely from `losing_streak >= 3` (raw consecutive-loss counting via `AnalyticsEngine::getUserMetrics()`) — it **never reads `trade_classification` at all**. Meanwhile `src/ColexHelpers.php` (used by `api/colex/stats.php`, the same feature's stats widget) correctly counts real `trade_classification = 'revenge_trade'` rows. A user can have 3 consecutive real losses with zero of them actually flagged `revenge_trade` by the real classification logic (chat warns about tilt, stats widget correctly shows 0 revenge trades) — or the reverse.

**CONFIRMED — "undisciplined classification" is defined three different ways**, with `elevated_win` as the swing case:
- `$fsGradeMap`'s "C" bucket (`reports.php`) / `TraderProfileBehaviorEngine::isCGrade()` / `getGrowthTimeline()`'s hardcoded SQL — **3 values** (`loss_high_risk`, `tilt_warning`, `revenge_trade`), excludes `elevated_win`.
- `TradingArchiveEngine::UNDISCIPLINED_CLASSIFICATIONS` — **4 values**, includes `elevated_win`.
- `api/analytics/behavior.php`'s arrays — 3 values, matches the first set, but this file is confirmed dead/unreachable code.

These are disclosed-as-different-purpose in code (grade bucket vs. star-rating cap) so may be intentional, but the practical effect is that "was this an undisciplined trade" has two live, disagreeing answers depending on which file computes it.

**CONFIRMED — `GRADE_MAP` is a hand-duplicated copy, not a shared constant.** `reports.php`'s `$fsGradeMap` and `TraderProfileBehaviorEngine::GRADE_MAP` are currently byte-identical (confirmed by direct comparison) but have zero shared-constant mechanism preventing future drift.

A separate, disclosed-as-different concept — `reports.php`'s `disciplineReason()`/`most_disciplined_trade` — picks "most disciplined" from Stage-1-vs-Stage-2 plan-adherence signals, never reading `trade_classification` at all. Not a bug, just another "discipline"-labeled metric with yet another independent formula, worth naming clearly in any future UI copy.

---

## 10. Coaching Risk Recommendations

`public/journal.php`'s `rnComputeRiskRecommendation()` is the only "Recommended Risk %" computation in the app, and it is correctly, explicitly reused (not recomputed) by both the prop and live coaching sections on the same page. `rnGoalProgress()`'s "estimated wins needed" is similarly single-sourced within Journal. Colex has no independent recommended-risk or balance-to-recover computation (confirmed via grep). **No disagreement found in this category** — this is one of the better-centralized areas of the app already.

---

## 11. Checklist Adherence

**CONFIRMED — three genuinely different statistics, all real, all live, all called some form of "checklist adherence":**
1. **Continuous average** (`AVG(ticked_pct)`) — `reports.php`'s Strategy Evolution comparison, `TraderProfileBehaviorEngine::getPatienceSignal()`.
2. **Binary "fully complete" rate** — `TradingArchiveEngine::segmentMetrics()`'s `checklist_skip_rate` (anything under 100% counts as skipped). A population consistently checking 90% of items every trade would show ~90% under formula 1 but ~0% "Follow-Through" under formula 2.
3. **Split-at-threshold with two different thresholds** — Trader Profile splits full/partial at `>= 99.5`; `journal.php`'s `rnChecklistHistoricalComparison()` splits at `>= 100` exactly. A 99.7%-ticked trade counts as "full" in one and "incomplete" in the other.

---

## 12. Reports Commentary Stats & Strategy-Level Baselines

**Reports itself is correctly single-sourced** — `$auditCommentary`/`$auditRecommendations` are built entirely from `getFourStagePeriodSummary()`/`getStyleDriftSignal()`, including the per-asset-class risk-overage tolerance and Contract-Constrained exclusion logic.

**CONFIRMED DISAGREEMENT — Trading Archive independently reimplements the same "declared vs. actual risk" concept with a materially different, older formula.** `TradingArchiveEngine::segmentMetrics()`'s `declared_adherence_rate` uses a flat 20% relative tolerance for every instrument (`if ($rp <= $dp * 1.2)`) with **no per-asset-class distinction and no Contract-Constrained exclusion** — this is the exact tolerance formula CLAUDE.md documents as superseded in `engine.php` itself (replaced by 1%-forex-majors/20%-metals per-asset-class tolerance, plus structural exclusion for mathematically-impossible-to-hit targets). Trading Archive was apparently never updated when `engine.php`'s own logic evolved. This directly feeds Trading Archive's `findings()`/`patterns()` narrative sentences (*"Declared risk was exceeded by more than 20% on N% of trades"*), which can disagree with Reports' equivalent commentary for the same account/period.

---

## 13. Trading Archive Eligibility & Stats

Confirmed **not** duplicated between `public/journal.php`'s `?workspace=archive` route and `public/trading-archive.php` — journal.php literally `require_once`s the same file; there is exactly one implementation. Every stat on the page routes through `TradingArchiveEngine::` static methods, with no inline SQL for balance/drawdown/deposit stats in the page file itself.

The one confirmed architectural gap here is §2's drawdown-source divergence (`liveAccountSnapshot()` vs. `getAccountMetrics()`) — already documented above, not repeated.

---

## 14. Colex-Generated Stats

`public/colex.php` performs no computation itself (pure UI shell). `api/colex/chat.php` correctly reuses `AnalyticsEngine::getUserAccounts()`/`getUserMetrics()`/`getChallengeMetrics()`/`getRecentTrades()`/`getFourStagePeriodSummary()` for most of its context. One naming red herring investigated and cleared: `$trade['broker_pnl'] ?? $trade['challenge_pnl']` appears in several places and looks like a P&L Rule violation at first glance, but `getRecentTrades()` pre-resolves the fully correct account-type-aware value into the (confusingly named) `'broker_pnl'` key before chat.php ever reads it — not a bug, just a misleading key name worth renaming during migration to prevent a future editor from "fixing" it into an actual bug.

Genuine Colex-specific independent computations: the tilt/revenge signal (§9) and the "Risk Consistency" score (§8c), both already documented above as confirmed disagreements. Colex's own "R:R Ratio" chat card (`avgWinner/abs(avgLoser)`, a payoff-ratio concept) is a third distinct "R:R"-labeled figure alongside `engine.php`'s per-trade R-multiple averages and Trading Archive's pooled declared-RR average — not necessarily wrong, but a third formula under a shared label, worth consolidating terminology during migration.

---

## 15. `calculator-source.html` — Account-State Boundary Classification

Per Kevin's explicit boundary (margin/instruments/TP-ladder/warnings = calculator-owned, untouched; account/prop-firm state = should be fetched, not computed), the file was read in full and every account-state-adjacent code path classified:

**(A) Confirmed calculator-owned, do not touch:** `INSTRUMENTS`/`MARGIN_PER_LOT` tables, `getResolvedRate()`/`getActiveTick()`/`pipValueAtPrice()`/`pipValueAtPrice2()`, `checkPriceMismatch()`, `_validateForm()`, all margin math, the `tcWarnings` array generation, the TP1–TP10 profit-scenario loop.

**(B) Account/prop-firm state — fetch-and-display (lower priority, already correct in spirit):** the `/api/account/state` fetch itself (pre-fills balance display fields, correctly always-overwrite), the breach/pass/closed popup *triggers* (`openClosedAccountPopup`/`openBreachPopup`/`openPassedPopup` — correctly relay an already-backend-decided state string, comments explicitly state the calculator never decides breach/pass itself), and `getSelectedBreachReason()`/`getSelectedPassedReason()` (pure manual-selection recorders).

**(B) Account/prop-firm state — independently re-computes (higher priority, real duplicate-computation risk):**
- The prop-firm panel inside `renderAccountImpact()` — locally decides `isAlreadyBreached` (a boolean) from backend-supplied percentages rather than reading a backend-supplied boolean; locally derives dollar limits (`startBal × pct`) and severity-tier thresholds (90%/70% hardcoded in JS).
- `renderPropFirmFallback()` — a fully independent, fully offline breach/drawdown/daily-loss/profit-target engine used whenever `/api/trades/validate` is slow or down, with its own copy of the same thresholds (and, per direct comparison, **not identical** to the primary panel's thresholds — 90%/70% for DD in both, but 100%/70% vs. 100%/50% for daily-loss severity — evidence of the exact drift risk two independent copies create).
- `renderLiveAccountCard()` — combines a fetched `current_balance` with a locally-computed trade risk to derive "if this trade loses" projected balance/blown-account boolean, since `/api/trades/validate` doesn't return this for personal accounts. Exactly the pattern Kevin flagged as the highest-priority duplicate-computation problem, for live accounts specifically.
- **The persistent-write fields in both trade-object builders (RR-mode and main mode)** — `account_status_label`, `drawdown_at_entry_pct`, `breathing_room_used_pct`, `breathing_room_remaining`, `trades_remaining_buffer` are built from trader-editable form inputs (`#accountSize`/`#currentDrawdown`) combined with **URL-query-string-sourced** rule percentages (`?max_dd_pct=`/`?daily_dd_pct=`, not a live fetch), then **POSTed to `/api/trades/draft` and persisted permanently into the `trades` table.** This is the single highest-priority finding in this section: it is not transient UI, it is the calculator writing its own independently-derived breach/drawdown-budget snapshot permanently into trade history, via a structurally different derivation path (URL params + editable balance input) than the canonical phase-aware backend system — exactly the shape of the incident that prompted this whole audit, just not yet manifested as a visible bug because today's URL params happen to usually carry correct values.
- The always-visible `pfWarningBanner` (`_updateWarning()`/`updatePFStatus()`) — a **third** independent drawdown-proximity check (yet another set of thresholds: 80%/100% for DD, 50%/100% for daily), fired after every Calculate click, purely from form inputs + URL params, no backend call at all.

**(C) Flagged for human review, not classified either way:** the generic (non-prop, non-personal) `tcAccCurrentBalance` grid path — clean fetch-and-display code, but unclear from the file alone whether this branch is ever reached in production; and the broader design question of whether `window._getPFContext()`'s URL-param sourcing of rule percentages (rather than a live fetch) should be replaced with a fetch as part of this migration, since every locally-recomputed value in this section ultimately traces back to it and it has no staleness protection the way the balance fields do.

---

## Consolidated List — Confirmed Disagreements, Ranked by Severity

1. **`api/trades/validate.php`'s breach detection silently disables itself** for accounts using per-phase-only rules (defaults to `max_dd_pct=0`), while `getChallengeMetrics()` (used by Drafts/Dashboard) correctly enforces the real threshold. **Highest severity — a trader can see zero warning in the Calculator on a trade that gets marked breached the moment they save it.**
2. **`api/trades/validate.php` violates the P&L Rule directly** — uses uncapped `broker_pnl`/`projected_pnl` for prop-firm balance, never `challenge_pnl`, and is not phase-scoped at all.
3. **The calculator persists its own independently-derived drawdown-budget snapshot into `trades` rows** (`account_status_label`, `drawdown_at_entry_pct`, etc.) via URL-param rule percentages + editable balance inputs — permanent, not transient, and structurally isolated from the canonical engine.
4. **Six independent rules-JSON threshold-resolution cascades**, with confirmed real divergence in three of them (`validate.php`, `api/account/state.php`, `AccountPicker::apChallengeRulesSummary()`) from the canonical cascade.
5. **`dashboard.php`'s `passed_phase` branch trusts stale `challenge_accounts.current_stage`** directly, the exact pattern already fixed elsewhere in the same file/handler for `getChallengeMetrics()`.
6. **Trading Center's Account Home shows raw, non-phase-aware balance** for prop-firm accounts.
7. **Colex's "total balance" answer** uses the same raw, non-phase-aware sum.
8. **Colex's tilt/revenge detection disagrees with Colex's own stats widget** — one uses a raw loss-streak heuristic, the other reads real `trade_classification` values, on the same feature.
9. **Three independent "Consistency Score" formulas**, one (Colex chat) undisclosed as distinct from the other two.
10. **Trading Archive's risk-overage/adherence tolerance is a stale, superseded formula** (flat 20%, no per-asset-class distinction, no Contract-Constrained exclusion) vs. Reports' current formula.
11. **Three independent checklist-adherence statistics**, with two different "full vs. partial" thresholds (99.5% vs. 100%).
12. **`GRADE_MAP` duplicated verbatim** in two files with no shared constant.
13. **"Undisciplined classification" defined three ways**, disagreeing on whether `elevated_win` counts.
14. **Active Journeys' phase-progress bar can exceed 100%** while every other progress display caps at 100%.
15. **`api/account/state.php`'s drawdown % is unclamped/signed** vs. `getChallengeMetrics()`'s clamped-at-zero version.
16. **`api/account/state.php`'s displayed rule thresholds are not phase-aware**, unlike its own (correctly fixed) balance computation.
17. **Trading Archive's live-account "current drawdown %"** diverges from `AnalyticsEngine`'s version for any account with deposit/withdrawal history.
18. **Calculator's two independent breach-severity threshold sets don't literally match each other** (`renderAccountImpact()` vs. `renderPropFirmFallback()`) — direct evidence of drift between two copies of the same logic.
19. **Colex blends two different-scope drawdown figures** (whole-account lifetime + phase-boundary daily) in one chat response.

---

## 16. Win Rate / Expectancy / Profit Factor / Streaks

Mostly correctly centralized through `AnalyticsEngine::getAccountMetrics()`/`getUserMetrics()`/`computeMetricsFromTrades()` — confirmed clean in `api/analytics/portfolio.php`, `api/analytics/account.php`, `api/colex/chat.php`. Real exceptions found:

**CONFIRMED DISAGREEMENT — Risk of Ruin is two mathematically different formulas under the same name.** `dashboard.php`'s strategy-filtered branch computes `pow((q×avgLoss)/(p×avgWin), 20)` — the classic gambler's-ruin formula with a **hardcoded exponent of 20**. `EdgeHealthEngine::computeRawMetrics()` computes `pow((1−expectancyR)/(1+expectancyR), 100/avgRiskPct)` — an expectancy-R-based formula with an exponent **dynamically scaled to the trader's own real average risk%**. These only coincidentally agree when a trader's average risk is exactly 5% (100/5=20); for any other risk%, "Risk of Ruin" reads differently on Dashboard vs. Reports/Journal/Trader Profile (all of which use `EdgeHealthEngine`'s version).

**CONFIRMED DISAGREEMENT — Strategy Adherence, same label, two different tolerance policies.** `dashboard.php`'s "Strategy Adherence" card reads `mv_strategy_adherence`, whose `trades_within_risk_cap` column is a **flat, zero-tolerance check** (`t.risk_pct <= sv.max_risk_per_trade_pct`, confirmed in `migration_fix_views_is_validation.sql`). `reports.php`'s 4-Stage Summary and Trader Profile's "Trust in Your Strategy" bar both read `AnalyticsEngine::computeFourStageRaw()`'s Stage 1, which uses the explicitly-built per-asset-class tolerance system (1%-20% relative deviation depending on instrument, built specifically because a flat/zero-tolerance check was found to over-flag legitimate lot-rounding variance as false over-risk). The materialized view was never updated when `engine.php`'s tolerance logic evolved — same account/strategy shows a stricter (lower) adherence % on Dashboard than on Reports/Trader Profile for identical trades.

Duplicated (not disagreeing, but unshared) formula shapes: a phase/segment-scoped "current streak" loop is copy-pasted byte-for-byte twice within `journal.php` itself (`rnBuildPhaseCardFromRows()` and the Live-segment builder); `TradingArchiveEngine::segmentMetrics()` independently computes `max_win_streak`/`max_loss_streak`/terminal-streak via its own walk (a genuinely different metric — max historical length vs. current streak — not itself wrong, just unshared).

**Per-instrument/per-session performance — three independent implementations of the same underlying fact, not unified through the materialized views**: `dashboard.php`'s no-strategy fallback reads `mv_instrument_performance`/`mv_session_performance` directly; `reports.php` builds its own raw `GROUP BY t.instrument`/`GROUP BY t.session` SQL with its own filters rather than reading the views; `TradingArchiveEngine.php` aggregates in-memory from its own already-fetched rows. Not confirmed wrong today, but three separate code paths computing the same fact means any future fix (e.g., the asset-class tolerance work) risks being applied to only one of the three.

---

## 17. Lifecycle-State → Status Label / Badge Mapping, and a Second Confirmed `current_stage` Bug

`src/AccountPicker.php`'s `apAccountLifecycleBadge()`/`apHumanizeLifecycleState()`/`apLifecycleGlowBadge()` are the current, actively-used, correctly-shared implementation for Trading Center, Manage Live Accounts, and (via delegation) Journal's account picker. Two lower-risk duplicates exist: `journal.php`'s `deriveState()`/`classifyAccount()` (now only used for nav-tab counts) and `rnAccountLifecycleBadge()` (confirmed **dead code, zero call sites** — but its mapping silently differs from the live `apAccountLifecycleBadge()`, with no `breached`/`closed` case at all, falling through to `'Active'` — harmless only while unreachable; a landmine if anyone ever re-wires a call site to it). The `$currentLabel` phase-label derivation (`match(true){str_contains($lifecycleState,'phase_1')=>'Phase 1',...}`) is implemented twice, verbatim, in `journal.php::rnBuildJourney()` and `TradingArchiveEngine::gatherPropSegments()` — both correctly feed the one canonical `JourneySegmentation::partitionAccountJourney()`, but the *input* to that shared function is computed independently in two places.

**CONFIRMED, MAJOR — a second, independent instance of the exact `current_stage`-vs-`lifecycle_state` staleness bug, in the app's sole lifecycle-mutating write path.** `dashboard.php`'s `resolve_trade_id` handler, in its `passed_phase` branch (~lines 324-342), reads `challenge_accounts.current_stage` **directly** to decide which `_complete` state to write — the exact stale-column-trust pattern `getChallengeMetrics()` was explicitly fixed to stop doing on 2026-08-07 (CLAUDE.md documents live-confirmed accounts where `lifecycle_state='funded_active'` while `current_stage` is still stuck at `'phase_1'`), and the exact pattern `api/trades/validate.php` has its own comment explicitly stating it deliberately avoids for the same reason. This branch was not given the same fix. Since this is the literal write path for `lifecycle_state` — the field every other page in the app treats as ground truth (Trading Center bucketing, Trading Archive segmentation, Journal badges, Reports' Boundary Analysis) — a drifted account confirmed as "Passed Phase" here can have the **wrong** `lifecycle_state` written (e.g., a funded or phase-2 account regressed back to `phase_1_complete`), which then propagates as bad ground truth everywhere else in the app. This was independently found and flagged by two separate research passes (breach/pass logic and this one), which increases confidence it's real, not a research artifact — recommend direct verification against a real drifted account before Phase 4 prioritization.

`strategies.php` also passes `current_stage` through to the Calculator via `sessionStorage`/URL params (rather than `lifecycle_state`) as trade-capture metadata — lower risk since it's not used to derive a displayed label locally, but worth including in the migration scope since it's one more place `current_stage` is trusted.

---

## 18. R:R / Pips / P&L Per-Trade Display

**Correctly centralized, no disagreement found.** Journal's single-trade Lifecycle view (`tl4*` functions) reads `AnalyticsEngine::getFourStageRaw()`'s output directly rather than recomputing from raw prices. Drafts/Dashboard read the stored `risk_reward_tp1` column directly, no independent recomputation. `rnPipsOrPoints()` is defined once and reused.

---

## 19. Trader Profile Module & Period-Boundary Computation

Trader Profile's own architecture is clean: `TraderProfileService.php` (pure orchestration, zero SQL), `TraderProfileIdentity.php` (pure selection logic, zero SQL), `TraderProfileNarrative.php`/`TraderProfileCoaching.php` (pure copy generation, zero SQL) — genuinely new signals in `TraderProfileBehaviorEngine.php` are disclosed as new and correctly pull Discipline/Consistency/Strategy-Adherence from `EdgeHealthEngine`/`AnalyticsEngine` rather than re-deriving them.

**CONFIRMED — period-boundary (Weekly/Monthly/Quarterly/Annual calendar range) math is independently implemented in at least four places**, a latent-drift risk rather than a currently-observed disagreement (all four are algebraically equivalent today): `reports.php`'s inline switch, `TraderProfileService::periodBounds()` (its own comment admits this is a deliberate duplication rather than an extraction), `ColexHelpers::calculateDateRange()`, and `api/colex/chat.php`'s own inline "first day of this month" logic. CLAUDE.md's own Reports section already flags that no shared `getReportingPeriodBounds()` function exists — this audit confirms the gap is wider than just Reports.

---

## 20. Trading Center, Remaining API Endpoints, MT5, Cron, Onboarding, Strategy

**CONFIRMED, MOST SEVERE FINDING IN THE ENTIRE AUDIT — `public/mt5-resolve.php`'s `createTradeFromMT5()` completely bypasses the app's sole lifecycle mutator.** Directly verified by reading the function and its call site. This is a real, live, reachable feature (`action === 'create_trade'` POST handler, the "create a TraderAlign trade from an unmatched MT5 statement row" workflow) that `INSERT`s a new trade directly with `status='closed'` and `broker_pnl` set from the parsed statement — it never computes `challenge_pnl`/`excess_pnl` (the P&L Rule's capped, prop-firm-correct figure), and never touches `trading_accounts.current_balance` or `challenge_accounts.current_balance` at all. Two compounding consequences for a prop-firm account: (1) this trade's effective P&L falls through the app-wide `challenge_pnl ?? broker_pnl ?? projected_pnl` cascade straight to the **uncapped** `broker_pnl` everywhere it's read — a direct P&L Rule violation, not just a display bug; (2) the account's balance fields **never update to reflect this trade at all** — every subsequent phase-aware balance/drawdown/breach computation for that account silently omits real trade history from that point forward. This is more severe than every other finding in this audit because it's not a wrong number shown somewhere — it's a real, permanent gap in the account's actual data, created by using a real, intended feature exactly as designed. This file/function was not among the four call sites CLAUDE.md's 2026-08-06/07 incident write-up documents as fixed.

**CONFIRMED, second-most-severe — `api/trades/validate.php` independently re-confirmed by a completely separate research pass.** This endpoint (already documented in §1i/§4/§6 by the balance and breach/pass research passes) was independently rediscovered by this pass too, with additional context: it is explicitly **not** among the four call sites the 2026-08-06/07 incident fix addressed, confirming it is a genuinely missed fifth site, not a partial fix. Cross-confirmation by two independent research passes (plus this session's own direct verification) raises confidence this is real and exactly as severe as described.

**CONFIRMED — `api/mt5/reconcile.php`'s `applyReconciliation()` has the same class of gap as `mt5-resolve.php`, on the *matching* (not creation) path.** It writes `broker_pnl` as a straight pass-through of the parsed statement's `profit` field, but **never** recomputes `challenge_pnl`/`excess_pnl`/`current_balance` for the trade it's reconciling. Reconciling an MT5-matched trade against an existing draft/open trade leaves that trade's phase-aware balance contribution stale, exactly like the creation path above, just via a different entry point. Both MT5 code paths need the same fix: route through the same `challenge_pnl`-capping logic `dashboard.php`'s `resolve_trade_id` already correctly implements, rather than writing `broker_pnl` and stopping.

**CONFIRMED — the rules-JSON threshold cascade is a 5-way divergence, not four as earlier sections found**, with a concrete, demonstrable failure mode: `src/AccountPicker.php`'s `apChallengeRulesSummary()` (Trading Center's Account Home) checks only `phase_1_target_pct` for phase 1 with no fallback to `phase_1_profit_target`/`profit_target_pct`/`target_pct`, and never checks phase-specific drawdown keys at all (flat `max_dd_pct`/`daily_dd_pct` only). **A real account whose `rules` JSON stores `phase_2_profit_target` (not `phase_2_target_pct`) will show a real number on Dashboard/Drafts/Reports and literally "Not specified" on Trading Center's own Account Home for the identical account.** `api/trades/validate.php` is confirmed as a fifth independent cascade with the same gap, live-called by the Calculator.

**CONFIRMED — draft-expiry urgency thresholds are silently broken for swing/position traders, a real, live UI bug unrelated to the balance/prop-firm cluster but the same duplication pattern.** `public/drafts.php`'s `$urgencyThresholds` (line 174) uses keys `swing_trader`/`position_trader`; the account's real trading style, sourced from `strategies.trading_style`, only ever stores the values `swing`/`position` (confirmed via Strategy Builder's own dropdown and `engine.php`'s documented values). Directly verified: `$thresholds = $urgencyThresholds[$draftTradingStyle] ?? $urgencyThresholds['day_trader']` — the key never matches for a real swing or position trader, so **every swing/position trader's "urgent" draft badge silently uses `day_trader`'s much stricter threshold (12h) instead of the intended 240h/2160h**, firing far earlier than the trader's real style calls for. A third, unrelated copy of the same draft-expiry concept (`config/config.php`'s `DRAFT_EXPIRY` constant) is confirmed to have **zero consumers anywhere** — fully dead. `cron/daily.php` has its own fourth, hardcoded copy (the one that actually performs the real expiry), keyed correctly against `user_profiles.trading_style` rather than `strategies.trading_style` — meaning the cron job and the UI badge can genuinely disagree about a draft's real urgency, sourced from two different tables.

**CONFIRMED — `src/RenderHelpers.php`'s `accountCurrentBalance()` — a function explicitly built and documented as *the* single source of truth for account balance — currently has zero real callers anywhere in the codebase.** Its own doc-comment states it exists specifically because of the three-different-balance-values incident and that "every part of TraderAlign... must read this SAME field, via this SAME function." A same-day attempt to route Journal's Funded card through it was reverted (documented in a `journal.php` comment) because it broke phase-scoped balance math for mid-evaluation accounts — the tension between "one balance function" and "balance means something different pre- vs. post-funding" was hit directly and never resolved. This function should not be treated as already-solving the problem; it's a real, if currently inert, prior attempt worth reviewing during Phase 2/4 design rather than duplicating a sixth time.

**CONFIRMED — `api/journal/save-goal.php` independently reconstructs account balance a sixth time** (lines 46-76), for the Growth Target feature's "current balance" input — its own hand-written copy of the funded-segment-balance pattern, explicitly modeled on `journal.php`'s Funded card by code comment, but not a shared call.

**Dead files/endpoints confirmed via reachability check (route + grep for any reference) before content was analyzed, consistent with this audit's methodology throughout:** `api/analytics/portfolio.php`, `api/analytics/account.php` (plus a second, previously-undocumented orphaned scorer inside it, `calculateRiskGrade()`), `api/analytics/test.php`, `api/analytics/career.php`, `api/analytics/behavior.php` — all zero references anywhere. `api/accounts/eligible-for-promotion.php` **is** routed but has zero JS/fetch callers (Trading Center deliberately doesn't use it, per existing CLAUDE.md notes) — its eligibility query is also confirmed to diverge from the shared `apPromotableLifecycleStates()` (doesn't restrict per challenge type the way the live path does). `api/analytics/test2.php` (`<?php echo "HELLO";`) was confirmed dead and moved to `dead-files/` as part of Phase 3, per this document's own established criteria.

**Confirmed clean, no findings:** `api/mt5/parse.php` (pure statement parser, no independent computation), `public/strategy.php`/`strategy-validation.php`/`api/strategy/save.php`/`load.php` (pure capture/persist, no adherence computation — that logic correctly lives only in `computeFourStageRaw()`), `public/onboarding.php`/`onboarding-stage3.php`/`api/onboarding/*.php` (pure form capture), `src/CredentialCrypto.php`/`ScopedDb.php`/`ReportsCoaching.php`/`JournalTradesQuery.php` (pure infrastructure/text-lookup), `api/accounts/transaction.php` (confirmed as the real, correctly-maintained single writer for live-account deposit/withdraw math — the canonical source, not a duplicate), `api/accounts/create.php`'s promote branch and `close.php` (correct, already the real state machine other code mirrors), `public/add-prop-account.php`/`add-live-account.php`/`manage-live-accounts.php` (properly delegate to shared account-fetching/eligibility functions), `api/journal/update-account-meta.php`/`trades-more.php`/`update-note.php`/`reveal-account-secret.php`/`api/checklist/save.php` (pure CRUD).

Minor/lower-confidence, worth naming but not acted on: `src/PropFirmHelpers.php`'s `resolvePropFirmDisplayLabel()` is a third independent "phase label from lifecycle_state" deriver (alongside `AccountPicker`'s and `journal.php`'s own) — currently agrees in output but is a separately-maintained match statement; `cron/daily.php`/`hourly.php` run a byte-identical "Verification Sweep" block — harmless (idempotent) duplication, worth consolidating for maintainability, not correctness.

---

## Consolidated List — Confirmed Disagreements, Ranked by Severity (Final)

Audit complete. All seven research passes finished; every item below is either directly verified by me against source, or cross-confirmed by two independent research passes, or both. Re-ranked to reflect the final, most severe finding, which only emerged from the last research pass.

1. **`public/mt5-resolve.php`'s `createTradeFromMT5()` completely bypasses the sole lifecycle mutator.** A real, live, reachable feature inserts closed trades with only uncapped `broker_pnl`, never computes `challenge_pnl`, and never updates account balance at all. This is the single most severe finding in the audit — not a wrong number shown somewhere, but a permanent, silent gap in an account's real data, created by using an intended feature exactly as designed. Directly verified by reading the function and confirming its call site is a real POST action on a live page. *(§20)*
2. **`api/trades/validate.php`'s breach detection silently disables itself** for accounts using per-phase-only rules (the documented-normal case for real accounts) — zero warning in the Calculator on a trade Drafts/Dashboard would immediately mark breached. Independently found and confirmed by two separate research passes plus my own direct read of the file. *(§4/§6/§20)*
3. **`api/trades/validate.php` violates the P&L Rule directly and is not phase-scoped** — same file as #2, a second independent defect. *(§1i/§20)*
4. **`api/mt5/reconcile.php`'s `applyReconciliation()` has the same class of gap as #1, on the matching (not creation) path** — writes `broker_pnl` from the parsed statement but never recomputes `challenge_pnl`/`excess_pnl`/balance for the trade being reconciled. *(§20)*
5. **The calculator persists its own independently-derived drawdown-budget snapshot into `trades` rows**, sourced from URL params + editable balance inputs. *(§15)*
6. **Two independent, confirmed instances of `dashboard.php`'s `resolve_trade_id` (the sole lifecycle mutator) trusting stale `challenge_accounts.current_stage`** instead of `lifecycle_state` — the balance-capping context and the `passed_phase` branch's phase-completion write. Can write a wrong `lifecycle_state` on a drifted account, corrupting ground truth for the whole app. *(§6/§17)*
7. **`src/ColexHelpers::getPeriodStats()` has multiple confirmed P&L Rule violations**, including a real double-count of P&L for prop-firm trades with both `challenge_pnl` and `broker_pnl` populated. Directly verified by reading the file. *(§16)*
8. **The rules-JSON threshold-resolution cascade is independently reimplemented in (at least) five places**, three confirmed to diverge from canonical, including a concrete demonstrable case: an account's declared Phase 2 profit target shows a real number on Dashboard/Drafts/Reports and literally "Not specified" on Trading Center's own Account Home for the identical account. *(§6/§20)*
9. **Draft-expiry urgency thresholds are silently broken for swing/position traders** — a real, live UI bug where every swing/position trader's "urgent" badge fires at 12 hours instead of the intended 240/2160 hours, because the display layer's threshold keys never match the account's real stored trading-style value. A third, fully dead copy of the same concept also exists with zero consumers, and the cron job that actually expires drafts reads yet a fourth copy, keyed against a different table than the UI badge. *(§20)*
10. **Risk of Ruin is two mathematically different formulas under the same name.** *(§16)*
11. **Strategy Adherence is two different tolerance policies under the same name.** *(§16)*
12. **A function explicitly built and documented as "the" single source of truth for account balance (`accountCurrentBalance()`) has zero real callers** — a prior, documented attempt to solve exactly this problem exists in the codebase already, unused, with a documented reason it was reverted (broke phase-scoped math). Worth reviewing during Phase 2 design rather than building a competing solution from scratch. *(§20)*
13. **Trading Center's Account Home shows raw, non-phase-aware balance** for prop-firm accounts. *(§1g)*
14. **Colex's "total balance" answer** uses the same raw, non-phase-aware sum. *(§1j/§16)*
15. **Colex's tilt/revenge detection disagrees with Colex's own stats widget.** *(§9)*
16. **`api/journal/save-goal.php` independently reconstructs account balance a sixth time.** *(§20)*
17. **Three independent "Consistency Score" formulas.** *(§8)*
18. **Trading Archive's risk-overage/adherence tolerance is a stale, superseded formula.** *(§12)*
19. **Three independent checklist-adherence statistics**, two different full/partial thresholds. *(§11)*
20. **`GRADE_MAP` duplicated verbatim** with no shared constant. *(§9)*
21. **"Undisciplined classification" defined three ways**, disagreeing on `elevated_win`. *(§9)*
22. **Active Journeys' phase-progress bar can exceed 100%.** *(§5)*
23. **`api/account/state.php`'s drawdown % is unclamped/signed.** *(§2)*
24. **`api/account/state.php`'s displayed rule thresholds are not phase-aware.** *(§6)*
25. **Trading Archive's live-account drawdown % diverges from AnalyticsEngine's version** for accounts with deposit/withdrawal history. *(§2)*
26. **Calculator's two independent breach-severity threshold sets don't literally match each other.** *(§15)*
27. **Colex blends two different-scope drawdown figures** in one chat response. *(§2)*
28. **Period-boundary math independently implemented in 4 places** — latent risk, not yet observed disagreeing. *(§19)*
29. **A dead-but-present duplicate badge mapping** (`rnAccountLifecycleBadge()`) that would silently disagree with the live one if ever re-wired. *(§17)*

**Total: 29 confirmed or strongly-suspected disagreements**, spanning balance, drawdown, breach/pass, phase transitions, scores, classification, checklist adherence, commentary stats, and status labels — across every major subsystem in the app (Journal, Reports, Drafts, Dashboard, Calculator, Trading Center, Trading Archive, Colex, MT5 reconciliation, and Trader Profile). This fully confirms the original diagnosis: not N unrelated bugs, one architectural gap.
