Files
solution-erp/scripts/wal-recovery-test.ps1
pqhuy1987 d07bbb98c5 [CLAUDE] Infra: W1 - 2 detector H24 + 3 script governance (eol-hygiene / wal-recovery 4-ca / spawn-model-audit)
Wave adap-6-broadcast W1 - 4 lane disjoint qua /hmw RUN-TRACE (wf_de36dea9-265, 4/4 lane 0-error).
Lead TU CHAY LAI ca 4 + tu fault-inject, KHONG tin return (gotcha #53 fire 2/2 @W0.6).

(1) governance-detectors.ps1 455->715
    - GAP-3 +2 token canonical: menu=54 (STATUS.md:19); policy = row CHUA co (W2 land)
      -> FAIL-LOUD + SKIP, khong im lang gia vo xanh.
    - Detector H24-1 title-freshness: anchor_patterns[] = MANG 2 dang (fix C2). Bat buoc co dang
      '**Status (post Session N <em-dash> YYYY-MM-DD):**' vi permission-matrix/SKILL.md
      (positive-control SONG) dung dang do va KHONG co 'Last updated'. Neu 1 regex => detector
      loai thang chinh positive-control cua no => W4 bat-kha-thi.
      NEO-PHAM-VI load-bearing: STATUS.md:6 chua 2026-08-01 / 1987-03-07 / 2026-20-42 (cat tu
      mark-id). Regex-ngay-bat-ky lay max => 2026-20-42 = ngay KHONG ton tai => moc-phai rac
      => trai<phai khong bao gio dung => 0 flag VINH VIEN ma van xanh.
    - Detector H24-2 carry-age INFORM-only: M doc tu agent-memory/memory-budget.json
      h24_cadence.light_every; thieu key => FAIL-LOUD, 0 so nhip hardcode.
    - Bug lane tu bat: PS ten bien CASE-INSENSITIVE => $M (cadence) == $m (loop match) => ghi de
      cadence => vo nhanh fail-loud. exit 0 CHE bug nay; chi stderr moi lo.
    - M3 scope giu: 4 hang so owner-da-ky KHONG dung (walLineCount 40 / ratio / maxGotcha / liveVariants).

(2) agent-frontmatter-eol-check.ps1 (NEW) - HYGIENE-only
    W0.6 spawn-probe @S121 do duoc CRLF-TOLERANT (agent CRLF 276 byte CR spawn an du 4 tang;
    token 45248 LF vs 45240 CRLF, lech 8 = chenh chu trong prompt) => gia thuyet 'CRLF giet
    registry' BI BAC => script ha cap xuong hygiene, khai that ca o header lan thong diep FLAG
    runtime. Scope-chat 37 file (agents/commands/skills), KHONG repo-wide (188 = duong-gia
    factory). Tu in GREEN-BUT-VACUOUS thay vi nhan cong.

(3) wal-recovery-test.ps1 (NEW) - 4/4 ca, 30 assert
    FIX #3-bis: bien re nhanh = DAY RANGE, KHONG phai NONWAL. Counterfactual chay luat cu =>
    tai hien DEADLOCK that voi rebase-exit=0 (hong ma bao thanh cong). Assert
    sut-has-no-NONWAL-param bang Get-Command reflection, KHONG grep => ne tu-tham-chieu (W0.4#1).

(4) spawn-model-audit.ps1 (NEW) - PA-2b
    FAIL-LOUD khi thieu TIER2_EXPECTED_FULL_ID (W2 chua land). Muc E liet 6 dieu no KHONG chung,
    gom precedence spawn-param vs frontmatter (CHUA TEST, defer phien lead=Fable) + fix#8a chua thu.

VERIFY LEAD TU DO (khong phai so sub khai):
  TOTAL FLAGS 42->49 (>=46) . permission-matrix/SKILL.md FIRE 3 dong (W4 kha thi; KHONG lane nao
  cham file do) . exit 0 + stderr 0 x4 . non-ASCII 0/4 . teeth 2/2: inject 276 CR -> FLAG, go -> 0;
  inject claude-opus-9-9 -> FLAG mismatch, sua -> het. Containment sach.

LOI LEAD @S121 (di vao adap-report, khong im):
  (a) San-1 lech: marker wf: dang ky SAU khi phong (luat = TRUOC).
  (b) Khang dinh fact-tren-dia ma KHONG do dia: lead phan 'backfill [carry:*] = orphan chua ai lam'
      -> SAI, da co 7 khoa tu 4727d16 (S119, da push). Co che: ra BANG WAVE thay khong ai duoc
      giao => suy ra chua lam. 'Khong wave nao so huu' != 'chua co tren dia'. Brief lead co 4 tien
      de sai (config path / acceptance ngay 07-14 vs that 07-15 / carry / do-dai); lane (1) do dia va va het.

GAP THAT thay claim sai (lane 1 tim, lead verify dia): carry-age vacuous vi CAU TRUC - 45
logic-segment nhung carry-lines=1 => streak==1 => 0 fire vinh vien ke ca sau W2. Lo = thieu nghi
thuc RE-STAMP moi phien => giao W3 session-end.md, KHONG phai W2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 12:47:55 +07:00

536 lines
24 KiB
PowerShell

<#
.SYNOPSIS
wal-recovery-test.ps1 - rerunnable regression TEST for FIX #3-bis (WAL fold branch rule).
.DESCRIPTION
Turns the hand-run W0.3 fault-injection (S120, run-folder W0-evidence 4.1/4.2) into a
test that can be re-run. This is a TEST, not a detector: it EXITS NON-ZERO on failure
(governance-detectors.ps1 is exit-0-always by mandate; this one is a gate for itself).
RULE UNDER TEST (FIX #3-bis, spec v3-PATCH):
K = git rev-list --count origin/main..HEAD
BOTTOM = git log --format=%s origin/main..HEAD | LAST line (= OLDEST commit in range)
K == 0 -> 'noop' empty range: no reset, no rebase, commit straight
BOTTOM starts wal: -> 'reset-soft' ca1 (NONWAL=0) AND ca2 (NONWAL=1) - SAME handling
else -> 'rebase-fixup' ca3 (bottom = ordinary commit)
NONWAL IS NOT A BRANCH VARIABLE. Select-WalFoldBranch does not even ACCEPT it as a
parameter - a structural anchor (asserted at runtime through Get-Command reflection,
NOT through a grep over source text: a grep would self-match this comment block).
WHY ca2 KILLS THE OLD RULE (spec v2 :92 branched on NONWAL==0):
shape base|wal:|[CLAUDE] has NONWAL=1 -> old rule picks REBASE -> todo head = pick wal:
-> fixup melts the top commit INTO the bottom one and INHERITS THE BOTTOM MESSAGE
-> range still contains a 'wal:' commit -> push-guard blocks -> re-squash -> same result
-> permanent STOP. The hazard is that it SUCCEEDS INCORRECTLY: exit 0, no rebase-merge
residue, no trace. So a test asserting only 'exit code == 0' would PASS on the bug.
ANTI-TAUTOLOGY ORACLE:
The expected branch is NOT a restatement of the rule. For every case BOTH candidate
branches are EXECUTED on independent throwaway trees and the post-state is MEASURED
(count of 'wal:' commits left in origin/main..HEAD after the fold + re-commit):
exactly one candidate CLEAN -> expectation is EMPIRICAL (measured, not asserted)
both candidates CLEAN -> oracle INCONCLUSIVE -> falls back to the spec-stated
expectation, and the table SAYS SO (honest label)
ca1/ca2 are EMPIRICAL. ca0/ca3 are DEFINITIONAL - declared, not hidden.
REGRESSION-GUARD (the real value): at ca2 the test asserts the SUT picks the branch this
very run MEASURED to be clean, and that the legacy rule picks the branch this very run
MEASURED to deadlock. Flip Select-WalFoldBranch back to NONWAL-branching and ca2 FAILS.
Verify the teeth with -InjectLegacyRule (must exit non-zero).
.PARAMETER RepoRoot
Repo root. Used as a CONTAINMENT ORACLE only: this test must never touch the real .git.
Containment is proven STRUCTURALLY - every path this script writes derives from -WorkDir,
and -WorkDir is asserted to sit outside RepoRoot. The RepoRoot HEAD + porcelain snapshot is
printed as corroboration but deliberately NOT asserted: in a concurrent multi-agent wave a
sibling lane or the WAL Stop-hook can legitimately move both mid-run, so asserting on them
would fail for exogenous reasons (a flaky gate is worse than no gate). No repo file is read
as test material - the trees are synthesized from scratch.
.PARAMETER WorkDir
Parent dir for the throwaway git trees. Default = $env:TEMP.
.PARAMETER KeepTemp
Keep the throwaway trees for inspection instead of deleting them.
.PARAMETER InjectLegacyRule
FAULT-INJECTION SELF-TEST. Swaps the SUT for the legacy NONWAL rule. Expected result:
ca2 FAILS and the script exits non-zero. Proves this test is not vacuously green.
.EXAMPLE
powershell.exe -ExecutionPolicy Bypass -File scripts/wal-recovery-test.ps1
powershell.exe -ExecutionPolicy Bypass -File scripts/wal-recovery-test.ps1 -InjectLegacyRule
#>
param(
[string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path,
[string]$WorkDir = $env:TEMP,
[switch]$KeepTemp,
[switch]$InjectLegacyRule
)
$ErrorActionPreference = 'Continue'
# ---------------------------------------------------------------------------
# Result plumbing
# ---------------------------------------------------------------------------
$script:AssertCount = 0
$script:FailCount = 0
$script:CaseFail = @{}
# PRE-fold measurement per case, captured from real git BEFORE any surgery. The guard
# below must reuse THIS, never re-measure a main tree that has already been folded.
$script:CaseMeasure = @{}
$script:Rows = New-Object System.Collections.Generic.List[object]
function Write-Section($title) {
Write-Host ''
Write-Host ("===== $title =====") -ForegroundColor Cyan
}
function Assert-That {
param([string]$CaseId, [string]$Name, [bool]$Ok, [string]$Detail)
$script:AssertCount++
if ($Ok) {
Write-Host (" [PASS] {0,-4} | {1,-26} | {2}" -f $CaseId, $Name, $Detail) -ForegroundColor Green
}
else {
Write-Host (" [FAIL] {0,-4} | {1,-26} | {2}" -f $CaseId, $Name, $Detail) -ForegroundColor Red
$script:FailCount++
$script:CaseFail[$CaseId] = $true
}
}
# ---------------------------------------------------------------------------
# RULE UNDER TEST (SUT) - FIX #3-bis
# Branch variable = BOTTOM of range (+ K for the empty case). NONWAL absent BY DESIGN:
# once the bottom is 'wal:', reset --soft is correct for NONWAL=0 and NONWAL>0 alike,
# so NONWAL cannot change the handling and must not select the branch.
# ---------------------------------------------------------------------------
function Select-WalFoldBranch {
param([int]$K, [string]$Bottom)
if ($K -eq 0) { return 'noop' }
if ($Bottom -match '^wal:') { return 'reset-soft' }
return 'rebase-fixup'
}
# LEGACY rule (spec v2 :92). Kept ONLY as the regression oracle - never as the SUT
# unless -InjectLegacyRule is passed to prove this test has teeth.
function Select-WalFoldBranchLegacy {
param([int]$Nonwal)
if ($Nonwal -eq 0) { return 'reset-soft' }
return 'rebase-fixup'
}
function Get-FoldBranch {
param([int]$K, [string]$Bottom, [int]$Nonwal)
if ($InjectLegacyRule) { return (Select-WalFoldBranchLegacy -Nonwal $Nonwal) }
return (Select-WalFoldBranch -K $K -Bottom $Bottom)
}
# ---------------------------------------------------------------------------
# git helpers - every path derives from $WorkDir / $RepoRoot, none hardcoded.
# ---------------------------------------------------------------------------
$script:SeqEditor = $null
function New-SeqEditor {
param([string]$Dir)
# Sequence editor: keep the FIRST pick, turn every later pick into fixup.
# Written with LF + no BOM so the git todo parser sees clean lines.
$p = Join-Path $Dir 'wal-test-seq-editor.ps1'
$body = @(
'param($TodoPath)',
'$lines = Get-Content -LiteralPath $TodoPath',
'$seen = $false',
'$out = New-Object System.Collections.Generic.List[string]',
'foreach ($l in $lines) {',
' if ($l -match "^pick ") {',
' if ($seen) { $out.Add(($l -replace "^pick ", "fixup ")) }',
' else { $seen = $true; $out.Add($l) }',
' } else { $out.Add($l) }',
'}',
'[System.IO.File]::WriteAllText($TodoPath, (($out -join "`n") + "`n"))',
'exit 0'
) -join "`n"
[System.IO.File]::WriteAllText($p, $body)
return $p
}
function New-CaseTree {
param([string]$Path, [string[]]$Shape)
if (Test-Path $Path) { Remove-Item -Recurse -Force -LiteralPath $Path }
New-Item -ItemType Directory -Path $Path -Force | Out-Null
Push-Location $Path
git init -q .
git config user.email 'wal-test@local'
git config user.name 'wal-test'
git config commit.gpgsign false
git config core.autocrlf false
# Isolate from any inherited hook path: a stray hook must not edit these trees.
git config core.hooksPath (Join-Path $Path '.git/no-such-hooks')
[System.IO.File]::WriteAllText((Join-Path $Path 'base.txt'), "base`n")
git add -- base.txt
git commit -q -m '[CLAUDE] Infra: base'
# Fake origin/main as a LOCAL ref - no network, no real remote.
git update-ref refs/remotes/origin/main HEAD
$n = 0
foreach ($subject in $Shape) {
$n++
[System.IO.File]::WriteAllText((Join-Path $Path "f$n.txt"), "content $n`n")
git add -- "f$n.txt"
git commit -q -m $subject
}
Pop-Location
}
function Measure-Range {
param([string]$Path)
Push-Location $Path
$k = [int](git rev-list --count origin/main..HEAD)
$log = @(git log --format=%s origin/main..HEAD)
Pop-Location
$bottom = ''
if ($log.Count -gt 0) { $bottom = $log[-1] }
return [pscustomobject]@{
K = $k
Bottom = $bottom
Nonwal = @($log | Where-Object { -not ($_ -match '^wal:') }).Count
Nwal = @($log | Where-Object { $_ -match '^wal:' }).Count
Log = $log
}
}
function Get-RefSnapshot {
param([string]$Path)
Push-Location $Path
$s = @(git for-each-ref --format='%(refname) %(objectname)') -join '|'
$h = (git rev-parse HEAD)
Pop-Location
return ("HEAD=$h REFS=$s")
}
# Executes one fold branch on $Path. Returns what was MEASURED, asserts nothing.
function Invoke-FoldBranch {
param([string]$Path, [string]$Branch, [int]$K)
$r = [pscustomobject]@{
Branch = $Branch
RebaseInvoked = $false
RebaseExit = $null
ResetInvoked = $false
HeadKEqOrigin = $null
StateLeft = $false
Error = ''
}
Push-Location $Path
if ($Branch -eq 'noop') {
# ca0: deliberately do NOTHING. No reset, no rebase.
}
elseif ($Branch -eq 'reset-soft') {
# Safety assert BEFORE touching anything (v2 W0.3 gate).
$a = (git rev-parse "HEAD~$K")
$b = (git rev-parse origin/main)
$r.HeadKEqOrigin = ($a -eq $b)
if ($r.HeadKEqOrigin) {
$r.ResetInvoked = $true
git reset --soft "HEAD~$K" | Out-Null
git diff --cached --quiet
if ($LASTEXITCODE -ne 0) { git commit -q -m '[CLAUDE] Infra: fold re-commit' }
}
else {
$r.Error = 'ABORT: HEAD~K != origin/main'
}
}
elseif ($Branch -eq 'rebase-fixup') {
$env:GIT_SEQUENCE_EDITOR = ('powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + $script:SeqEditor + '"')
# Hang-guard: never let git open an interactive message editor.
$env:GIT_EDITOR = 'powershell.exe -NoProfile -Command exit 0'
$r.RebaseInvoked = $true
git rebase -i origin/main 2>$null | Out-Null
$r.RebaseExit = $LASTEXITCODE
if ($r.RebaseExit -ne 0) { git rebase --abort 2>$null | Out-Null }
$env:GIT_SEQUENCE_EDITOR = $null
$env:GIT_EDITOR = $null
}
# Rebase-state residue check after EVERY step, on every branch.
$r.StateLeft = ((Test-Path '.git/rebase-merge') -or (Test-Path '.git/rebase-apply'))
Pop-Location
return $r
}
# Runs a candidate branch on a FRESH tree and reports the measured outcome.
function Test-Candidate {
param([string]$Root, [string]$Id, [string[]]$Shape, [string]$Branch)
$p = Join-Path $Root $Id
New-CaseTree -Path $p -Shape $Shape
$pre = Measure-Range -Path $p
$exec = Invoke-FoldBranch -Path $p -Branch $Branch -K $pre.K
$post = Measure-Range -Path $p
$outcome = 'CLEAN'
if ($post.Nwal -gt 0) { $outcome = 'DEADLOCK' }
return [pscustomobject]@{
Branch = $Branch; Exec = $exec; Post = $post; Outcome = $outcome; Path = $p
}
}
# ---------------------------------------------------------------------------
# Bootstrap + containment
# ---------------------------------------------------------------------------
Write-Section 'setup + containment (RepoRoot must stay untouched)'
$stamp = (Get-Date -Format 'yyyyMMddTHHmmss')
$root = Join-Path $WorkDir ("wal-recovery-test-" + $stamp)
New-Item -ItemType Directory -Path $root -Force | Out-Null
$script:SeqEditor = New-SeqEditor -Dir $root
Write-Host (" RepoRoot = {0}" -f $RepoRoot)
Write-Host (" WorkDir = {0}" -f $root)
if ($InjectLegacyRule) {
Write-Host ' MODE = FAULT-INJECT (-InjectLegacyRule): SUT swapped to legacy NONWAL rule.' -ForegroundColor Magenta
Write-Host ' EXPECTED RESULT = ca2 FAIL + non-zero exit.' -ForegroundColor Magenta
}
# Containment (a) HARD ASSERT: every tree this script builds hangs off $root, so proving
# $root is outside RepoRoot proves the real .git is unreachable. Structural, not flaky.
$rrFull = ($RepoRoot -replace '/', '\').TrimEnd('\')
$wdFull = ($root -replace '/', '\')
Assert-That 'env' 'workdir-outside-repo' (-not $wdFull.StartsWith(($rrFull + '\'), [StringComparison]::OrdinalIgnoreCase)) `
("WorkDir not under RepoRoot -> real .git cannot be reached")
# Containment (b) INFO ONLY: snapshot the real repo, re-printed at the end. NOT asserted -
# see .PARAMETER RepoRoot: concurrent lanes / the WAL Stop-hook move these legitimately.
$repoIsGit = Test-Path (Join-Path $RepoRoot '.git')
$repoHeadBefore = 'n/a'
$repoDirtyBefore = -1
if ($repoIsGit) {
$repoHeadBefore = (git -C $RepoRoot rev-parse HEAD)
$repoDirtyBefore = @(git -C $RepoRoot status --porcelain).Count
Write-Host (" RepoRoot HEAD before = {0} ; porcelain entries = {1}" -f $repoHeadBefore, $repoDirtyBefore)
}
else {
Write-Host ' [note] RepoRoot is not a git repo - containment (b) reduced to path check only' -ForegroundColor DarkGray
}
# Structural anchor: the SUT must not be ABLE to branch on NONWAL.
$sutParams = @((Get-Command Select-WalFoldBranch).Parameters.Keys)
$hasNonwal = $false
foreach ($pn in $sutParams) { if ($pn -match '(?i)nonwal') { $hasNonwal = $true } }
Assert-That 'env' 'sut-has-no-NONWAL-param' (-not $hasNonwal) `
("Select-WalFoldBranch params = [{0}] (structural, via Get-Command - not a source grep)" -f ($sutParams -join ', '))
# ---------------------------------------------------------------------------
# The 4 cases. Shape = commits laid on top of base, BOTTOM-FIRST.
# ---------------------------------------------------------------------------
$WAL1 = 'wal: flush 1'
$WAL2 = 'wal: flush 2'
$REAL = '[CLAUDE] Infra: real work'
$cases = @(
[pscustomobject]@{ Id = 'ca0'; Shape = @(); Label = 'base'; SpecBranch = 'noop' },
[pscustomobject]@{ Id = 'ca1'; Shape = @($WAL1, $WAL2); Label = 'base|wal:|wal:'; SpecBranch = 'reset-soft' },
[pscustomobject]@{ Id = 'ca2'; Shape = @($WAL1, $REAL); Label = 'base|wal:|[CLAUDE]'; SpecBranch = 'reset-soft' },
[pscustomobject]@{ Id = 'ca3'; Shape = @($REAL, $WAL1); Label = 'base|[CLAUDE]|wal:'; SpecBranch = 'rebase-fixup' }
)
foreach ($c in $cases) {
$script:CaseFail[$c.Id] = $false
Write-Section ("{0} : {1}" -f $c.Id, $c.Label)
# --- 1. build the tree and MEASURE the branch inputs from real git ---
$main = Join-Path $root ($c.Id + '-main')
New-CaseTree -Path $main -Shape $c.Shape
$m = Measure-Range -Path $main
$script:CaseMeasure[$c.Id] = $m
Write-Host (" measured: K={0} NONWAL={1} BOTTOM=[{2}]" -f $m.K, $m.Nonwal, $m.Bottom)
foreach ($l in $m.Log) { Write-Host (" range: {0}" -f $l) -ForegroundColor DarkGray }
$sut = Get-FoldBranch -K $m.K -Bottom $m.Bottom -Nonwal $m.Nonwal
$legacy = Select-WalFoldBranchLegacy -Nonwal $m.Nonwal
Write-Host (" SUT picks = {0}" -f $sut)
Write-Host (" legacy rule = {0} (branches on NONWAL - spec v2 :92)" -f $legacy)
# --- 2. ORACLE: execute BOTH candidates on independent trees, measure outcome ---
$cfReset = Test-Candidate -Root $root -Id ($c.Id + '-cf-reset') -Shape $c.Shape -Branch 'reset-soft'
$cfRebase = Test-Candidate -Root $root -Id ($c.Id + '-cf-rebase') -Shape $c.Shape -Branch 'rebase-fixup'
Write-Host (" counterfactual reset-soft -> n(wal: in range)={0} -> {1}" -f $cfReset.Post.Nwal, $cfReset.Outcome)
Write-Host (" counterfactual rebase-fixup -> n(wal: in range)={0} -> {1} [rebase-exit={2}]" -f `
$cfRebase.Post.Nwal, $cfRebase.Outcome, $cfRebase.Exec.RebaseExit)
$clean = @()
if ($cfReset.Outcome -eq 'CLEAN') { $clean += 'reset-soft' }
if ($cfRebase.Outcome -eq 'CLEAN') { $clean += 'rebase-fixup' }
$oracleKind = 'DEFINITIONAL'
$expected = $c.SpecBranch
if ($c.Id -ne 'ca0' -and $clean.Count -eq 1) {
$oracleKind = 'EMPIRICAL'
$expected = $clean[0]
}
if ($oracleKind -eq 'EMPIRICAL') {
Write-Host (" ORACLE = EMPIRICAL: exactly one candidate survives the push-guard -> expected={0}" -f $expected) -ForegroundColor Yellow
}
else {
Write-Host (" ORACLE = DEFINITIONAL: both candidates outcome-equivalent on the n(wal:) metric") -ForegroundColor DarkGray
Write-Host (" -> expected={0} taken from spec v3-PATCH, NOT measured. Declared, not hidden." -f $expected) -ForegroundColor DarkGray
}
# --- 3. execute the SUT-chosen branch on the main tree ---
$snapBefore = Get-RefSnapshot -Path $main
$exec = Invoke-FoldBranch -Path $main -Branch $sut -K $m.K
$snapAfter = Get-RefSnapshot -Path $main
$post = Measure-Range -Path $main
# --- 4. asserts ---
Assert-That $c.Id 'branch-selection' ($sut -eq $expected) `
("SUT={0} expected={1} ({2} oracle)" -f $sut, $expected, $oracleKind)
Assert-That $c.Id 'no-rebase-state-left' (-not $exec.StateLeft) `
('.git/rebase-merge + .git/rebase-apply ABSENT after the step')
Assert-That $c.Id 'cf-no-rebase-state' ((-not $cfReset.Exec.StateLeft) -and (-not $cfRebase.Exec.StateLeft)) `
('.git/rebase-merge ABSENT on BOTH counterfactual trees too')
# REBASE-EXIT is never non-zero - on the main tree AND on the counterfactual that rebased.
$rebaseExits = @()
if ($null -ne $exec.RebaseExit) { $rebaseExits += $exec.RebaseExit }
if ($null -ne $cfRebase.Exec.RebaseExit) { $rebaseExits += $cfRebase.Exec.RebaseExit }
$badExit = @($rebaseExits | Where-Object { $_ -ne 0 }).Count
Assert-That $c.Id 'rebase-exit-never-nonzero' ($badExit -eq 0) `
("rebase exits observed = [{0}] ; non-zero count = {1}" -f (($rebaseExits | ForEach-Object { "$_" }) -join ','), $badExit)
if ($sut -eq 'reset-soft') {
Assert-That $c.Id 'headK-eq-origin-pre-reset' ($exec.HeadKEqOrigin -eq $true) `
("HEAD~K == origin/main asserted BEFORE reset (K={0})" -f $m.K)
}
if ($c.Id -eq 'ca0') {
Assert-That $c.Id 'noop-touched-nothing' ((-not $exec.ResetInvoked) -and (-not $exec.RebaseInvoked)) `
('no reset, no rebase issued on an empty range')
Assert-That $c.Id 'git-tree-identical' ($snapBefore -eq $snapAfter) `
('.git ref-state byte-identical before/after')
}
if ($c.Id -eq 'ca2') {
# v3-PATCH acceptance: ca2 must not touch git rebase AT ALL.
Assert-That $c.Id 'ca2-never-rebases' (-not $exec.RebaseInvoked) `
('git rebase NOT invoked on ca2')
}
if ($oracleKind -eq 'EMPIRICAL') {
Assert-That $c.Id 'post-state-clean' ($post.Nwal -eq 0) `
("n(wal: in origin/main..HEAD) after SUT branch = {0}" -f $post.Nwal)
}
$verdict = 'PASS'
if ($script:CaseFail[$c.Id]) { $verdict = 'FAIL' }
$script:Rows.Add([pscustomobject]@{
Case = $c.Id; Shape = $c.Label; K = $m.K; Nonwal = $m.Nonwal
Sut = $sut; Legacy = $legacy; Expected = $expected; Oracle = $oracleKind; Verdict = $verdict
})
}
# ---------------------------------------------------------------------------
# REGRESSION-GUARD - the actual value of this script.
# Not a restatement of the rule: both claims below are MEASURED this run.
# ---------------------------------------------------------------------------
Write-Section 'REGRESSION-GUARD (ca2) - legacy NONWAL rule must stay rejected'
$c2 = $cases | Where-Object { $_.Id -eq 'ca2' }
$g1 = Test-Candidate -Root $root -Id 'guard-ca2-legacy' -Shape $c2.Shape -Branch 'rebase-fixup'
# PRE-fold inputs of ca2, captured from real git in the loop above. Re-measuring
# ca2-main here would read the ALREADY-FOLDED tree (K=1, bottom = the re-commit) and
# silently ask the rule a different question than the one under test.
$m2 = $script:CaseMeasure['ca2']
Write-Host ' Replay of W0.3 4.2 - legacy branch (rebase+fixup) executed on a ca2 tree:'
Write-Host (" rebase-exit = {0} (0 = it SUCCEEDS, which is exactly the hazard)" -f $g1.Exec.RebaseExit)
Write-Host (" rebase-state left = {0}" -f $g1.Exec.StateLeft)
foreach ($l in $g1.Post.Log) { Write-Host (" after: {0}" -f $l) -ForegroundColor DarkGray }
Write-Host (" n(wal: in range) = {0}" -f $g1.Post.Nwal)
# Fixed-point probe: re-folding the deadlocked range never escapes it.
$g2 = $null
if ($g1.Post.Nwal -gt 0) {
$g2 = Invoke-FoldBranch -Path $g1.Path -Branch 'rebase-fixup' -K $g1.Post.K
$after2 = Measure-Range -Path $g1.Path
Write-Host (" re-squash (2nd fold) -> n(wal:)={0} [rebase-exit={1}] -> fixed point = PERMANENT STOP" -f `
$after2.Nwal, $g2.RebaseExit)
Assert-That 'grd' 'deadlock-is-fixed-point' ($after2.Nwal -gt 0) `
('re-squashing the legacy result still leaves a wal: commit -> push-guard blocks forever')
}
Assert-That 'grd' 'legacy-branch-deadlocks' (($g1.Exec.RebaseExit -eq 0) -and ($g1.Post.Nwal -gt 0)) `
("MEASURED: legacy branch exits 0 yet leaves n(wal:)={0} -> succeeds INCORRECTLY" -f $g1.Post.Nwal)
$legacyCa2 = Select-WalFoldBranchLegacy -Nonwal $m2.Nonwal
$sutCa2 = Get-FoldBranch -K $m2.K -Bottom $m2.Bottom -Nonwal $m2.Nonwal
Assert-That 'grd' 'legacy-picks-the-bad-one' ($legacyCa2 -eq 'rebase-fixup') `
("legacy(NONWAL={0}) = {1} = the branch measured to deadlock" -f $m2.Nonwal, $legacyCa2)
Assert-That 'grd' 'sut-rejects-legacy-branch' ($sutCa2 -ne 'rebase-fixup') `
("SUT = {0} != the measured-deadlock branch. Revert the rule to NONWAL and THIS assert fails." -f $sutCa2)
Assert-That 'grd' 'rules-diverge-at-ca2' ($sutCa2 -ne $legacyCa2) `
("SUT={0} vs legacy={1} - divergence is what makes this guard non-vacuous" -f $sutCa2, $legacyCa2)
# ---------------------------------------------------------------------------
# Result table
# ---------------------------------------------------------------------------
Write-Section 'RESULT'
Write-Host ('{0,-5} {1,-22} {2,-3} {3,-7} {4,-13} {5,-13} {6,-13} {7,-13} {8}' -f `
'CASE', 'SHAPE', 'K', 'NONWAL', 'SUT', 'LEGACY', 'EXPECTED', 'ORACLE', 'VERDICT')
Write-Host ('-' * 118)
foreach ($r in $script:Rows) {
$col = 'Green'
if ($r.Verdict -eq 'FAIL') { $col = 'Red' }
Write-Host ('{0,-5} {1,-22} {2,-3} {3,-7} {4,-13} {5,-13} {6,-13} {7,-13} {8}' -f `
$r.Case, $r.Shape, $r.K, $r.Nonwal, $r.Sut, $r.Legacy, $r.Expected, $r.Oracle, $r.Verdict) -ForegroundColor $col
}
$passCases = @($script:Rows | Where-Object { $_.Verdict -eq 'PASS' }).Count
$totalCases = $script:Rows.Count
# Containment (b) re-check - INFO, never a verdict input (concurrency-exogenous).
Write-Host ''
if ($repoIsGit) {
$repoHeadAfter = (git -C $RepoRoot rev-parse HEAD)
$repoDirtyAfter = @(git -C $RepoRoot status --porcelain).Count
$same = (($repoHeadAfter -eq $repoHeadBefore) -and ($repoDirtyAfter -eq $repoDirtyBefore))
Write-Host (" [info] RepoRoot HEAD {0} -> {1} ; porcelain {2} -> {3} ; unchanged={4}" -f `
$repoHeadBefore.Substring(0, 7), $repoHeadAfter.Substring(0, 7), $repoDirtyBefore, $repoDirtyAfter, $same)
if (-not $same) {
Write-Host ' [info] delta above is NOT a failure: containment is proven by workdir-outside-repo.' -ForegroundColor DarkGray
Write-Host ' A concurrent lane or the WAL Stop-hook can move HEAD/porcelain mid-run.' -ForegroundColor DarkGray
}
}
# ---------------------------------------------------------------------------
# Cleanup + exit. pass/fail COMPUTED from the counters - never a pasted label.
# ---------------------------------------------------------------------------
if ($KeepTemp) {
Write-Host ("temp trees KEPT at: {0}" -f $root) -ForegroundColor Yellow
}
else {
Set-Location $WorkDir
Remove-Item -Recurse -Force -LiteralPath $root -ErrorAction SilentlyContinue
Write-Host ("temp trees removed ({0}) - pass -KeepTemp to inspect" -f $root) -ForegroundColor DarkGray
}
Write-Host ''
Write-Host ("cases {0}/{1} PASS | asserts {2} run, {3} failed" -f $passCases, $totalCases, $script:AssertCount, $script:FailCount) -ForegroundColor Cyan
$exitCode = 0
if ($script:FailCount -gt 0) { $exitCode = 1 }
if ($passCases -ne $totalCases) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host 'VERDICT: PASS' -ForegroundColor Green
}
else {
Write-Host 'VERDICT: FAIL' -ForegroundColor Red
if ($InjectLegacyRule) {
Write-Host 'NOTE: -InjectLegacyRule was set. A FAIL here is the EXPECTED result: it proves the test has teeth.' -ForegroundColor Magenta
}
}
exit $exitCode