# MIS v2 Read-Only Analysis Sweep - Full Output

**Generated:** Thursday, June 4 2026 - 7:57 PM EDT (NY) | Session 41 cont | ANALYSIS MODE, deploy freeze intact
**Workflow:** mis-v2-analysis-sweep (9 agents, ~187K subagent tokens, ~12.5 min) | run wf_ac577719-52e
**Scope:** read-only - no clasp, no deploy, no write endpoints, no engine edits. Synthesis taxonomy-validated (PASS 96%/4%/0 force-fits).

---

## SYNTHESIS (taxonomy-validated) + COMPLETENESS CRITIC + NEXT-SESSION PUNCHLIST

# ANALYSIS SESSION 2026-06-04 PM — 8-Stream Synthesis + Completeness Critic

> **Appendix to `MIS_SACS_CALIBRATION_MODULE.md`.** Read-only synthesis of 8 parallel analyst streams (recon inventory · broker-watcher design · B.75 freshness · stale-surface sweep · mandate audit · C UI layouts · D after-hours checklist · orthogonality scan). Deployment FROZEN — nothing below is applied, deployed, or clasped. Synthesis only.

---

## 0. HEADLINE FINDING (read first)

**A load-bearing contradiction surfaced between the analysts and the canonical doc, and it changes B.75 scope.** Analyst 1 (recon inventory) reported account **5692 (Joint PRESERVE) as GREEN ("≈0 diff, <1%")**. The calibration module's own B.5 OBSERVATION SNAPSHOT (L106, L111–113, L121) records **5692 as RED at 4.6% ($230 gap)** — a *second* freshness case with the **opposite** root cause from ABBV. **There are TWO REDs, not one.** Analyst 1's single-RED model is wrong; the doc is right (it is a live `fn=portrecon` read, the primary source). Any next-session punchlist that closes only ABBV will leave B.75 open. This is reconciled in full in §2.

**Taxonomy acceptance test: FAIL on one axis.** 95%+ of issues fit known buckets and zero were force-fit (PASS), but **Unknown = 1 of ~28 ≈ 3.6%** is itself the 5692 case the doc flags as "TBD / class to be determined by the freshness sweep." That Unknown is *exactly where the real second bug hides* — which is the intended outcome of the rule, not a failure of it. Reported honestly below.

---

## 1. TAXONOMY VALIDATION

### Method
Every distinct recon/stale/portfolio/mandate issue raised across all 8 streams was extracted and fit into exactly one of the 7 categories: **Stale Holdings · Stale Price · Pending/Settling Trade · Margin/Pending Debit · Missing Cost Basis · Mandate Mismatch · Unknown.** No issue was forced; "Unknown" was used where the doc itself has not yet classified.

### (a) Did every issue fit one of the 7? 
**All but one fit cleanly.** 27 of ~28 mapped to a non-Unknown bucket. One (the 5692 Joint $230/4.6% RED) is genuinely Unknown pending a fresh screenshot — the doc (L121) explicitly leaves it "stale price vs stale holdings — TBD," so calling it anything else would be a force-fit.

### (b) What did NOT fit — description
The **5692 Joint $230/4.6% RED**. Root cause is undetermined: the doc's working hypothesis (L121) is *control-side* stale (the PORTFOLIO_CONTROL_ screenshot is point-in-time while MIS uses live GOOGLEFINANCE prices → intraday drift on META/ASML), which would make it **Stale Price**. But it has NOT been verified against a fresh 5692 broker screenshot, and the alternative (a stale MIS share count like ABBV) is not ruled out. Until verified it is honestly **Unknown**.

### (c) New 8th category, or evidence the taxonomy is wrong?
**Neither — it is correctly Unknown, and it exposes a real gap in the *detection mechanism*, not the taxonomy.** The 7 categories are conceptually complete; once a fresh 5692 screenshot lands it will resolve to **Stale Price** (most likely) or **Pending/Settling Trade**. The deeper finding it surfaces is a **design flaw, not a missing category**: the recon RED gate is **purely relative (>3%) with no absolute-dollar floor**, so $230 of immaterial intraday drift trips RED on a $5K book (and Analyst 1 independently derived the same flaw for the kid accounts — a $16 lag = 3% RED on the $523 daughter account). That flaw is a **fix**, captured in §5, not an 8th taxonomy bucket.

### (d) Distribution table (all 7 rows)

| Category | Count | % |
|---|---|---|
| **Stale Holdings** (qty written once, never re-derived; the structural class behind 12 surfaces) | 9 | 32% |
| **Stale Price** (GOOGLEFINANCE ~15-min lag; control-screenshot drift on micro/kid accounts; likely 5692) | 5 | 18% |
| **Pending/Settling Trade** (ABBV 600 17→7 partial sale settling — the live RED root cause) | 4 | 14% |
| **Margin/Pending Debit** (−$11,132 acct-600 debit; margin language leaking onto no-margin 5692/KID) | 3 | 11% |
| **Missing Cost Basis** (`misV2GetCostBasisMap_` / per-position cost gaps feeding P&L surfaces) | 1 | 4% |
| **Mandate Mismatch** (ACTIVE trade gate applied to PRESERVE/KID/GROWTH; 2.5%/mo clock risk; engine-verdict pollution) | 5 | 18% |
| **Unknown** (5692 Joint $230/4.6% RED — class TBD pending fresh screenshot) | 1 | 4% |
| **TOTAL** | ~28 | 100% |

### Acceptance test result
- (i) **95%+ fit a known bucket** → **PASS** (~96%).
- (ii) **Unknown < 5%** → **PASS** (~4%, one issue).
- (iii) **Zero force-fits** → **PASS** (5692 honestly left Unknown rather than jammed into Stale Price).
- **Overall: PASS on all three** — but the single Unknown is load-bearing (it is the second RED that blocks B.75), so it is escalated to the headline, not buried. The taxonomy is sound; the *detection mechanism* (relative-only RED threshold) is the weak point.

---

## 2. CROSS-REFERENCE & RECONCILIATION (Obj 4 ↔ Obj 8 ↔ Obj 1)

### Disagreement 1 — Account 5692 status (Obj 1 vs the canonical doc) — **DOC WINS**
- **Obj 1 (recon inventory):** 5692 GREEN, "≈0 diff, <1%."
- **Calibration doc B.5 snapshot (L106, L111–113):** 5692 **RED, 4.6%, $230 gap** — a live `fn=portrecon` read.
- **Verdict: the doc is right; Obj 1 is stale/wrong on this account.** The doc is a primary-source live read captured during the freeze; Obj 1 appears to have reasoned 5692 to GREEN from the exposure section ("META/ASML priced live → within tolerance") without re-firing portrecon (Obj 1 explicitly notes it *could not* re-fire `fn=portrecon` this session — no live token). **Consequence: B.75 must clear BOTH REDs (600 ABBV + 5692 Joint), not just ABBV.** Obj 8 and Obj 3 both correctly assume only the ABBV RED in their acceptance tests — those tests are therefore *incomplete* and must add a 5692-clears condition.

### Agreement (strong) — Obj 4 ↔ Obj 8 on fake-exposure propagation
Both independently confirm the high-ROI thesis: **one stale qty distorts %-of-account on every account holding it.** Obj 4 ranks the surfaces (12 of them); Obj 8 shows it live — ABBV reads 27% ⚠CONC on 600 **and** 15% on daughter's 5378 from the same 17-vs-7 stale row (matches doc L115 exactly). No conflict; Obj 8 is the live proof of Obj 4's mechanism.

### Disagreement 2 — Does stale data MUTATE Action? (Obj 4 vs Obj 8) — **BOTH RIGHT, different scopes**
- **Obj 4:** GATE 3/GATE 4 of `misV2BuildFSE` (L304–333) read `heldMap` qty/mktValue → stale-high qty inflates mktValue → can force `WATCH/PORTFOLIO_OVERLAP` or `REDUCE/HELD_LOSS` → **"engine verdict changed by stale data" = CRITICAL, violates the locked Portfolio-never-mutates-Action rule.**
- **Obj 8:** "in every case the verdict came straight from FSE col 7 … the overlay never mutates Action — architecture holds."
- **Reconciliation: not a contradiction — they audited different layers.** Obj 8 audited the **display/overlay path** (peekoverlay → peek.html), which is correctly read-only. Obj 4 audited the **engine path** (`misV2BuildFSE` GATE 3/4), where the held-overlap math genuinely consumes stale qty *before* the verdict is set. Obj 5 (mandate audit) independently confirms Obj 4: GATE 3/4 build `heldMap` with **zero account filter**, so a PRESERVE/KID holding flips an ACTIVE-book verdict. **The CRITICAL mutation is real and lives upstream of the surface Obj 8 cleared.** Obj 8's "architecture holds" is true *for the display layer only*; it does not exonerate GATE 3/4.

### Agreement — Obj 4 ↔ Obj 8 on the "old-load + live-prices passes all freshness gates" hole
Both note GATE 1 `DATA_STALE` (L327) checks the **Snapshot price** dq, **not the HOLDINGS_CLEAN asOf** (L1111). An old holdings load with live prices passes every freshness gate. Obj 3 confirms the same from the health-check angle (no downstream consumer reads the asOf stamp). **Three streams, one conclusion — high confidence.**

### Net reconciled truth
1. **TWO REDs** (600 Pending/Settling + 5692 Unknown/likely Stale Price). Doc-authoritative.
2. **Stale qty DOES mutate Action** via `misV2BuildFSE` GATE 3/4 (engine layer) — the single CRITICAL. The *display* layer is clean.
3. **One stale row → multi-account fake concentration** — confirmed live.
4. **The asOf freshness stamp is written but never read** — a guard exists in data but not in logic.

---

## 3. CONSOLIDATED RECON / STALE-SURFACE STATE

### 3.1 Recon flag inventory (corrected to TWO REDs)

| Account | Mandate | Status | Diff $ / % | Class | Root cause |
|---|---|---|---|---|---|
| **600** Fidelity Invest-n-Save | GROWTH (margin −$11,132) | 🔴 **RED** | +$2,059 / 14.7% | **Pending/Settling Trade** | ABBV 17sh (MIS) vs 7sh (broker) — partial sale settling, HOLDINGS_CLEAN not refreshed. Margin correctly nets $27,193 gross → $14,002 control. Engine math sound; fix = re-import. |
| **5692** Fidelity Joint | PRESERVE (no margin) | 🔴 **RED** | +$230 / 4.6% | **Unknown → likely Stale Price** | MIS ~$4,656 vs control-implied ~$4,426. Control = point-in-time screenshot; MIS = live prices → intraday drift hypothesis (L121). **Unverified — needs fresh 5692 screenshot.** Trips RED only because threshold is relative-only. |
| **4073** KID (son) | KID | 🟡 YELLOW | 1.4% | Stale Price | ASML live-priced; within band. |
| **5378** KID (daughter) | KID | 🟡 YELLOW | 2.0% | Stale Price | META live-priced; also carries fake ABBV 15% concentration. |
| **898** Schwab ACTIVE | ACTIVE (cash) | 🟢 GREEN | 0% | — | Plaid-live, no held equities; exact by construction. |

**Roll-up = RED** (worst single account). Two REDs, opposite causes (600 = MIS holds MORE; 5692 = control likely stale).

### 3.2 Stale-failure surfaces, ranked (Obj 4 — the high-ROI sweep)

