# mfe-eval.ps1 - Harness-16 Memory-Fidelity-EVAL (MFE) - S93 (2026-06-29) # # WHAT THIS IS (and is NOT): # MFE = COVERAGE / RETENTION eval: when hot-memory (always-loaded Tier-1) is # provisioned, how much of the REQUIRED governance set actually FITS, and (per # role) how much is still CARRIED in the persistent diary. It answers a question # the %-print (size-by-length) cannot: provisioned != remembered != applied. # This is DISTINCT from H6.7 "memoryDelta-routing-fidelity" (the right delta # landing in the right agent-memory). DO NOT conflate the two senses. # # NON-NEGOTIABLES (Harness-11 / Harness-16): # (1) NO-API : Select-String + byte/file parse ONLY. NEVER calls a model. # (2) READ-ONLY on token_governor : MFE consumes the budget caps (single-source), # NEVER writes/auto-tunes them (owner-authority floor, budget.json role_boundary_note). # (3) PS 5.1, ASCII-only script body (gotcha #30). Target files read -Encoding UTF8. # (4) Exit 0 always : a measure-and-report tool, NOT a build gate. # # DETERMINISTIC ($0) block (Branches: lead Coverage + age-band + Goodhart; sub per-role coverage). # Branch-A judge (recall/apply) = SCAFFOLD only (-Judge) : numbers MEANINGLESS until # sample-questions mature AND an independent cross-session judge runs (see honest_caveats). # # Usage: # powershell.exe -ExecutionPolicy Bypass -File scripts\mfe-eval.ps1 # all tiers # powershell.exe -ExecutionPolicy Bypass -File scripts\mfe-eval.ps1 -Tier lead # powershell.exe -ExecutionPolicy Bypass -File scripts\mfe-eval.ps1 -Tier sub # powershell.exe -ExecutionPolicy Bypass -File scripts\mfe-eval.ps1 -Judge # show judge scaffold (no scoring) param( [string]$RepoRoot = "$PSScriptRoot\..", [ValidateSet('lead','sub','all')] [string]$Tier = 'all', [switch]$Judge = $false, [switch]$Detail = $false, # @S152 owner#32: per-item L1-presence - print each MISSed denominator item by name [switch]$Ack = $false, # @S152 B4: acknowledge current strikes -> resets the GOODHART-WARN latch baseline [datetime]$Today = (Get-Date) ) $ErrorActionPreference = 'Stop' $CHECK = [char]0x2705 # green check glyph, kept as code-point (ASCII body, gotcha #30) # --------------------------------------------------------------------------- # resolve paths + load single-source config # --------------------------------------------------------------------------- $RepoRoot = (Resolve-Path $RepoRoot).Path $budgetPath = Join-Path $RepoRoot '.claude\agent-memory\memory-budget.json' if (-not (Test-Path $budgetPath)) { Write-Host "memory-budget.json not found: $budgetPath"; exit 0 } $budget = Get-Content $budgetPath -Raw -Encoding UTF8 | ConvertFrom-Json $cfg = $budget.mfe if ($null -eq $cfg) { Write-Host "memory-budget.json missing 'mfe' config block (Harness-16)"; exit 0 } $leadCap = [int]$budget.token_governor.tier1_hotfeed_tokens.lead_tokens # LIVE-read, never hardcode $memCap = [int]$budget.token_governor.tier1_hotfeed_tokens.memory_sub_tokens $marksPath = Join-Path $RepoRoot '.claude\governance\ACTIVE-MARKS.md' $ledgerPath = Join-Path $RepoRoot 'docs\governance\error-ledger.md' $gotchaPath = Join-Path $RepoRoot 'docs\gotchas.md' $statePath = Join-Path $RepoRoot $cfg.state_file function Read-Utf8([string]$p) { if (Test-Path $p) { return Get-Content $p -Encoding UTF8 } else { return @() } } function Bytes([string]$s) { return [Text.Encoding]::UTF8.GetByteCount($s) } function Cells([string]$line) { # split a markdown table row into inner cells (drop leading/trailing empties) $parts = $line.Trim().Trim('|') -split '\|' return ($parts | ForEach-Object { $_.Trim() }) } function Remove-Diacritics([string]$t) { $n = $t.Normalize([Text.NormalizationForm]::FormD) $sb = New-Object Text.StringBuilder foreach ($c in $n.ToCharArray()) { if ([Globalization.CharUnicodeInfo]::GetUnicodeCategory($c) -ne [Globalization.UnicodeCategory]::NonSpacingMark) { [void]$sb.Append($c) } } return $sb.ToString().ToLowerInvariant() } function Content-Words([string]$s, [string[]]$stop) { $folded = Remove-Diacritics $s $words = $folded -split '[^a-z0-9]+' | Where-Object { $_ -and $_.Length -ge 2 -and ($stop -notcontains $_) } return ($words | Select-Object -Unique) } Write-Host "" Write-Host "=== MFE (Memory-Fidelity-EVAL) - Harness-16 - $($Today.ToString('yyyy-MM-dd')) ===" Write-Host " (coverage/retention eval; NOT memoryDelta-routing-fidelity H6.7)" Write-Host " lead hot-feed cap (live) = $leadCap tok | mem-sub cap = $memCap tok" Write-Host "" # =========================================================================== # LEAD TIER : must-remember denominator -> Coverage(fit) + age-band + Goodhart # =========================================================================== function Measure-Lead { # --- Source A: Active marks (status Active-High / Active) --- $marks = @(); $markBytes = 0; $markDates = @() $inSec = $false foreach ($ln in (Read-Utf8 $marksPath)) { if ($ln -match '^##\s') { $inSec = ($ln -match 'ACTIVE') ; continue } # ACTIVE-HIGH + ACTIVE; skips MEDIUM/SUPERSEDED if ($inSec -and $ln -match '^\|\s*`(RC-[a-z0-9-]+)`') { $rid = $matches[1] $c = Cells $ln $status = $c[$c.Count-1] if ($status -match 'Active-High' -or $status -match 'Active\b') { $marks += $rid; $markBytes += (Bytes $ln) if ($rid -match '-(\d{2})-(\d{2})-(\d{4})-\d{2}-\d{2}-\d{2}$') { $markDates += [datetime]::new([int]$matches[3],[int]$matches[2],[int]$matches[1]) } } } } # --- Source B: error-ledger Active-Guards + AS-table + RCA strikes --- $guards = @(); $guardBytes = 0; $guardCounters = @{} $asIds = @(); $asGotchaRefs = New-Object System.Collections.Generic.HashSet[string] $asBytes = 0; $strikesTotal = 0; $rcaCount = 0 $section = '' foreach ($ln in (Read-Utf8 $ledgerPath)) { if ($ln -match '^##\s') { if ($ln -match 'Active-Guards') { $section = 'guards' } elseif ($ln -match 'L\.a') { $section = 'as' } else { $section = '' } continue } if ($ln -match '^### (E-\d+)') { $rcaCount++ ; continue } if ($section -eq 'guards' -and $ln -match '^\|' -and $ln -match [regex]::Escape($CHECK)) { $c = Cells $ln if ($c.Count -ge 5 -and $c[0] -notmatch '^-+$' -and $c[0] -ne 'Guard') { $net = $c[$c.Count-1] if ($net -notmatch '^\s*-' -and $net -notmatch 'retire') { $g = $c[0]; $guards += $g; $guardBytes += (Bytes $ln); $guardCounters[$g] = $c[1] if ($c[3] -match '(\d+)') { $strikesTotal += [int]$matches[1] } # Strikes col } } } if ($section -eq 'as' -and $ln -match '^\|\s*(AS-\d+)\b') { $asIds += $matches[1]; $asBytes += (Bytes $ln) foreach ($m in [regex]::Matches($ln, '#(\d+)')) { [void]$asGotchaRefs.Add($m.Groups[1].Value) } } } # --- Source C: recurring gotchas --- $reTokens = @($cfg.recurrence_tokens) $allGotchas = 0; $recurGotchaNums = @(); $recurBytes = 0 $lines = Read-Utf8 $gotchaPath for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '^### (\d+)\.') { $allGotchas++ $num = $matches[1] $blk = $lines[$i] for ($j = $i+1; $j -lt $lines.Count -and $lines[$j] -notmatch '^### '; $j++) { $blk += "`n" + $lines[$j] } $folded = Remove-Diacritics $blk $isRecur = $false foreach ($t in $reTokens) { if ($folded.Contains((Remove-Diacritics $t))) { $isRecur = $true; break } } if ($isRecur) { $recurGotchaNums += $num; $recurBytes += (Bytes ($lines[$i])) } } } # --- DEDUP (merge_guard_to_as): collapse gotchas/guards that an AS-row references --- $merge = [bool]$cfg.merge_guard_to_as $netRecur = @($recurGotchaNums | Where-Object { -not $asGotchaRefs.Contains($_) }) $netGuards = @() foreach ($g in $guards) { $ctr = [string]$guardCounters[$g] $absorbed = $false if ($merge) { if ($ctr -match 'AS-\d+') { $absorbed = $true } else { foreach ($m in [regex]::Matches($ctr, '#(\d+)')) { if ($asGotchaRefs.Contains($m.Groups[1].Value)) { $absorbed = $true; break } } } } if (-not $absorbed) { $netGuards += $g } } $denomCount = $marks.Count + $asIds.Count + $netGuards.Count + $netRecur.Count $denomBytes = $markBytes + $asBytes + $guardBytes + $recurBytes $tokLow = [math]::Round($denomBytes / 4.0) $tokHigh = [math]::Round($denomBytes / 3.0) $pctHigh = if ($leadCap -gt 0) { [math]::Round(100.0 * $tokHigh / $leadCap, 2) } else { 0 } $fits = ($tokHigh -le $leadCap) Write-Host "[LEAD] must-remember denominator (deduped, merge_guard_to_as=$merge):" Write-Host " marks(Active+High)=$($marks.Count) AS-classes=$($asIds.Count) net-extra-guards=$($netGuards.Count) net-extra-recurring-gotchas=$($netRecur.Count)" Write-Host " => DENOMINATOR = $denomCount items (raw before dedup: marks $($marks.Count) + AS $($asIds.Count) + guards $($guards.Count) + recurring $($recurGotchaNums.Count))" Write-Host "" Write-Host "[LEAD] Coverage-FIT (does the set FIT the hot-feed cap?):" Write-Host " span bytes=$denomBytes => ~$tokLow - $tokHigh tok (RANGE, see caveat) / cap $leadCap tok = $pctHigh% worst-case" if ($fits) { Write-Host " FIT = PASS (worst-case fits; headroom huge - must-remember set is tiny vs cap)" } else { Write-Host " FIT = OVER (worst-case exceeds cap) -> decision: INCREASE budget (lack-of-SPACE case)" } Write-Host " CAVEAT: char/4 is NOT real tokens; VN-diacritic ~3.0-3.5 byte/tok => byte/4 = upper-bound headroom." Write-Host " Reported as a RANGE [bytes/4 .. bytes/3.0]; FIT uses bytes/3.0 (worst). Real count inside the band." Write-Host "" # --- age-band : FLAG old-but-still-required, NEVER cut (mark RC-...10-29-11) --- $oldN = 0 foreach ($d in $markDates) { if (($Today - $d).TotalDays -gt 30) { $oldN++ } } Write-Host "[LEAD] age-band (FLAG only - age=false-proxy, mark RC-...10-29-11):" Write-Host " marks with parseable date=$($markDates.Count); >30d old but still Active=$oldN -> KEPT (status-driven, age-blind)" Write-Host " (guards/gotchas carry session-refs not dates -> age = status-driven only; drop on status-change, never by age)" Write-Host "" # --- Goodhart anchor : print recurring-error reality next to coverage --- $lastStrikes = -1; $warnBase = -1; $histOld = @() if (Test-Path $statePath) { try { $stOld = Get-Content $statePath -Raw -Encoding UTF8 | ConvertFrom-Json $lastStrikes = [int]$stOld.strikes_total if ($stOld.PSObject.Properties.Name -contains 'warn_baseline') { $warnBase = [int]$stOld.warn_baseline } if ($stOld.PSObject.Properties.Name -contains 'history') { $histOld = @($stOld.history) } } catch { } } # B4 @S152 LATCH: WARN compares against a PERSISTED warn_baseline that only moves on -Ack. # Old behavior compared to the immediately-previous run then overwrote state -> the # measuring run CONSUMED its own warning (S152 eval S6: audit re-ran mfe minutes later, # WARN already silent). Migration: no warn_baseline key yet -> seed from pre-B4 # strikes_total (old file) or from the current count on a truly fresh state; the seed # itself never fires (baseline opens the ledger, it is not a rise). if ($warnBase -lt 0) { $warnBase = if ($lastStrikes -ge 0) { $lastStrikes } else { $strikesTotal } } if ($Ack) { $warnBase = $strikesTotal } Write-Host "[LEAD] Goodhart anchor (NO self-grading - real recurring-error signal):" Write-Host " error-ledger: strikes_total=$strikesTotal RCA_entries=$rcaCount AS-classes=$($asIds.Count)" if ($Ack) { Write-Host " (ACK: warn-baseline set to $strikesTotal - future WARN fires only when strikes exceed this)" } elseif ($strikesTotal -gt $warnBase -and $fits) { Write-Host " GOODHART-WARN (LATCHED): strikes rose ($warnBase -> $strikesTotal) while set still FITS -> coverage may LIE. Repeats every run until acknowledged via -Ack." } else { Write-Host " (rule: if strikes rise across runs while coverage stays high -> the score lies. warn-baseline=$warnBase last_run_strikes=$lastStrikes)" } # persist strikes for cross-run compare (MFE state only; does NOT touch token_governor) # Write LF + UTF8-no-BOM explicitly (erratum EOL 2026-07-16 lo #6 point-of-generation floor, S129): # PS5.1 Set-Content -Encoding UTF8 wrote BOM+CRLF -> worktree CRLF churn AND a 3-byte BOM in the # committed blob. Reader at line 193 pins -Encoding UTF8 in the SAME commit (no BOM-sniff dependency). try { # B4 @S152: history[] = append-unbounded BY DESIGN (absence-of-entry = never-happened # stays a safe read; if a FIFO cap is ever added, eviction MUST be explicit - # log-BOUNDED design-note dede7ec5). ack_strikes persists the WARN-latch baseline. $histNew = @($histOld) + ,(@{ at = $Today.ToString('yyyy-MM-dd HH:mm'); strikes_total = $strikesTotal; rca_entries = $rcaCount }) $st = [ordered]@{ strikes_total = $strikesTotal; rca_entries = $rcaCount; at = $Today.ToString('yyyy-MM-dd'); warn_baseline = $warnBase; history = $histNew } $json = (($st | ConvertTo-Json -Depth 5) -replace "`r`n", "`n") + "`n" [System.IO.File]::WriteAllText($statePath, $json, (New-Object System.Text.UTF8Encoding($false))) } catch { } Write-Host "" } # =========================================================================== # SUB TIER : per-role coverage (denominator from role .md, numerator from diary) # =========================================================================== function Measure-Sub { $stop = @($cfg.sub_denominator.stop_list) $anchors = @($cfg.sub_denominator.denom_header_anchors) $minWords = [int]$cfg.sub_denominator.content_word_min $roleFiles = Get-ChildItem (Join-Path $RepoRoot '.claude\agents\*.md') -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne 'README.md' } Write-Host "[SUB] per-role memory coverage (denominator = role-file anti-patterns/baseline; numerator = carried in diary):" Write-Host " match = exact-id OR >=$minWords distinct content-words after stop-list (leading-verb-only = false-max, defeated)" Write-Host "" foreach ($rf in $roleFiles) { $role = $rf.BaseName $rlines = Read-Utf8 $rf.FullName # denominator: numbered '^N.' lines UNDER a denom-anchored '##' header, + a NEVER:/scope line $denom = @(); $inDenom = $false foreach ($ln in $rlines) { if ($ln -match '^##\s') { $inDenom = $false foreach ($a in $anchors) { if ($ln -match [regex]::Escape($a)) { $inDenom = $true; break } } continue } if ($inDenom -and $ln -match '^\s*\d+\.\s+(.+)') { $denom += $matches[1] } if ($ln -match 'NEVER' -and $ln -match ':') { $denom += ($ln -replace '.*NEVER\s*:?\s*','') } } $denom = @($denom | Where-Object { $_ -and $_.Trim().Length -gt 3 }) # diary (THE agent diary - NOT project/user MEMORY.md) $diaryPath = Join-Path $RepoRoot ".claude\agent-memory\$role\MEMORY.md" if (-not (Test-Path $diaryPath)) { Write-Host (" {0,-22} N/A (no persistent diary -> ephemeral/unspawned)" -f $role) continue } $diaryRaw = (Read-Utf8 $diaryPath) -join "`n" $diaryFold = Remove-Diacritics $diaryRaw $hit = 0 foreach ($item in $denom) { $words = Content-Words $item $stop $matched = $false if ($words.Count -ge $minWords) { $found = 0 foreach ($w in $words) { if ($diaryFold -match ('(? N/A, NOT '0% retention' Write-Host (" {0,-22} N/A (no numbered anti-pattern / NEVER block in role-file to extract)" -f $role) continue } $pct = [math]::Round(100.0 * $hit / $den) $flag = if ($pct -lt 60) { " <- LOW (real retention signal, not a target-miss)" } else { "" } Write-Host (" {0,-22} {1,3}/{2,-3} = {3,3}% (MEASURED, not target-then-force){4}" -f $role, $hit, $den, $pct, $flag) } Write-Host "" Write-Host "[SUB-WORKFLOW] N/A by design: fan-out workflow agents (hmw.js sub--) are EPHEMERAL" Write-Host " (no persistent diary; their only memory channel is the lead-injected memory-pack -> source-vs-cap is meaningless)." Write-Host "" } # =========================================================================== # JUDGE scaffold (Branch A) : -Judge only. SCAFFOLD - numbers MEANINGLESS. # =========================================================================== function Show-JudgeScaffold { $sqPath = Join-Path $RepoRoot $cfg.sample_questions Write-Host "[JUDGE-SCAFFOLD] Branch-A recall/apply (OPT-IN, cadence-gated, NO-API at scaffold level):" if (Test-Path $sqPath) { $sq = Get-Content $sqPath -Raw -Encoding UTF8 | ConvertFrom-Json Write-Host " sample-questions loaded: $($sq.questions.Count) (seeded $($sq.seeded_date), anchored by stable-id)" Write-Host " scorer = EMPTY (no answers recorded). recalled/applied = (none) ; meaningful = FALSE" } else { Write-Host " sample-questions file NOT found: $sqPath" } Write-Host " HONEST: a same-session self-grade is MEANINGLESS (the agent just read the answers)." Write-Host " Real numbers need (a) sample-questions matured in age AND (b) an independent" Write-Host " cross-session / different-model judge. Until then this is plumbing-smoke only." Write-Host "" } # =========================================================================== # dispatch # =========================================================================== if ($Tier -eq 'lead' -or $Tier -eq 'all') { Measure-Lead } if ($Tier -eq 'sub' -or $Tier -eq 'all') { Measure-Sub } if ($Judge) { Show-JudgeScaffold } Write-Host "=== MFE done (deterministic block; READ-ONLY on budget; exit 0) ===" exit 0