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>
536 lines
24 KiB
PowerShell
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
|