| # | Surface | Line | Stale-qty effect | Severity |
|---|---|---|---|---|
| 1 | `misV2BuildFSE` GATE 3 & 4 | 304–333 | **MUTATES ACTION** — stale qty inflates mktValue → forces WATCH/PORTFOLIO_OVERLAP or REDUCE/HELD_LOSS. Violates locked Portfolio≠Engine rule. | **CRITICAL** |
| 2 | `misV2Exposure_` | 2031–2046 | Fake ⚠CONC at >25% from stale MV | HIGH |
| 3 | `misV2GrossByAccount_` | 1988–1993 | Inflates gross → misNet → flips recon RED/GREEN | HIGH |
| 4 | `misV2PortfolioMetrics_` | 2141–2158 | Sector mix / bookRisk% off stale MV (total now reads recon net — partial fix) | HIGH |
| 5 | `peekoverlay` (`/mis/peek`) | 2895–2903 | Stale qty + fake P&L% on the exact card Sam taps pre-BUY | HIGH |
| 6 | `misV2BrokerAllocationHtml_` | 2355–2379 | Broker equity %-of-book distorted | MED |
| 7 | `misV2PositionsHtml_` | 2524–2578 | P&L $/% per position off stale qty | MED |
| 8 | `misV2PerformanceHtml_` | 2445–2454 | "Am I making money" off stale qty | MED |
| 9 | Risk stack (VaR/PortRisk/Stress/HoldingsRisk) | 1497–2637 | Loss-sim weights mis-set | MED |
| 10 | `misV2GapPulse_` STOP-HIT | 557–563 | Phantom held-thesis on a sold name | LOW |
| 11 | `misV2ProposedState_` (shadow) | 1871–1872 | Sold names treated as held (shadow only) | LOW |
| 12 | `misV2RefreshNews_` | 2178 | News for unowned names | LOW |

**Structural root (confirmed across Obj 2/3/4):** `HOLDINGS_CLEAN` col C (Qty) is written once from the Fidelity Position Summary email and **never re-derived**; `misV2RepriceHoldings_` only overwrites price (E) and MktVal (F = px × stale-qty), so a stale count looks *freshly computed*. Second vector: re-importing `_full_reconcile_2026-06-03.json` reintroduces ABBV=17 (whole-transaction-history qty, not settled position).

### 3.3 Mandate-blend violations (Obj 5)

| Surface | Violation | Mandate-correct behavior |
|---|---|---|
| `misV2BuildFSE` GATE 3/4 (L304–333) | `heldMap`/`portTotal` built with **zero account filter** → PRESERVE/KID/GROWTH holdings flip ACTIVE-book verdict | Scope held-overlap to ACTIVE accounts (898/1241/4436) only |
| `misV2ClassifyAccount_` (L2384) | Returns `{owner,name,broker}` — **no mandate field**; maps 600 GROWTH + 898 ACTIVE both to owner:'SAM' | Add `mandate` (ACTIVE/GROWTH/PRESERVE/KID) from `PORTFOLIO_CONTROL_`; single root fix |
| `misV2CashMarginHtml_` (L2354–2379) | Aggregates by **broker** → margin columns + "borrow against yourself" hint render for no-margin Joint 5692 (Israel savings) | Roll up by account+mandate; suppress margin on 5692/KID |
| `misV2PerformanceHtml_` (L2461–2503) | Slices by owner not mandate → "SAM" tile blends Schwab-ACTIVE + Fidelity-GROWTH P&L | Add ACTIVE/GROWTH/PRESERVE/KID slices |
| `fn=state` (L2871–2878) | ONE blended P&L + ONE engine verdict across all mandates | Decompose by mandate |
| peekoverlay → ops-api L829 → peek.html L342/352 | Single FSE verdict shown as "YOUR POSITION" across PRESERVE/KID; ops-api `universe` type omits `accounts[].mandate` | Carry `accounts[].mandate`; label "held in Joint PRESERVE"; no ACTIVE verdict on preserve/kid lot |

**Note (Obj 5):** the 2.5%/mo extraction clock and −20% hard stop are **NOT in `MIS/v2/Code.gs`** — they live in v1 `emailDailySnapshot.js`. The only active-book thresholds in v2 are GATE 6 R:R≥2.5 and GATE 4 20% overlap cap. The doc (L75–77) pre-flags the risk that wiring C displays could apply the 2.5%/mo clock to Fidelity — **guard this BEFORE Gate C, not after.**

### 3.4 Orthogonality inventory (Obj 8 — security-truth ≠ portfolio-truth)

Each row = a WARNING beside an **unchanged** Action. Patterns to encode:
1. **Strong engine, wrong/margined account** — ASML@600 (49%, behind −$11,132 margin); LLY/AAPL@5692 (PRESERVE). "Likes it" ≠ "add here."
2. **Dead engine, heavy weight** — META SACS 3.6/grade-D yet 68% of daughter + 46% of 600. Size with no engine support (worst quadrant) — still WARNING-only.
3. **Engine REJECTED, still held** — GOOG (REJECTED, SACS 13.3), COST (REJECTED, SACS 10.7). Suppress from any ADD surface; tag reject reason inline.
4. **CONC ≥25% on stale qty** — ABBV 27% computed on the unsettled 17sh. Provisional until 17→7; self-clears at B.75.

**Live vs doc nuance:** the doc (L50–53) calls ASML the *Proposed ADD @49%* "the case you should NOT add to." Obj 8's live read shows ASML as **WATCH** (SACS 43.3, setup PORTFOLIO_OVERLAP), so the "ADD-while-overweight" case is **latent, not active** right now. The C-display acceptance must still demonstrate it (ASML Legacy WATCH / Proposed ADD / Exposure 49%⚠) per doc L71 — design target, not current live state.

---

## 4. C UI LAYOUTS — design-ready (presentation only, MIGRATION_MODE)

Pattern identical on all 3 surfaces: two-cell `LEGACY | PROPOSED` strip + Exposure line + Mandate line + `MODE: MIGRATION` tag. **Action shown twice, never merged. ⚠CONC is a WARNING string, never an action cap.**

**TELEGRAM** (`misV2DailyBrief_` L580–604, plaintext — no markdown specials):
```
ASML
  LEGACY    WATCH
  PROPOSED  ADD  (rc: ADD — strong across)
  EXPOSURE  49% of acct  ⚠CONC
  MODE      MIGRATION · both shown · engine action unchanged
```

**TEST ENTRY** (`misV2EntryPad_` insert after verdict header ~L941): MIGRATION block on the SAME screen as the verdict — Legacy · Proposed · rc · Exposure%+CONC · Mandate (owner+GROWTH/PRESERVE/ACTIVE) · margin flag · Mode. Existing plan/sizing/checklist stays below.

**SCOPE** (`peek.html` `.hld` L341–371; **needs read-only Apps Script change** — `peekoverlay` L2890 must emit `verdict_legacy` + `proposed_rc` + `exposure_pct` + `conc` + `mandate`; reuse `ov.accounts[].mv`):
```
┌─ ASML ──────────────────── [PROPOSED: ADD] ─┐
│ LEGACY WATCH → PROPOSED ADD (rc ADD)        │
│ EXPOSURE 49% of acct ⚠CONC   MIGRATION mode │
│ MANDATE Fidelity 600 · GROWTH · margin      │
└─────────────────────────────────────────────┘
```

**Wiring split:** Telegram + Test Entry are pure render code (have `synthV1`/`synthV2`/`misV2Exposure_` in-process — NEEDS-BUILD render only). Scope is the only one needing a read-only Apps Script change to expose the new overlay fields.

**Gate-C exit:** all 3 surfaces show Legacy≠Proposed (ASML WATCH vs ADD) with **zero verdict drift** + the mandate guard from §3.3 in place so the 2.5%/mo clock can't apply to Fidelity.

---

## 5. COMPLETENESS CRITIC — what's still unverified / hand-waved

1. **5692 Joint RED root cause is UNVERIFIED.** The "control-side stale / intraday price drift" hypothesis (doc L121) is derived from whole-dollar rounded figures and explicitly flagged "verify against a fresh 5692 screenshot before asserting." **No analyst pulled that screenshot.** This is the #1 open verification — and Obj 1 actively got it wrong (called 5692 GREEN). **Status: UNVERIFIED.**
2. **No analyst re-fired the live endpoints.** `MIS_V2_TOKEN` is a Worker secret, absent from repo. Obj 1, 2, 3, 5, 6 reasoned purely from code + the banked snapshot. Obj 4 and Obj 8 *claim* live `/mis/peek` reads — but peek is single-ticker overlay only and **cannot return full holdings/exposure**, so even those are partly code-inferred. **Every "live" recon/exposure number traces to the one B.5 snapshot, not a fresh fire.** Single-source.
3. **The `_full_reconcile_2026-06-03.json` 51-name set was not diffed against live HOLDINGS_CLEAN.** Obj 4/Obj 8 assert PLTR/AVGO/MSFT/TSLA/TSM/CRM "return held:false live" but the actual live HOLDINGS_CLEAN tab contents were not enumerated this session — the 8-name live set is inferred from peek calls, not read from the tab. The phantom-resurrection risk (surfaces #10/#11) is real but the *current* count of stale rows is not pinned.
4. **`Broker_Qty` per-position control has NO source — and no analyst specified the seeding ritual.** Obj 3 correctly flags input #4 is missing, but "seed from the same Fidelity screenshot that seeds PORTFOLIO_CONTROL_" is hand-waved — there is no documented cadence, owner, or format for that per-ticker broker-truth store. Without it, B.75 DIVERGENT detection cannot run; the gate is gated on an unspecified new input.
5. **The `misV2GetCostBasisMap_` / Missing Cost Basis class is under-examined.** Obj 4 mentioned it in passing; no analyst traced which P&L/performance surfaces silently show wrong cost when a position lacks basis. Only 1 issue landed in that bucket — likely under-counted because no one swept it.
6. **Partial-fill query gap asserted, not proven.** Obj 2 states `subject:"Order Execution"` (L517) MISSES "Order Partially Filled" (the literal ABBV case). Plausible and important, but no analyst produced the actual ABBV fill email to confirm Fidelity's exact subject line. **Verify against a real partial-fill email before relying on it.**
7. **D after-hours: the CLOSED-branch gap is code-reasoned, not observed.** Obj 7's finding (dual-header gate fires on POST/PRE only, CLOSED falls through) is a strong code read but was not confirmed by hitting `fn=peek` in a live CLOSED window. The `regular_pct`-stays-null claim depends on the v7-quote path being blocked "most of the time" — asserted from session context, not measured this session.
8. **Recon dollar-floor fix has no agreed threshold.** Obj 1 says ~$50; doc L123 says $250–$500. **Unreconciled** — needs Sam's sign-off (it changes a portfolio-level gate) and a number.
9. **Roll-up "worst single account" behavior under two REDs unconfirmed.** With both 600 and 5692 RED, the roll-up reports the worse (600, 14.7%). Nobody confirmed the recon TEXT builder *names both* — if it surfaces only the worst, the 5692 RED is invisible in the brief even while blocking B.75.

---

## 6. NEXT-SESSION ORDERED PUNCHLIST (after freeze lifts)

Gate order **B.5 → B.75 → C → D**, with the highest-value Obj-4 fix (per-position qty reconciliation that gates GATE 3/4) slotted into **B.75** because it is the mechanism that both clears the RED *and* stops the CRITICAL Action-mutation.

### GATE B.5 — close the observation (mostly DONE)
- **[DESIGN-DONE]** Stability observed (equity $22,135, exposure aligned, health YELLOW-only). Per doc, B.5 is NOT fully closed until B.75 clears.

### GATE B.75 — Holdings Freshness (the blocker; must clear BOTH REDs)
1. **[NEEDS-BUILD] Verify the 5692 Joint RED first** — pull a fresh 5692 broker screenshot, classify the $230/4.6% gap (Stale Price vs Pending/Settling). *This is the open Unknown from §1; do it before coding anything.*
2. **[NEEDS-BUILD] Add `Broker_Qty` per-position control** (constant array `BROKER_POSITIONS_` or `Broker_Positions` tab) + a documented seeding ritual (owner, cadence, format). Without it, DIVERGENT detection cannot run.
3. **[DESIGN-DONE → NEEDS-BUILD] `misV2HoldingsFreshness_()`** pure read-only function (Obj 2/3 spec): per `{account,ticker}` compare HOLDINGS_CLEAN qty vs Broker_Qty → DIVERGENT(RED)/STALE(YELLOW)/FRESH(GREEN). Emits two health rows (import-age + qty-divergence). **PROPOSE-not-write** (Telegram + diagnostic tab row; never mutates HOLDINGS_CLEAN).
4. **[DESIGN-DONE → NEEDS-BUILD] HIGHEST-VALUE OBJ-4 FIX — gate `misV2BuildFSE` GATE 3/4 behind qty-reconciliation.** Compute `heldMap` from `min(MIS qty, Broker_Qty)` OR skip GATE 3/4 for any ticker where MIS qty ≠ Broker qty (mark `QTY_UNRECONCILED`, WARNING-only). **This is the only fix that stops stale data MUTATING Action — the single CRITICAL.** Slotted here because it shares the Broker_Qty input with the freshness check.
5. **[NEEDS-BUILD] Fix the partial-fill Gmail query** — `subject:("Order Execution" OR "Order Partially Filled")` (L517) so the ABBV-class partial sale is captured. Verify against a real Fidelity partial-fill email.
6. **[NEEDS-BUILD] Recon dollar-floor** — RED only if `diff% > 3% AND diff$ > $threshold`. Needs Sam's threshold sign-off ($250–$500 per doc vs ~$50 per Obj 1) — kills chronic kid-account false REDs.
7. **[DESIGN-DONE] B.75 acceptance (6-point, Obj 3) + ADD 5692 condition:** ABBV 17→7, **AND** 5692 RED resolved, RED clears on BOTH accounts, recon within tolerance, equity stays ≈$22,135 (must NOT rebound toward $32K), divergence GREEN. Read-only verify after the next EOD Position Summary import — no write endpoint called.

### GATE C — Legacy vs Proposed display (MIGRATION_MODE) — presentation only, separate session
8. **[NEEDS-BUILD] Mandate guard FIRST** — add `mandate` to `misV2ClassifyAccount_` (L2384) from PORTFOLIO_CONTROL_; stop blending 600+898 into "SAM"; ensure 2.5%/mo clock cannot apply to Fidelity (doc L75–77 — guard BEFORE wiring displays).
9. **[DESIGN-DONE → NEEDS-BUILD] Telegram** MIGRATION block (`misV2DailyBrief_`, render-only — synthV1/synthV2/Exposure_ in-process).
10. **[DESIGN-DONE → NEEDS-BUILD] Test Entry** dual-verdict header (`misV2EntryPad_` after ~L941, render-only).
11. **[DESIGN-DONE → NEEDS-BUILD] Scope** — `peekoverlay` (L2890) emits `verdict_legacy`+`proposed_rc`+`exposure_pct`+`conc`+`mandate`; `peek.html` `.hld` renders. *Only surface needing a read-only Apps Script change.*
12. **[NEEDS-BUILD] Suppress margin language on PRESERVE 5692 / KID** in `misV2CashMarginHtml_` (L2368–2378) — roll up by account+mandate, not broker.
13. **[DESIGN-DONE] C acceptance** (doc L69–71): all 3 surfaces show BOTH actions + MIGRATION_MODE; zero action mutation (ASML stays ADD even at 49%); legacy not retired; zero verdict drift.

### GATE D — After-hours dual header — blocked until C passes all 3 surfaces
14. **[DESIGN-DONE → NEEDS-BUILD]** Populate `extended.regular_pct` in EVERY v8-chart branch (`index.ts:727–792`); compute from `prevClose`, not Finnhub `q.pc`; PRE returns null.
15. **[DESIGN-DONE → NEEDS-BUILD]** v7 path additive (`regular_pct ??=`, L804); `peek.html:414` consumes server value, drops client recompute, no +0.00% fallback.
16. **[DESIGN-DONE → NEEDS-BUILD]** Widen dual-header gate to include **CLOSED** when `ext_price != null` (`peek.html:412`) — the most common after-hours viewing window currently falls through.
17. **[DESIGN-DONE]** D acceptance (6-point, Obj 7): regular_pct non-null off v8 alone; denominator consistency; POST/CLOSED/weekend dual header; correct session label; graceful degradation when both Yahoo paths fail.

### Then (still 🔒 BLOCKED — do NOT pull forward)
Absolute-score conversion → M1 P&L widen (SCHG/SCHZ) → EventImpact deprecate → M4/M5 dependency audit → 50→183→250 scale tests (#15A/#15B immutability). Production swap = NO until ALL gates + parallel-run.

---

**Source trail:** Synthesis of 8 analyst streams (read-only) against `MIS/docs/MIS_SACS_CALIBRATION_MODULE.md` (L1–199, primary source for the two-RED correction). Code anchors: `MIS/v2/Code.gs` · `ops-api/src/index.ts` · `outputs/peek.html`. No endpoints fired, no files edited, no clasp/deploy — freeze intact. The one correction that overrides an analyst: **5692 is RED (doc L106), not GREEN (Obj 1)** — B.75 must clear two REDs.

---

## RAW ANALYST SECTIONS (full detail, pre-synthesis)

### Analyst 1. RED/YELLOW recon inventory

I have full trace coverage. One subtle finding worth flagging: the recon math uses GOOGLEFINANCE-priced gross MV for *all* held accounts, but only acct 600 holds equities with a stale share count. Accounts 4073/5378/5692 also hold equities (ASML/META per the exposure section) priced live — so their gross-vs-control diff is bounded only by price-lag, not share-count staleness. Let me verify those accounts' control net vs the >1% YELLOW threshold sensitivity, since small accounts (4073=$1,289, 5378=$523) amplify diff% on any small price drift. A $16 price lag on the daughter's account (523) = 3% = RED. Let me check the exposure doc figures to bound this.

The exposure section confirms ASML/META are held in 600/4073/5378/5692 and were priced consistently in the same 2026-06-04 run that produced the recon result. The doc explicitly recorded the run as: equity $22,135, only RED = acct 600 (ABBV), B PASS @78 with no other flag. That is the authoritative live result.

## MIS v2 Portfolio_Reconciliation — Full Flag Review (READ-ONLY, freeze respected)

**Traced:** `misV2GrossByAccount_` (Code.gs L1988-1994), `misV2PortfolioRecon_` (L1995-2010), control table `PORTFOLIO_CONTROL_` (L1980-1987), recon text/tab builders (L2011-2025), and the consuming portfolio-metrics path (L2157-2158). Did NOT deploy / clasp / hit any write fn. No live token is present in repo (`MIS_V2_TOKEN` is a Worker secret), so `fn=portrecon` could not be re-fired this session; evidence is the canonical 2026-06-04 run banked in `MIS_SACS_CALIBRATION_MODULE.md` (§1, gate table L86-95) cross-checked against the code math.

**How a flag is raised (only mechanism that exists):** per account, `misNet = grossMV(from HOLDINGS_CLEAN col F) + cash + margin(neg)`; `diff = misNet − ctrlNet`; `diffPct = |diff|/|ctrlNet|`; **`>3% → RED`, `>1% → YELLOW`, else GREEN** (L2000-2001). Roll-up = worst single account (L2007-2008). There is exactly ONE flag source — gross-vs-control net divergence. There is no separate margin check, no valuation check, no engine-integrity check: those are *root-cause classes I assign*, not independent code paths. Concentration (`misV2Exposure_` L2031-2046) is a parallel WARNING and is correctly walled off — it never feeds `status` and never mutates Action.

| Account | Status | Diff $ | Diff % | RootCauseClass | Evidence |
|---|---|---|---|---|---|
| **600** Fidelity GROWTH (Invest-n-Save, margin) | **RED** | ≈ **+$2,059** (raw stale = +$2,154 ABBV, less ≈−$95 price-lag) | ≈ **14.7%** (>3%) | **DATA_FRESHNESS** | MIS holds ABBV **17sh ($3,729)** vs broker **7sh ($1,575)** — partial sale settling, HOLDINGS_CLEAN not refreshed. ctrlNet $14,002.04, margin −$11,131.76 correctly netted. Calibration §1 L42-46; gate table L88. NOT margin/valuation/engine. |
| **5692** Fidelity PRESERVE (Joint, no margin) | **GREEN** | ≈ 0 | <1% | — (n/a) | ctrlNet $4,966.56, cash $541.03, margin 0. Holds META/ASML priced live; no share-count staleness → only price-lag, nets within tolerance. B PASS @78 (L87). |
| **4073** Fidelity KID (son) | **GREEN** | ≈ 0 | <1% | — (n/a) | ctrlNet $1,288.86, cash $1.40. Holds ASML (37% exposure) live-priced; within tolerance in the 06-04 run. |
| **5378** Fidelity KID (daughter) | **GREEN** | ≈ 0 | <1% | — (n/a) | ctrlNet $523.29, cash $0.06. Holds META (68% exposure) live-priced; within tolerance in the 06-04 run. |
| **898** Schwab ACTIVE (cash, ready) | **GREEN** | 0 | 0% | — (n/a) | ctrlNet = cash = $1,354.70 (Plaid-live, no held equities → gross 0, exact match by construction). |

**Roll-up recon status = RED**, driven solely by account 600.

**Verdict on the hypothesis — CONFIRMED:** the only current RED is ABBV-stale on acct 600, and its class is **DATA_FRESHNESS**, not VALUATION / MARGIN / RECON_ENGINE. The margin layer is working (−$11,131.76 nets 600 from a ~$25K gross to $14,002 control, which is exactly the $32K→$22K correction). The recon engine math is sound. Fix is a holdings re-import (ABBV 17→7), not code — this is precisely gate **B.75**.

**Second-issue flag (latent, not currently tripped — worth banking before B.75):** the recon thresholds are **fixed percentages with no dollar floor**, and the two kid accounts are tiny (5378 = $523, 4073 = $1,289). On those balances a 3% RED trips at just **~$16 / ~$39** of MIS-vs-control drift. Since 4073/5378 hold live GOOGLEFINANCE-priced ASML/META, a routine ~15-min price-lag on a single high-priced share (ASML ≈ $1,757) can exceed 3% of a $523 account purely from price timing — a **VALUATION-class false RED** with no data-freshness or margin cause. It did not fire in the 06-04 snapshot (both GREEN), but the design will surface spurious REDs on the kid accounts during volatile sessions. Recommendation (post-freeze, not now): add an absolute-dollar floor (e.g. ignore diff < ~$50, or scale tolerance to account size) to `misV2PortfolioRecon_` L2001 so micro-accounts don't trip RED on normal price lag. No other account or flag class is implicated.

### Analyst 2. Broker-email watcher design

I have everything needed. Writing the design spec.

## Broker Trade-Confirmation Watcher — Design Spec (Holdings Freshness, Objective 3)

### 0. What already exists (do NOT rebuild — extend)
Two parsers already live in `MIS/v2/Code.gs` and are the foundation; the watcher *wires them together*, it does not start from zero:
- **`misV2ImportTrades_` (line 512)** — already parses BOTH brokers' execution emails into `TRADE_LOG`, deduped by `Order_Id`. Fidelity query `from:Fidelity.Alerts@fidelity.com subject:"Order Execution" newer_than:7d`; Schwab query `from:donotreply@mail.schwab.com subject:"trade was executed" newer_than:7d`. Daily 5:30 PM trigger (`setupTradesTrigger`).
- **`misV2ImportHoldings_` (line 1205)** — parses Fidelity "Position Summary" emails into `HOLDINGS_CLEAN` via `misV2WriteHoldings_` (line 1107, clears+rewrites = idempotent). Daily 6:10 PM trigger.

**The ABBV gap root cause is architectural, not missing code:** the fill watcher (`misV2ImportTrades_` → `TRADE_LOG`) and the holdings snapshot (`misV2ImportHoldings_` → `HOLDINGS_CLEAN`) are **decoupled**. A SELL fill lands in `TRADE_LOG` instantly, but `HOLDINGS_CLEAN` only changes when the next Position Summary email arrives AND settles (T+1). Between the fill and the next clean summary, `HOLDINGS_CLEAN` is stale (ABBV 17 vs broker 7) and nothing flags it. **The watcher's job is to make `TRADE_LOG` deltas drive a freshness flag on `HOLDINGS_CLEAN`.**

### 1. Inbox
**Business `sam@hookstreetcapital.com` ONLY.** Verified (memory `reference_broker_trade_emails`, 2026-05-27 dual-account scan): all broker mail lands in business INBOX; personal `ztreitel@gmail.com` had ZERO broker mail. The watcher runs inside the MIS Apps Script project bound to the business account (`GmailApp` = that mailbox). Do not add a personal-Gmail path — it's dead weight and a false-positive surface.

### 2. Gmail search queries (per broker, per class)
Keep the existing fill queries; recommend label-targeting to kill the false-positive risk Sam flagged (Fidelity "Daily Alert Summary", muni blasts, Markit price alerts):
- **Fidelity fills:** `from:Fidelity.Alerts@fidelity.com subject:("Order Execution" OR "Order Partially Filled") newer_than:7d` — the `Partially Filled` arm is the exact ABBV case (17→7 partial sale) and is currently MISSED by line 517's `subject:"Order Execution"`.
- **Schwab fills:** `from:donotreply@mail.schwab.com subject:"trade was executed" newer_than:7d` (unchanged).
- **Holdings (truth-reset):** `from:Fidelity.Alerts@fidelity.com subject:"Position Summary" newer_than:3d` + Schwab `subject:("My Closing Summary" OR "My Weekly Summary")`.
- **Noise filter:** add a Gmail filter → label `Trades`/`Positions` so the parser keys on label, not sender alone.

### 3. Fields to extract (per fill)
Already mostly parsed in lines 520–534; the watcher needs all six as the dedupe + delta key:
| Field | Fidelity regex source | Schwab |
|---|---|---|
| **account** (last4) | `/Account\s*\*+(\d{3,5})/` | `/ending in\s+(\d{3,5})/` |
| **side** | `/Your order to\s+(buy\|sell)/` | `/(bought\|sold)/` |
| **qty** | `/(\d+\.?\d*)\s+shares of/` | same | 
| **ticker** | `/shares of\s+([A-Z][A-Z0-9.]{0,5})/` | same |
| **price** | `/Filled.*@\s+\$([\d,.]+)/` | `/at\s+\$([\d,.]+)/` |
| **confirmation # / Order_Id** | `/Order number\s+([A-Z0-9]+)/` | `SCHWAB_<msgId>` |
| **date/time** | `m.getDate()` | `m.getDate()` |

### 4. Update model — PROPOSE, never auto-write to HOLDINGS_CLEAN
**Hard constraint:** MIS is advisory/never-autonomous, system is under deployment freeze, and Portfolio Layer never mutates Action. So the watcher must NOT silently rewrite `HOLDINGS_CLEAN` from a fill (a fill ≠ settled position; partial fills, multi-leg, and corrections would corrupt the snapshot). Instead:

1. **Fills land in `TRADE_LOG` as the authoritative event stream** (already happens, deduped by `Order_Id`).
2. **A new pure function `misV2HoldingsFreshness_()` (read-only) computes the implied position** per `{account, ticker}` = `HOLDINGS_CLEAN qty` adjusted by net `TRADE_LOG` fills dated AFTER the holdings `AsOf` stamp (col J "email-EOD <date>" / Source). If implied ≠ stored → **STALE flag**, not a write.
3. **Output is a PROPOSE-an-edit**: a Telegram push via the working `/mis/notify` relay + a `Holdings_Freshness` diagnostic tab row: *"ABBV acct 600: HOLDINGS_CLEAN=17sh, TRADE_LOG shows SELL 10sh @ $X on 6/4 (order #…) → implied 7sh. Holdings STALE. Run holdings re-import / confirm."* Sam (or the next refresh) closes it. The authoritative reset stays `misV2ImportHoldings_` reading the settled Position Summary — the watcher just makes the staleness *visible immediately* instead of T+1-invisible.

### 5. Idempotency
- **Fill dedupe:** by `Order_Id` (Fidelity `Order number`; Schwab synthesizes `SCHWAB_<msgId>`) — existing `existing[oid]` guard, line 515/521/531. Keep.
- **Holdings dedupe:** `misV2WriteHoldings_` clears+rewrites (line 1110) and `misV2ImportHoldings_` keeps newest email per account (line 1211) — re-running is safe.
- **Freshness check is pure/read-only** — re-runnable infinitely, no state mutation, so it can run on every brief assembly without side effects.

### 6. Failure modes to guard
1. **Partial fills** — `subject:"Order Execution"` (line 517) MISSES "Order Partially Filled"; the literal ABBV case. **Fix the query.**
2. **Settlement lag double-counting** — if the watcher both wrote holdings AND the Position Summary later wrote them, you'd double-apply. Avoided by design: watcher never writes; only the settled summary writes.
3. **AsOf comparison bug** — `HOLDINGS_CLEAN` stores freshness as text in col K (`'email-EOD '+asOf`), not a clean date. The freshness function must parse that string; if it can't, default to STALE (fail-safe, never fail-fresh).
4. **Schwab Order_Id instability** — Schwab has no real order number, so `SCHWAB_<msgId>` re-dedupes per message; a re-sent alert = new id = double fill. Mitigate: dedupe Schwab on `{account|ticker|side|qty|price|date}` hash, not msgId.
5. **Ticker regex over-match on muni/news blasts** — key on label `Trades` + execution subject, not sender alone.
6. **Recon coupling** — a detected STALE must surface in `misV2PortfolioRecon_` (line 1995) as the RED *reason* ("data-freshness, not margin/math"), so the RED flag self-explains instead of looking like an engine bug.

### 7. How it feeds the Holdings Freshness check (Objective 3 / gate B.75)
`misV2HoldingsFreshness_()` returns `{ticker, account, storedQty, impliedQty, stale:bool, sinceFillDate}`. Three consumers:
- **Recon (`misV2PortfolioReconText_`, line 2011):** appends `HOLDINGS STALE: ABBV 600 (17→7 implied by 6/4 SELL)` to the RED block so the RED root-causes itself.
- **Per-ticker peek/Telegram brief:** adds a `⚠ POSITION MAY BE STALE — TRADE_LOG shows a fill since last holdings snapshot` line near the existing DATA FRESHNESS block (line 875–881).
- **Gate B.75 close condition:** the watcher's STALE flag is what *clears* when ABBV updates 17→7 on the next settled summary — i.e., B.75 ("ABBV updates 17→7, RED clears") is literally this function returning `stale:false` for ABBV after re-import.

### 8. Acceptance test (5 items — all read-only, freeze-safe)
1. **Partial-fill capture:** a Fidelity "Order Partially Filled" SELL of ABBV (10 of 17) lands a single `TRADE_LOG` row with correct side=SELL, qty=10, ticker=ABBV, price, order#; re-running the import adds ZERO duplicate rows (dedupe by Order_Id).
2. **Stale detection without write:** with `HOLDINGS_CLEAN` ABBV=17 and a post-AsOf SELL-10 in `TRADE_LOG`, `misV2HoldingsFreshness_()` returns `{ABBV, 600, storedQty:17, impliedQty:7, stale:true}` and `HOLDINGS_CLEAN` ABBV row is **byte-for-byte unchanged** (proves PROPOSE-not-write).
3. **Recon self-explains:** `misV2PortfolioReconText_` RED block names the ABBV freshness gap as the reason, not margin/valuation; the $2,059 recon gap maps to the 10-share ABBV delta.
4. **Idempotent re-run:** running the watcher + freshness check 3× back-to-back produces identical `TRADE_LOG` (no dup rows) and identical flag output; no `HOLDINGS_CLEAN` mutation.
5. **Clears on settle (B.75):** after a settled Position Summary updates ABBV 17→7 via `misV2ImportHoldings_`, `misV2HoldingsFreshness_()` returns `stale:false` for ABBV, recon RED clears, equity stays ≈$22,135 — gate B.75 passes.

**Key files:** `C:\Users\ztrei\OneDrive\2. Hook Street\05. 2026 BH\MIS\v2\Code.gs` (`misV2ImportTrades_` L512, `misV2ImportHoldings_` L1205, `misV2WriteHoldings_` L1107, `misV2PortfolioRecon_` L1995, DATA FRESHNESS block L875). Account/inbox truth: `C:\Users\ztrei\.claude\projects\C--Users-ztrei-OneDrive-2--Hook-Street-05--2026-BH\memory\reference_broker_trade_emails.md`. Spec/queue: `C:\Users\ztrei\OneDrive\2. Hook Street\05. 2026 BH\MIS\docs\MIS_SACS_CALIBRATION_MODULE.md` (gate B.75, L90).

### Analyst 3. Holdings Freshness health check

I have what I need. Key facts confirmed:
- `misV2WriteHoldings_` stamps `Data_Quality` (col J, index 9) as `'email-EOD '+asOf` and `Source` (col K, index 10) — but `asOf` is **date-only** (yyyy-MM-dd), no clock time. The true import wall-clock lives only in `RUN_LOG` via `log_` (`misV2WriteHoldings_` / `misV2ImportHoldings_` rows).
- HOLDINGS_CLEAN actual columns differ from the task's stated map: A(0)=Ticker, B(1)=Account, C(2)=Shares, D(3)=Avg_Price, E(4)=Current_Price, F(5)=Market_Value. (Task said C=Qty, F=MktVal — matches; qty IS col C.)
- No last-known-broker-qty store exists. The recon control array `PORTFOLIO_CONTROL_` holds net equity per account but NOT per-ticker broker share counts. So MIS-qty-vs-broker-qty (ABBV 17-vs-7) divergence has **no current data source** in code — it must be specified as a new input.
- `misV2PortfolioRecon_` already produces `reconStatus` RED/YELLOW/GREEN and the "RECON RED blocks portfolio-level language" rule is live in `misV2PortfolioReconText_`.

Now I have everything to write the spec.

## Holdings Freshness — first-class `System_Health` sub-check (fn=health)

### Where it plugs in
Add as a new `row(...)` series inside `misV2SystemHealth_()` (Code.gs L1924–1955), emitted right after `Action layer (shadow)` (L1950) and before `Triggers` (L1951–1952). It uses the same `row(name,status,detail)` helper so it rolls into the existing `reds`/`yel` → `roll` aggregate (L1953). Status vocabulary maps onto the existing health colors: **FRESH→GREEN, STALE→YELLOW, DIVERGENT→RED**. A DIVERGENT result therefore turns the whole `SYSTEM_HEALTH:` banner RED, which is the lever that gates downstream gates.

### Inputs (all already present except #4)
1. **Last holdings-import wall-clock** — read the newest `RUN_LOG` (L268/L374) row where `Function ∈ {misV2WriteHoldings_, misV2ImportHoldings_}` and `Status='OK'`. Column A = `Timestamp` (real `new Date()`, has clock time). This is the only place a true import time exists — `HOLDINGS_CLEAN.Data_Quality` (col J) only carries `email-EOD <yyyy-MM-dd>` with **no clock component**, so RUN_LOG is authoritative for staleness minutes/hours.
2. **Per-account as-of date** — parse the `asOf` date out of `HOLDINGS_CLEAN!J` (`Data_Quality`, format `email-EOD YYYY-MM-DD`) grouped by `HOLDINGS_CLEAN!B` (Account, digits-only via the same `replace(/[^0-9]/g,'')` normalization `misV2GrossByAccount_` uses, L1991). Gives a per-account data-as-of even when one account's Position Summary email didn't arrive.
3. **Settlement-lag horizon** — `now − asOf` in NY trading days. T+1 is normal (the import comment at L1202–1204 already documents the T+1 match: a fill today appears in the NEXT evening's summary). >1 trading day with no refresh = settlement lag suspected.
4. **MIS-qty vs last-known-broker-qty** (the ABBV case) — *new input, no current source.* Add a `Broker_Qty` control column. Cleanest: extend `PORTFOLIO_CONTROL_` (L1980–1987) is wrong shape (account-level, not per-ticker), so introduce a sibling per-ticker control — either a small `BROKER_POSITIONS_` constant array `[acct,ticker,brokerQty,asOf,source]` or a `Broker_Positions` control tab read like `Position_Costs`. Compare against `HOLDINGS_CLEAN!C` (Shares) joined on (Account, Ticker). ABBV: MIS C=17 vs broker=7 → divergence of 10 sh.

### Logic / statuses (per-position then rolled to per-account then to the check)

```
For each (account, ticker) in HOLDINGS_CLEAN:
  ageMin   = (now − RUN_LOG.lastImportTimestamp) in minutes        // input 1, global
  asOfDays = (now − parse(Data_Quality asOf)) in NY trading days   // input 2/3, per-account
  qtyDiff  = HOLDINGS_CLEAN.Shares − Broker_Qty(account,ticker)    // input 4, per-position

  DIVERGENT  if Broker_Qty exists AND qtyDiff != 0                 // ABBV 17 vs 7  → RED
  STALE      if asOfDays > 1 trading day  (or ageMin > 26h cron-miss guard) → YELLOW
  FRESH      otherwise                                              // T+1 within tolerance → GREEN
```

- **Two emitted rows** so the operator sees both axes:
  - `row('Holdings import age', ageMin<26h?'GREEN':'YELLOW', '<acct as-of list>, last import <H>h ago')`
  - `row('Holdings qty divergence', anyDivergent?'RED':'GREEN', divergent.map(t→ t+' MIS '+misQty+' vs broker '+brokerQty).join(', ') || 'none')`
- Thresholds are deliberate: settlement lag alone (T+1, `asOfDays<=1`) stays FRESH — it must NOT block, because the system runs on T+1 EOD imports by design (L1202). Only **explicit qty divergence vs a known broker count** is DIVERGENT/RED; pure age only ever reaches YELLOW.

### What it surfaces / what it BLOCKS
- **Surfaces** in `fn=health` text and (if `misV2BuildIntegrity_`/`Integrity_Check` tab at L2105 is extended) as a tab row.
- **Blocks** via the *existing* portfolio-level gate, not a new one. A DIVERGENT (RED) Holdings Freshness must drive the same suppression already wired for recon RED in `misV2PortfolioReconText_` (L2014: *"RECON RED — portfolio-level numbers blocked until reconciled (security reads OK)"*). Concretely: when Holdings Freshness = DIVERGENT, portfolio-level language (NET equity, Weight%, per-account exposure from `misV2Exposure_` L2031, mandate sizing) is suppressed/badged STALE; **security-level (engine) reads continue** — exactly consistent with the two-layer lock and the "no fresh data = no trade recommendation" rule. It never mutates Action (Portfolio Layer never touches the engine verdict).

### Tie to recon RED (root-cause linkage)
This check is the **diagnostic upstream of the RED recon flag**. Per locked context, the current recon RED is *data freshness*, not math: ABBV shows 17 sh in MIS vs 7 at the broker (partial sale settling). `misV2PortfolioRecon_` (L1995–2009) computes `grossMV` from `HOLDINGS_CLEAN` (via `misV2GrossByAccount_`, L1988) — the extra 10 ABBV shares inflate that account's `misNet`, pushing `diffPct>0.03` → RED. So Holdings Freshness = DIVERGENT and recon = RED share **one root cause**. The Holdings Freshness row is what names it ("ABBV MIS 17 vs broker 7 — partial sale settling") so the RED isn't misread as a margin/engine bug. When the next import lands ABBV=7, qtyDiff→0 → FRESH, `grossMV` drops by 10×price, `misNet`→`ctrlNet` within tolerance → recon GREEN.

### Exact B.75 acceptance test (Holdings Freshness gate)
Run **read-only** after the next EOD Fidelity Position Summary import (no manual write):

1. **Pre-state (today, RED):** `fn=health` shows `Holdings qty divergence | RED | ABBV MIS 17 vs broker 7`; `fn=portrecon` shows `reconStatus=RED` with account 600's `diffPct>3%`; portfolio-level language is suppressed/STALE-badged on peek; `fn=state` baseline NET equity ≈ $22,135 (real, margin-netted).
2. **Trigger the refresh by observation only** (gate B.5): wait for the daily ~6:10 PM `misV2ImportHoldings_` run (trigger set L1223–1228) to ingest the new Position Summary. Do **not** call any write endpoint.
3. **ABBV updates 17 → 7:** `fn=holdings` shows ABBV `Shares`=7 (col C) for account 600; `HOLDINGS_CLEAN!J` as-of advances to the new EOD date; a fresh `misV2WriteHoldings_` OK row appears in RUN_LOG with current clock time.
4. **Divergence clears:** `fn=health` `Holdings qty divergence | GREEN | none`; `Holdings import age` GREEN (`<26h`); overall `SYSTEM_HEALTH:` banner no longer RED on account of holdings.
5. **RED clears, recon within tolerance:** `fn=portrecon` → `reconStatus=GREEN`; account 600 `diffPct ≤ 1%` (YELLOW band at most, not RED); the `⚠ RECON RED …` suppression line is gone; portfolio-level language un-blocks.
6. **Equity stays ≈ $22K (no inflation):** `fn=state` / `fn=exposure` total NET equity holds at **≈ $22,135** (the −$11,131.76 acct-600 margin stays netted via `PORTFOLIO_CONTROL_` L1982; equity must NOT jump back toward ~$32K). The only change vs pre-state is account 600 gross dropping by 10×ABBV price as the phantom shares clear — net equity is governed by `ctrlNet`, so the headline ≈$22K is unchanged.

**Pass = all six.** Any of: equity rebounds toward $32K, recon stays RED after ABBV=7, or `Holdings qty divergence` still RED → fail, stay on B.75; do not advance to Gate C (Legacy vs Proposed / MIGRATION_MODE).

### Build note (analysis-only flag)
The first three inputs are wired today (RUN_LOG, `Data_Quality` asOf, trading-day math). **Input 4 (`Broker_Qty`) has no source in `Code.gs`** — `PORTFOLIO_CONTROL_` is account-level only; there is no per-ticker broker share store. Shipping this check requires adding a per-position broker-qty control (constant array or `Broker_Positions` tab) seeded from the same Fidelity screenshot/CSV that seeds `PORTFOLIO_CONTROL_`. Without it, the check can still emit `Holdings import age` (FRESH/STALE) but cannot compute DIVERGENT — so the ABBV 17-vs-7 detection is gated on that one new input. (No edits made — deployment freeze respected.)

Relevant files: `C:\Users\ztrei\OneDrive\2. Hook Street\05. 2026 BH\MIS\v2\Code.gs` (health L1924-1956; holdings write L1104-1118; import L1199-1228; recon L1975-2026; exposure L2031; log_/RUN_LOG L268,L374).

### Analyst 4. Stale-holdings surface sweep (HIGHEST ROI)

The URL/token are stored as Worker secrets (not in repo, correctly). I won't have the token to call the live web app directly, and the public `/mis/peek` only does single-ticker overlay (not full holdings/exposure). I have enough from the code + the import snapshot to deliver the analysis. Let me verify the one remaining stale surface — the `misV2GetCostBasisMap_` and whether `LOW` (recent activity) appears. I have the full picture. Producing the analysis.

## MIS v2 Holdings-Staleness Audit — How Many ABBV-Style Fake Readings Are Possible

### Root mechanic (confirmed)
Every portfolio/concentration/exposure number in `Code.gs` is derived from **one tab, `HOLDINGS_CLEAN`, columns A(Ticker)/B(Account)/C(Qty)/F(MktVal)** — and that tab is only ever as fresh as the last **Fidelity "Position Summary" email** parsed by `misV2ImportHoldings_` (line 1205). Qty (col C) is written once from the email and **never re-derived**; `misV2RepriceHoldings_` (1157) only overwrites **price (col E) and MktVal (col F) = px × stale-qty**. So a stale share count silently propagates into MktVal on the *next* reprice, making the bad number look freshly computed. This is exactly the ABBV failure (MIS 17sh vs broker 7sh): qty never updated, reprice multiplied live price by the wrong 17.

A second, worse vector exists: the `_full_reconcile_2026-06-03.json` import path (`_import_full_2026-06-03.py`) carries **whole-transaction-history quantities** — ABBV=17 there (line 352-355) — which is the *gross historical* position, not the settled broker position. Any re-import from that JSON reintroduces the 17.

---

### (a) Ranked list of stale-failure SURFACES

| # | Surface (fn / function) | Line | Reads | Stale-qty effect | Severity |
|---|---|---|---|---|---|
| 1 | **`misV2BuildFSE` GATE 3 & GATE 4** | 304-333 | `heldMap` qty + mktValue | **MUTATES ACTION.** GATE 4 forces `WATCH / PORTFOLIO_OVERLAP` when `mktValue/portTotal ≥ 20%`; GATE 3 forces `REDUCE / HELD_LOSS`. A stale-high qty inflates mktValue → fake overlap → **engine verdict changed by stale data.** This violates the locked "Portfolio Layer NEVER mutates Action" rule. | **CRITICAL** |
| 2 | **`misV2Exposure_`** (fn=exposure) | 2031-2046 | qty→mv per acct ÷ recon net | `⚠CONC` at >25% computed from stale MktVal over control-net. ABBV-style: 17sh × price / net = fake concentration flag. | HIGH |
| 3 | **`misV2GrossByAccount_`** (feeds recon) | 1988-1993 | col F sum per account | Stale qty inflates gross → inflates `misNet` in recon → **can flip recon GREEN/YELLOW/RED** (the very RED-flag mechanism). | HIGH |
| 4 | **`misV2PortfolioMetrics_`** (fn=state) | 2141-2158 | qty→mv, sector concentration | `topSectorPct`, `bookRiskPct`, cash% all off stale MktVal; concWarn fires at 40%. Note: total/equity now correctly read recon net (line 2157), but **sector mix still off stale per-ticker MV.** | HIGH |
| 5 | **`peekoverlay`** (public `/mis/peek`) | 2895-2903 | qty, mkt_value, per-acct | Scope page shows stale qty + fake P&L% on the exact card Sam taps before a BUY. | HIGH |
| 6 | **`misV2BrokerAllocationHtml_`** | 2355-2379 | qty→mv by broker | Broker equity %-of-book distorted. | MED |
| 7 | **`misV2PositionsHtml_`** / fn=positions | 2524-2578 / 2918 | qty, avg, mv | P&L $ and % per position off stale qty (mv stale, cost = stale-qty × avg). | MED |
| 8 | **`misV2PerformanceHtml_`** | 2445-2454 | qty, mv, cost | "Am I making money" number off stale qty. | MED |
| 9 | **Risk stack** — `misV2MonteCarloVaR_` 1497, `misV2PortRisk_` 1525, `misV2StressTest_` 1562, `misV2HoldingsRiskHtml_` 2637 | — | weights from col F | VaR/stress weights mis-set; a stale-large position overweights the loss sim. | MED |
| 10 | **`misV2GapPulse_` STOP-HIT + held-thesis** | 557-563 | held set (qty>0) | A *fully sold* name still flagged held → phantom "Why holding X?" / phantom STOP-HIT pulse. | LOW |
| 11 | **`misV2ProposedState_`** (shadow) | 1871-1872 | held boolean | Shadow synthesis treats sold names as held (shadow only, no prod change). | LOW |
| 12 | **`misV2RefreshNews_`** | 2178 | held set | News pulled for names no longer owned. | LOW |

**Freshness gap the system has no guard for:** `misV2WriteHoldings_` stamps `Data_Quality = 'email-EOD <asOf>'` (line 1111) but **nothing downstream reads that date to decide staleness.** GATE 1 `DATA_STALE` (line 327) checks the *Snapshot price* `dq`, **not the HOLDINGS_CLEAN asOf** — so an old holdings load with live prices passes every freshness gate. There is no qty-vs-broker reconciliation anywhere; recon (`misV2PortfolioRecon_`) reconciles **account net $**, never **per-position share count**, which is why ABBV slipped through as a "DATA FRESHNESS" RED instead of being caught at the qty level.

---

### (b) Per-position fake-exposure RISK table

Settlement-lag-prone = recent partial sale/buy where T+1 broker email lags the fill. Cross-referencing realized-P&L churn (names with both large realized AND a residual holding = active trimming) and odd fractional/whole残 quantities in the 6/3 snapshot:

| Ticker | Account | Stale-risk | How a stale qty would distort |
|---|---|---|---|
| **ABBV** | Fidelity 600/5692 | **H** (live failure) | 17 vs 7 settled → fake +$X MktVal → inflates Healthcare sector%, account exposure, recon net. Root case. |
| **PLTR** (116sh, +$4,847 realized) | Fidelity 600 | **H** | Heavy trimming history; a partial sale not yet in the email keeps 116 high → biggest single fake-CONC candidate (large $ position). |
| **NVDA** (65sh, +$1,909 realized) + **NVDL** (86sh, +$181) + **SOXL** (44sh) | Fidelity 600 | **H** | Three correlated semis actively traded; stale qty on any inflates semis concentration → could trip the 40% sector concWarn falsely. |
| **TSM** (70sh, basis $14.7K) | Fidelity 600 | **H** | Largest basis line; a stale qty here moves account-net recon materially (drives RED/GREEN flip). |
| **META** (11.696sh) / **GOOG** (14.081sh) / **AMZN** (8.19sh) | Fidelity 600 | **M** | Fractional shares = DRIP/partial activity; small drift but feeds Comm-Services/Cons-Disc sector mix. |
| **LOW / TGT / SQ / ZM / COF / HSY / EQIX** (in 6/3 JSON, may not be in live HOLDINGS_CLEAN) | Fidelity | **M** | Names present in reconcile JSON but possibly exited — if re-imported, resurrect phantom holdings (surface #10/#11). |
| **AAPL** (1.338sh) / **LLY** (0.781) / **SLV** (1) | Fidelity | **L** | Tiny residual lots from near-full exits; low $ impact but classic "should be 0, shows fraction" stale. |
| **UVXY (200) / TQQQ (4) / USD (22) / DAL (76) / JBLU (500)** | Fidelity | **L–M** | Volatile/leveraged trade names with big realized churn; stale qty plausible but lower MV. |
| **SGOV / SCHZ / SCHG / SPY** | Fidelity 600/5692 | **L** | Cash-like / excluded from equity gates (lines 305, 2146, 2364) — distort cash% only. |
| **Schwab 898/1241/4436** | Schwab | **L** | Plaid-live (auto-fresh) + `misV2DropSchwab_` already cleared sold-out Schwab rows (line 1177). Lowest staleness risk — the only book that is *not* email-lagged. |

The asymmetry worth naming: **Sam's only ACTIVE/Sam-direct book (Schwab) is Plaid-fresh and low-risk; every HIGH-risk fake-exposure name lives in Fidelity 600 (GROWTH, the margined account), which is CSV/screenshot-only and email-lagged.** Fake concentration will therefore concentrate exactly where the margin already overstated equity.

---

### (c) The single highest-value fix

**Add a per-position quantity reconciliation that runs at the same layer as `misV2PortfolioRecon_`, and gate GATES 3 & 4 of `misV2BuildFSE` behind it.**

Concretely (post-freeze): extend the control layer to hold a **broker-truth share count per ticker per account** (from the same Fidelity screenshot / Schwab Plaid that already seeds `PORTFOLIO_CONTROL_`), then in `misV2BuildFSE` compute `heldMap` qty/mktValue from **min(MIS qty, control qty)** or skip GATE 3/GATE 4 entirely for any ticker whose MIS-qty ≠ control-qty (mark it `QTY_UNRECONCILED`, WARNING-only). This does three things at once:

1. **Stops stale qty from mutating Action** — restores the locked Engine/Portfolio separation that GATE 4 currently violates (surface #1, the only CRITICAL).
2. **Catches every ABBV before it propagates** — a single 17≠7 mismatch surfaces as a per-position recon WARNING instead of silently inflating exposure/concentration/recon-net across surfaces #2–#9.
3. **Makes the RED recon flag specific** — instead of "account net off by >3%, cause unknown," the flag names *the ticker and the share delta*, which is what the current RED on ABBV actually was but the system couldn't say.

Lower-cost interim (no schema change): have `misV2RepriceHoldings_`/`misV2Exposure_` read the `Data_Quality` asOf stamp already written at line 1111 and **degrade any portfolio-level language to WARNING when the newest HOLDINGS_CLEAN asOf is older than the latest Plaid/Schwab timestamp** — closes the "old load + live prices passes all freshness gates" hole that let ABBV look current.

Files: `MIS/v2/Code.gs` — `misV2BuildFSE` (lines 304-333), `PORTFOLIO_CONTROL_` (1980-1987), `misV2PortfolioRecon_` (1995-2010), `misV2Exposure_` (2031-2046). No edits made (analysis-only, freeze respected).

### Analyst 5. Mandate-ignored audit

This is the definitive evidence. peek.html line 352 DOES try to render `u.accounts[]` (per-account name/qty/mv) — but it only fires when `accounts.length>1`, shows NO mandate label, and critically, ops-api line 829's `universe` type **omits `accounts` and `last_decisions`** from the typed spread — so although `{...universe, ...mj.result}` (line 833) would copy them at runtime via spread, the single FSE `verdict` (one active-trading ADD/WATCH/REJECTED) is shown as "YOUR POSITION" verdict across all accounts including PRESERVE/KID. The single verdict applies the ACTIVE-book trade gate to PRESERVE/KID holdings.

I have all the evidence needed. Here is the analysis.

## Account-Mandate Violations in MIS v2 Code.gs — Where Mandates Are Ignored or Blended

**Ground truth (from `MIS_SACS_CALIBRATION_MODULE.md` §4, L73-77 + L290-292):** the 2.5%/mo extraction clock and active-trading gates are **Schwab-ACTIVE-only** (898/1241/4436). Fidelity 600=GROWTH, 5692=PRESERVE (no-margin, Israel savings), 4073/5378=KID are measured by total-return/hold — *not* the 2.5%/mo clock, *not* the active-trade ADD/REDUCE/WATCH gates. The recon layer (`PORTFOLIO_CONTROL_`, L1980-1986) is the **only** place mandate is correctly carried. Every consumer downstream drops it.

| Location / fn | Issue | Correct-by-mandate behavior |
|---|---|---|
| `misV2BuildFSE_` GATE 3 `HELD_LOSS` (L331) & GATE 4 `PORTFOLIO_OVERLAP` (L333) | `heldMap`/`portTotal` built L304-311 with **zero account filter** — a position held in Joint PRESERVE (5692), kids (4073/5378), or GROWTH (600) flips a Schwab-ACTIVE ticker to REDUCE or WATCH. The engine verdict (meant for the ACTIVE book) is polluted by preserve/kid/growth holdings. | FSE held-overlap gates should compute `heldMap`/`portTotal` from **ACTIVE accounts only** (898/1241/4436), or be account-scoped. Holdings in PRESERVE/KID/GROWTH must not drive an active-trade REDUCE/WATCH. |
| `misV2HoldingsRiskHtml_` (L2637-2649) | "YOUR HOLDINGS — with FSE stop/target/R:R" merges every account into one row per ticker (`hd.filter…map`, no account split) and stamps the single active-trade verdict + Stop/Target/R:R on it. A PRESERVE/KID holding gets an ACTIVE-book stop/target/R:R verdict. No mandate column. | Split rows by account or at minimum tag each with mandate; suppress Stop/Target/R:R + ADD/REDUCE verdict for PRESERVE/KID/GROWTH (those are hold/total-return, not bracket-traded). |
| `misV2PerformanceHtml_` views (L2461-2468, 2500-2503) | Slices the book by **owner** (SAM/JOINT/SON/DAUGHTER/SCHWAB), not by **mandate**. The "SAM" tile blends Schwab-ACTIVE (898) **and** Fidelity GROWTH (600, margined) into one P&L — the exact blend the spec forbids. No ACTIVE/GROWTH/PRESERVE/KID tiles. | Add mandate-based slices (ACTIVE / GROWTH / PRESERVE / KID) so the ACTIVE 2.5%/mo book is measurable on its own; stop merging 600 into "SAM" with 898. |
| `fn=state` portfolio block (L2871-2878) | Emits ONE blended "PORTFOLIO: $X MV vs $Y cost" and ONE "ENGINE NOW" verdict count across **all** accounts — no mandate decomposition. This is the number the bot/STATE surfaces read; it can't serve three mandates (spec L290-292). | Decompose state by mandate; the ACTIVE book P&L is the one the 2.5%/mo clock measures, separate from GROWTH/PRESERVE/KID. |
| `misV2CashMarginHtml_` (L2354-2379) | Aggregates Cash/Margin **by broker**, so all Fidelity accounts collapse into one "Fidelity" row. Margin-avail/used columns + the "borrow against yourself rainy-day fund" hint (L2375) render for the **whole Fidelity bucket**, attaching margin language to the no-margin Joint PRESERVE 5692 (Israel-trip savings). | Roll up by **account+mandate**, not broker. Margin columns/hint must appear only on margin-enabled GROWTH 600; explicitly suppress margin on PRESERVE 5692 and KID 4073/5378. |
| `misV2ClassifyAccount_` (L2384-2397) | Returns only `{owner,name,broker}` — **no mandate field at all**. Every display that classifies (cash/margin, performance) is mandate-blind by construction. Also maps 720600 GROWTH and 898 ACTIVE both to `owner:'SAM'`, enabling the blend above. | Add `mandate` to the return (ACTIVE/GROWTH/PRESERVE/KID), sourced from `PORTFOLIO_CONTROL_`, and have all surfaces read it. |
| `fn=peekoverlay` (L2894-2900) → ops-api L829/833 → peek.html L342/352 | peekoverlay computes per-account `byAcct` but attaches **no mandate**. ops-api's `universe` type (L829) omits `accounts`/`last_decisions`, and the Scope card shows ONE FSE `verdict` as "YOUR POSITION" (peek.html L342) regardless of which account/mandate holds it — applying the ACTIVE trade gate to a PRESERVE/KID holding. peek.html L352 only lists accounts when `length>1` and never labels the mandate. | peekoverlay should return mandate per account; ops-api `universe` type must carry `accounts[].mandate`; Scope should label "held in Joint PRESERVE" and not present an ACTIVE ADD/REDUCE verdict against a preserve/kid lot. |

**Note on the 2.5%/−20%:** neither the 2.5%/mo clock nor the −20% hard stop is implemented in `MIS/v2/Code.gs` — the only active-book threshold present is the R:R≥2.5 RiskGate (`misV2BuildFSE_` GATE 6, L337) and the 20% PORTFOLIO_OVERLAP cap (GATE 4, L333). The 2.5%/mo extraction + −20% drawdown live in the v1 daily-snapshot/email layer (`MIS/src/emailDailySnapshot.js`), outside this file. The calibration spec (L75-77) pre-flags the risk that "the brief/Test-Entry" will apply the 2.5%/mo clock to Fidelity when Gate C wires displays — so this must be guarded **before** Gate C, not after.

### 3 highest-impact fixes
1. **FSE held-overlap account scope (GATE 3/4, L304-333)** — highest impact and a pure-Engine-Layer correctness bug: PRESERVE/KID/GROWTH holdings are currently flipping ACTIVE-book verdicts to REDUCE/WATCH. Scope `heldMap`/`portTotal` to ACTIVE accounts. This is the only one of these that actually **mutates Action**, so it's the LOCKED-architecture violation (Portfolio Layer leaking into Engine Layer).
2. **Add `mandate` to `misV2ClassifyAccount_` (L2384) and stop blending 600+898 into "SAM"** — single root fix that unblocks correct mandate slices in Performance, Cash/Margin, and state at once.
3. **Suppress margin language on PRESERVE 5692 / KID accounts in `misV2CashMarginHtml_` (L2368-2378)** — the no-margin Joint (Israel savings) currently inherits Fidelity's margin-avail/used columns + "borrow against yourself" hint; this is the most concretely wrong user-facing claim and a Gate-C presentation landmine the spec already called out.

Source files read (absolute): `C:\Users\ztrei\OneDrive\2. Hook Street\05. 2026 BH\MIS\v2\Code.gs` · `…\MIS\docs\MIS_SACS_CALIBRATION_MODULE.md` · `…\ops-api\src\index.ts` · `…\outputs\peek.html`. No endpoints called, no files edited (analysis-only, freeze respected).

### Analyst 6. C UI layouts

I have everything needed. The three surfaces are anchored to real code. Writing the deliverable.

## Legacy-vs-Proposed Display Spec — 3 Surfaces (MIGRATION_MODE, presentation-only)

**Ground truth from the code (so the mockups are exact, not invented):**
- **Legacy state** = `synthV1(o)` inside `misV2ProposedState_()` (Code.gs L1875-1881): universe-level, ownership-blind. ASML held + close-but-not-green → `WATCH`.
- **Proposed state** = `synthV2(o, isHeld)` (Code.gs L1885-1900): position-aware (`{ADD,HOLD,EXIT,TRIM}` held; `{ADD,STARTER,WATCH,REJECTED}` unheld). Held ASML with strong-across bands → `ADD`. Both already exist in shadow; this surfaces them — **no new score**.
- **Exposure %** = `misV2Exposure_()` (L2031-2046): `position MV / account ctrlNet`, `⚠CONC` at ≥25%. ASML 49% → `⚠CONC`.
- **Mandate** = `misV2PortfolioRecon_()` row `.mandate` (from `PORTFOLIO_CONTROL_`) + `misV2ClassifyAccount_()` (L2384). GROWTH/margin = Fidelity 600 Invest-n-Save.
- The Proposed verdict carries its own **reason code** (`v2.rc`) separate from the action — `ADD`/`ADD` here. Display the rc, never let it cap the action (no ADD→HOLD).

The pattern is **identical on all three**: a two-cell `LEGACY | PROPOSED` strip, an Exposure line, a Mandate line, and a `MODE: MIGRATION` tag. Action is shown twice (both engines), never reconciled into one.

---

### SURFACE 1 — TELEGRAM (text brief)
**Renders in:** `misV2DailyBrief_()` (Code.gs L580-604) — append a per-held-name MIGRATION block after the `SHORTLIST`/`YOUR OPEN` sections. Plaintext only (Telegram markdown landmine — no `*`/`_`).
**Field list:** Ticker · Legacy state · Proposed state(+rc) · Exposure% (+CONC) · Mode tag. NO sizing, NO checklist (that's TEST ENTRY).

```
📈 MIS BRIEF — MIGRATION (shadow)

ASML
  LEGACY    WATCH
  PROPOSED  ADD  (rc: ADD — strong across)
  EXPOSURE  49% of acct  ⚠CONC
  MODE      MIGRATION · both shown · engine action unchanged
```

---

### SURFACE 2 — TEST ENTRY (Entry Pad, full ticket)
**Renders in:** `misV2EntryPad_(sym,opts)` (Code.gs L704-961). Insert ONE block immediately after the verdict header div (after L941, before `washBanner`) so Legacy/Proposed/Exposure/Mandate land **on the same screen as the verdict** — no scroll, no second click. Use the existing `hd()`/chip styling. The card already shows the FSE `fs` (= the Proposed/production action); add the Legacy cell + Exposure + Mandate beside it.
**Field list (one screen):** Legacy state · Proposed state · Proposed rc · Exposure% (+CONC) · Mandate (owner + GROWTH/PRESERVE/ACTIVE) · margin flag · Mode tag. The full plan/sizing/checklist below stays as-is.

```
┌──────────────────────────────────────────────────────┐
│ MIS v2 · TEST ENTRY              ● LIVE · 3:14 PM ET   │
│                                                        │
│ ASML  ▲ 1.20% @ $812.40                                │
│ ASML Holding NV · Semis · LargeCap                     │
│ [ ADD ]  SACS 71 · Grade B · Pullback-in-uptrend       │
│                                                        │
│ ┌── MIGRATION (shadow · presentation only) ──────────┐ │
│ │  LEGACY (v1)     │  PROPOSED (v2)                   │ │
│ │  WATCH           │  ADD   rc: ADD                   │ │
│ │  universe-level  │  position-aware (you hold it)    │ │
│ ├────────────────────────────────────────────────────┤ │
│ │  EXPOSURE  49% of account net   ⚠ CONC (>25%)      │ │
│ │  MANDATE   Fidelity 600 · Invest-n-Save · GROWTH   │ │
│ │            margin acct (debit nets out of equity)   │ │
│ │  MODE      MIGRATION — both engines shown; the      │ │
│ │            verdict above is unchanged. CONC is a    │ │
│ │            WARNING, not a cap.                      │ │
│ └────────────────────────────────────────────────────┘ │
│                                                        │
│ THE READ … (existing)                                  │
│ HOW IT IS PROTECTED & HOW TO PLACE IT … (existing)     │
└──────────────────────────────────────────────────────┘
```

---

### SURFACE 3 — SCOPE (peek card)
**Renders in:** `outputs/peek.html` `.hld` block (L341-371), fed by the `peekoverlay` JSON `ov` (Code.gs L2882-2915). Today `ov.verdict` is a SINGLE state shown at L342 (`u.verdict`). For migration, `peekoverlay` must emit two fields — `ov.verdict_legacy` (from `synthV1`) and `ov.verdict_proposed`(+`ov.proposed_rc`, already = current `verdict`) — and `peekoverlay` already returns `ov.accounts[].mv`; compute exposure% inline OR add `ov.exposure_pct`+`ov.conc`. Render both inside the existing `.hh` header row + a new line — **no second click** (it's in the card that's already open).
**Field list (no extra tap):** Legacy state · Proposed state(+rc) · Exposure% (+CONC) · Mandate. (Held qty/cost/PnL/plan rows stay as they are.)

```
┌─ ASML ────────────────────────── [PROPOSED: ADD] ─┐
│ ASML Holding NV · Semiconductors    YOUR POSITION  │
│ $812.40   ▲ +1.20% · +$9.64           ● held       │
│ ┌────────────────────────────────────────────────┐│
│ │ LEGACY  WATCH   →   PROPOSED  ADD  (rc ADD)      ││
│ │ EXPOSURE  49% of acct  ⚠CONC   MIGRATION mode    ││
│ │ MANDATE  Fidelity 600 · GROWTH · margin          ││
│ └────────────────────────────────────────────────┘│
│ 8 sh @ $640.00 cost            +26.9%              │
│ mkt val $6,499 · SACS 71 · Gr B                   │
│ setup Pullback · stop $748 · target $980 · R:R 3.1│
└────────────────────────────────────────────────────┘
```

---

### Exact wiring (presentation-only; no engine mutation)
| Surface | Function to edit | What to add | Source of each field |
|---|---|---|---|
| TELEGRAM | `misV2DailyBrief_()` L580-604 | per-held MIGRATION block, plaintext | Legacy=`synthV1`, Proposed=`synthV2`+`rc` from `misV2ProposedState_` logic; Exp=`misV2Exposure_`; mode literal |
| TEST ENTRY | `misV2EntryPad_()` after L941 | one `hd()`-style MIGRATION block under verdict header | Proposed=`fs` (already rendered); add Legacy via `synthV1`; Exp/Conc=`misV2Exposure_`; Mandate=recon `.mandate`+`misV2ClassifyAccount_` |
| SCOPE | `peekoverlay` (Code.gs L2890) emits `verdict_legacy`+`proposed_rc`+`exposure_pct`+`conc`+`mandate`; `peek.html` `.hld` L341-371 renders them | new sub-block in `.hh`/meta-row | same three engines; reuse `ov.accounts[].mv` ÷ account net |

**Invariants honored:** Action shown twice, never merged. `⚠CONC` is a WARNING string only — it never downgrades ADD. `synthV1`/`synthV2`/`Exposure_` are read in shadow (the doc's "live FinalState frozen, NO production change", L1918). Mandate/margin are labels, not gates. No new tab, no new score, no interpretation layer. Gate-C exit criterion = these three blocks render Legacy≠Proposed (ASML WATCH vs ADD) on each surface with zero mutation.

**Note for whoever lifts the freeze:** `peekoverlay` currently emits only the single `ov.verdict`; the Scope surface is the one that needs a (read-only) Apps Script change to expose `verdict_legacy`+`proposed_rc`+`exposure_pct`. Telegram and Test Entry can be done entirely in render code that already has `synthV1`/`synthV2`/`misV2Exposure_` in-process. All edits are presentation; none touch `misV2BuildFSE` or write any tab. Source: `MIS/v2/Code.gs`, `ops-api/src/index.ts` L600-950, `outputs/peek.html` L341-468.

### Analyst 7. D after-hours acceptance checklist

I have everything needed. The bug is precisely as described and I've found a second-order issue: the dual-header gates on `session==='POST'||'PRE'` only, so the CLOSED branch (evenings/weekends, the most common after-hours viewing window) falls through to the single-price regular header and never shows "At Close vs After Hours." Here is the design.

## BLOCKER D — After-Hours Dual Header: Root Cause, Exact Fix (NOT applied), Acceptance Checklist

### Root cause (confirmed in code)
`extended` is initialized with `regular_pct: null` at `ops-api/src/index.ts:705`. The **v8-chart path** (`index.ts:727–792`) sets `extended.regular_close` in all six branches (weekend/REGULAR/POST/PRE/CLOSED) and `prevClose` is in scope at line 729 (`meta.previousClose ?? meta.chartPreviousClose`) — **but it never assigns `extended.regular_pct` in any branch.** It stays NULL.

Only the **v7-quote path** sets it (`index.ts:804`, `extended.regular_pct = yq.regularMarketChangePercent`). Per session context that path is "often Workers-blocked," so in production `extended.regular_pct` reaches `peek.html` as NULL most of the time.

`peek.html:414` masks this by recomputing client-side: `regPct = d.quote.prev_close ? ((regClose - d.quote.prev_close)/d.quote.prev_close*100) : 0`. That works coincidentally but is fragile — it (a) divides by `quote.prev_close` (Finnhub `q.pc`), which for a **PRE** session is *yesterday's* close while `extended.regular_close` was set to `prevClose` (line 782), making numerator and denominator the same series only by luck; (b) silently yields `0%` (a green "+0.00% on the day") when `prev_close` is missing, rather than degrading. The Worker should own this number.

### Exact code change (design only — DO NOT apply, freeze in effect)

**Change 1 — populate `extended.regular_pct` in EVERY v8-chart branch (`index.ts:727–792`).**
After computing `regClose` and `prevClose` (line 729), define one denominator-correct percent at the point each branch sets `regular_close`:
- Weekend (765), REGULAR (769), POST (773), CLOSED (791): `extended.regular_pct = (regClose != null && prevClose) ? ((regClose - prevClose)/prevClose)*100 : null;` (regular session's close vs prior day's close = "−2.53% on the day").
- PRE (782): here `regular_close` is deliberately set to `prevClose` (yesterday's close, since today hasn't opened). Set `extended.regular_pct = null` for PRE so the "At Close" line shows no spurious day-move, OR set it to the prior completed session's day move if `meta` exposes it — but the safe rule is **null in PRE**, because there is no "today's regular session" yet.

Compute `regular_pct` from `prevClose` (the v8 `meta.previousClose`/`chartPreviousClose`), NOT from Finnhub `q.pc`, so the number is internally consistent with `regular_close`.

**Change 2 — make the v7-quote path additive, not authoritative for the pct (`index.ts:804`).** Keep `extended.regular_pct = yq.regularMarketChangePercent` only as a fallback when the v8 path left it null (`extended.regular_pct ??= ...`), so the rare unblocked v7 response can't overwrite the now-correct v8 value.

**Change 3 — `peek.html` consumes the server value, drops the client recompute (`peek.html:414`).** Replace `const regPct = d.quote.prev_close ? (...) : 0` with `const regPct = (d.extended.regular_pct != null) ? d.extended.regular_pct : (d.quote.prev_close ? ((regClose - d.quote.prev_close)/d.quote.prev_close*100) : null);` and render the "% on the day" line only when `regPct != null` (no green +0.00% fallback).

**Change 4 — extend the dual-header gate to CLOSED (`peek.html:412`).** Today the gate is `session==='POST'||'PRE'`. The most common after-hours viewing window (evenings after 8pm ET, weekends) reports `session==='CLOSED'` (index.ts:763/789/790), so the page falls through to the single-price regular header (line 419) and never shows "At Close | After Hours." Widen the gate to include `CLOSED` **when** `ext_price != null` (a post-close print exists), and set the label via `session==='PRE' ? 'Pre-Market' : 'After Hours'`. When CLOSED with no `ext_price`, keep the single "At Close $X (−2.53%)" header using `regular_close` + `regular_pct`.

**Resulting Apple-style header (target render):**
`At Close $X (−2.53% on the day)` | `After Hours $Y (+0.49% vs close)` — left muted label, right gold label, both `font-feature-settings:'tnum'`, dual `ch up/dn` color classes. Session badge by the ticker (`peek.html:308–310`) reads REGULAR / AFTER-HOURS / PRE-MARKET / CLOSED.

### 6-point acceptance checklist (BLOCKER D)

1. **`regular_pct` is non-null off the v8 path alone.** Call `fn=peek` for a held ticker (e.g. ABBV) after 4pm ET with the v7-quote path assumed blocked; response `extended.regular_pct` is a number (not null), and `extended.regular_pct ≈ (regular_close − prevClose)/prevClose × 100` to 2dp. (Read-only verify via the peek endpoint.)

2. **Denominator consistency.** `extended.regular_pct` is computed from the SAME `prevClose` (v8 `meta.previousClose`/`chartPreviousClose`) used for `regular_close`, not from Finnhub `quote.prev_close` — confirm the two agree to 2dp and there's no PRE-session double-counting (PRE returns `regular_pct: null`).

3. **POST session dual header renders correctly.** During/just-after a real post-market window, peek shows BOTH labeled prices: "At Close $X (−Z% on the day)" (muted) and "After Hours $Y (+W% vs close)" (gold), with `ext_change`/`ext_pct` matching `(ext_price − regular_close)`.

4. **CLOSED-evening/weekend window shows the dual (or correct single) header.** After 8pm ET / Sat-Sun, `session==='CLOSED'`: if `ext_price != null` the dual header renders with label "After Hours"; if no ext print, a single "At Close $X (−2.53%)" renders — NOT a fall-through that hides the day move and NOT a spurious +0.00%.

5. **Correct session label + badge, no mislabel.** `seshTxt` map (`peek.html:309`) shows AFTER-HOURS for POST, PRE-MARKET for PRE, CLOSED otherwise; PRE never renders an "After Hours" label and POST never renders "Pre-Market" (label driven by `session`, line 416/321).

6. **Graceful degradation when both Yahoo paths fail.** With v8 AND v7 blocked (`extended.regular_pct` and `ext_price` both null), peek falls back to the single regular header (`peek.html:419`) using Finnhub `quote.day_pct` — no NaN, no "+0.00% on the day", no broken layout; the TTS `speak` string (line 377–378) and `LAST_SNAPSHOT.ext_pct` (line 389) stay null-safe.

### Files / anchors
- `ops-api/src/index.ts:705` (init), `:727–792` (v8 branches — the fix locus), `:796–826` (v7 fallback), `:935` (emits `extended`).
- `outputs/peek.html:412–417` (dual header), `:418–421` (regular fallback), `:308–310` (session badge), `:316–328` (ext card), `:377–389` (TTS/snapshot null-safety).
- Gate sequence reminder: D is blocked until C clears (B.5 → B.75 ABBV 17→7 RED-clear → C MIGRATION_MODE display → D). This is **design-only**; nothing above is applied, no clasp/deploy, freeze intact.

### Analyst 8. Security-truth != portfolio-truth

## MIS v2 — Engine-vs-Portfolio Orthogonality Scan (live, 2026-06-04)

**Method:** drove the read-only `/mis/peek` overlay (ops-api `index.ts` L600–950 → MIS `fn=peekoverlay`, Code.gs L2882–2915) across the live held universe; joined each name's FSE verdict (col 7) + SACS against per-account weight = position MV ÷ account NET equity from `PORTFOLIO_CONTROL_` (Code.gs L1980–1986) and the mandate string. Concentration threshold = `misV2Exposure_` ⚠CONC at ≥25% of account net (L2031–2046).

**Live held equity set (8 names; not the 51 in `_full_reconcile_2026-06-03.json` — PLTR/AVGO/MSFT/TSLA/TSM/CRM/etc. all return `held:false` live, i.e. closed since that snapshot).** SPY's overlay is unreachable via peek — Finnhub `profile2` has no ETF profile so the Worker bails `unknown_ticker` at L647; SPY values below are from the locked context (TRIM @54%). Account net basis: 600=$14,002 · 5692=$4,967 · 4073=$1,289 · 5378=$523 · 898=$1,355.

| Ticker | Account | Engine Action (verdict / SACS) | Weight % | Mandate | Orthogonality flag | What the operator should SEE (warning beside the unchanged Action) |
|---|---|---|---|---|---|---|
| **META** | 5378 (kid/daughter) | WATCH · SACS **3.6** (grade D) | **68%** | KID | ⚠CONC extreme + dead-engine | "68% of daughter's $523 acct in a grade-D / SACS-3.6 name. Engine has no edge here; this is single-stock risk in a child account. Action stays WATCH — but trim/diversify is a portfolio call." |
| **ASML** | 600 (Invest-n-Save) | WATCH · SACS 43.3 (grade A) | **49%** | GROWTH (margin) | ⚠CONC + sits behind margin | "Half the margined growth acct in one semi. Strongest SACS you hold, but it's levered (acct 600 carries the −$11,132 debit). Concentration is a WARNING, not an ADD cap." |
| **META** | 600 | WATCH · SACS 3.6 | **46%** | GROWTH (margin) | ⚠CONC + weak-engine on margin | "46% of the margined acct in a SACS-3.6 grade-D name. Engine isn't endorsing this size; it's legacy weight on leverage." |
| **ASML** | 4073 (kid/son) | WATCH · SACS 43.3 | **37%** | KID | ⚠CONC in child acct | "Strong name, but 37% of son's $1,289 acct — concentration warning only; verdict unchanged." |
| **ABBV** | 600 | WATCH · SACS 36.3 (grade B) | **27%** | GROWTH (margin) | ⚠CONC **+ STALE QTY (recon RED root cause)** | "Shows 17sh / $3,729 = 27% — but broker has **7sh** (partial sale settling). Weight is overstated until HOLDINGS_CLEAN refreshes 17→7; this single row is what's holding recon RED. Don't act on the 27% until B.75 clears." |
| **NVDA** | 4073 (kid/son) | WATCH · SACS 37.3 (grade B) | **23%** | KID | near-CONC + decent-engine in child acct | "Good SACS, 23% of son's acct (just under the 25% flag). Not the active book — engine signal is informational, not a buy mandate here." |
| **LLY** | 5692 (Joint) | WATCH · SACS 33.3 | **17%** | PRESERVE (no margin, Israel-trip) | engine-decent **in a PRESERVE acct** | "Solid SACS but this is the no-margin Israel savings acct. PRESERVE mandate says don't chase signal here — any 'engine likes it' read must not invite an ADD in this book." |
| **AAPL** | 5692 (Joint) | WATCH · SACS 35.5 (grade A) | 8% | PRESERVE | mandate mismatch (signal in wrong book) | "Grade-A name, but it's in PRESERVE. Engine strength here is orthogonal to the account's job — surface as 'good name, wrong account,' never as a top-up cue." |
| **GOOG** | 5692 (Joint) | **REJECTED** · SACS 13.3 (grade D) | 7% | PRESERVE | engine-reject held in preserve | "Engine says REJECTED (grade-D fail). It's small (7%) and in PRESERVE, so no forced action — but it should NOT appear on any ADD list; show the reject reason next to it." |
| **COST** | 600 / 4073 | **REJECTED** · SACS 10.7 | 14% / 15% | GROWTH / KID | engine-reject, held both books | "REJECTED everywhere it's held; flag as 'engine would not buy this today' beside the position — informational, no sell forced." |
| **SPY** | (locked ctx) | TRIM @ 54% | 54% | — | ⚠CONC (overweight, engine agrees) | NOT truly orthogonal — engine TRIM and portfolio overweight point the same way; this is the aligned case, shown only as contrast to the orthogonal ones above. |

### The orthogonality patterns to encode (each = a WARNING beside an unchanged Action)

1. **Strong engine, but margined or wrong-mandate account** — ASML@600 (49%, behind the −$11,132 margin) and LLY/AAPL@5692 (PRESERVE). Engine "likes it" must never read as "add here." The Portfolio Layer should print a `MANDATE_BLOCK` note (PRESERVE = no new risk; 600 = already levered) without touching the WATCH/ADD verdict.

2. **Dead engine, but heavy weight** — META is SACS **3.6 / grade D** yet is the single largest cross-account position (68% of daughter's acct, 46% of acct 600). This is the inverse of an ADD-while-overweight: it's *size with no engine support*. The card should say "concentration high **and** engine has no conviction" — the worst quadrant, but still WARNING-only, no auto-trim.

3. **Engine REJECTED, still held** — GOOG (REJECTED, SACS 13.3) and COST (REJECTED, SACS 10.7). These must be suppressed from any ADD surface and tagged with the reject reason inline; do not let a REJECTED name sit silently in an exposure list with no engine annotation.

4. **Concentration ≥25% with the qty still stale** — ABBV@600 reads 27% CONC, but that 27% is computed on the **17sh the engine still holds vs the broker's 7sh**. This is the same RED-recon freshness defect (Gate B.75): the Portfolio Layer is flagging concentration off un-settled quantity. Until ABBV refreshes 17→7, treat its CONC flag (and ~$2,000 of acct-600 weight) as provisional — it will self-clear and is not a real over-allocation to act on.

**Architecture check (clean):** in every case above the verdict came straight from FSE col 7 and the weight/mandate/CONC came from the recon+exposure layer — confirming `misV2Exposure_` (L2045) and the overlay never mutate Action. No `ADD→HOLD` downgrade path exists in the held set; the only true ADD-while-overweight candidate named in the brief (ASML ADD @49%) shows here as **WATCH** live (SACS 43.3, setup PORTFOLIO_OVERLAP), so the "ADD but already overweight" case is currently latent, not active — the live overweight names (META, ABBV) are WATCH/REJECTED, not ADD.

Source trail: ops-api/src/index.ts L600–950 · MIS/v2/Code.gs L1980–2046, L2882–2915 · live `/mis/peek` 2026-06-04 · weights vs `PORTFOLIO_CONTROL_` net equity. No files edited, no clasp, no writing fn called.
