<#
.SYNOPSIS
  agent-frontmatter-eol-check.ps1 - HYGIENE-only EOL check for agent/command/skill frontmatter.
  NOT a fatal-defect guard. The "CRLF kills the agent registry" claim was REFUTED by experiment
  (S121 W0.6) and this script must not be read, cited, or gated on as if it enforced that claim.

.DESCRIPTION
  WHAT IT DOES
    Enforces the .gitattributes policy line '* text=auto eol=lf' (repo root) on the frontmatter
    of the rule-set files:
        .claude/agents/*.md
        .claude/commands/*.md
        .claude/skills/**/SKILL.md
    It reads RAW BYTES and counts CR (0x0D) inside the YAML frontmatter block.

  WHAT IT IS NOT - the honest grade of this check (read before trusting it)
    The S121 W0.6 spawn-probe MEASURED the opposite of the assumption that motivated this
    script. A fully CRLF-ised agent file (276 CR bytes) spawned CORRECTLY through all four
    stages: discovery / parse / spawn / execute. Token accounting was 45248 (LF) vs 45240
    (CRLF); that 8-token delta is explained by wording differences in the prompt, not by EOL.
    Verdict: CRLF-TOLERANT. The "LF-only frontmatter reader" hypothesis was REFUTED.
    => This is therefore HYGIENE, not defect-prevention. No CR finding produced here is known
       to break anything. Do not describe it as fatal. Do not gate a build on it.

  WHY IT STILL EXISTS - the scope limit of that refutation
    The W0.6 measurement covers: BEHAVIOUR level only / .claude/agents/ only / ONE win32 build.
    AXIS limit (S129, hub erratum 2026-07-16 4-cell matrix): W0.6 was an EOL-only A/B - the
    description-quote format was held constant - so it establishes CRLF-vs-LF spawn tolerance
    only, and says NOTHING about the description-colon-quote axis of the hub death-matrix.
    Do not cite it as evidence for or against quoting being load-bearing.
    NOT covered: .claude/commands/, .claude/skills/, hook .ps1 readers, any other build or
    platform. LF hygiene on those surfaces remains unverified-by-experiment, and the
    .gitattributes eol=lf policy applies to them regardless of whether any reader currently
    chokes. That residual - plus keeping the worktree consistent with the committed blob -
    is the entire value proposition. It is a small one. It is stated at its real size on purpose.

  SEVERITY LADDER (a deliberate consequence of the refutation)
    HIGH - reserved for SELF-BROKEN only: the synthetic control below failed, so the scan
           result is meaningless. Never used for a CR finding - none is known to break anything.
    MED  - CR inside the frontmatter block: the surface a parser would read, and the surface
           W0.6 probed on agents/ only.
    LOW  - CR in the body only: same eol=lf policy, further from any reader hot-path.
    Exit code is ALWAYS 0. Inform/hygiene net, never a hard gate.

  POSITIVE CONTROL - SYNTHETIC, and declared as such (spec v2 (4)(a))
    The scoped set measured 0 CR at S121, so a green run is the expected outcome. A green run
    therefore proves NOTHING by itself: a detector that is merely broken is also green. Every
    run self-tests the byte-reader against two IN-MEMORY buffers BEFORE touching disk:
        (+) CRLF synthetic agent file -> reader MUST report frontmatter CR > 0
        (-) LF   synthetic agent file -> reader MUST report frontmatter CR = 0
    These controls are synthetic BY NECESSITY, not by preference: because W0.6 refuted
    CRLF-fatality, no real "file killed by CRLF" exists to serve as a natural positive control.
    Note the limit of what a synthetic control buys: it proves the READER works. It does NOT
    prove that CRLF harms anything - the experiment says it does not.
    If either control fails, a HIGH self-broken FLAG is raised and the scan is reported
    UNTRUSTWORTHY.

  DESIGN NOTES
    - Byte-reader, not text-reader: Get-Content (default and -Raw) drops or normalises CR, so a
      text-level check could never fail on a CRLF file - it would be vacuous BY CONSTRUCTION.
      [System.IO.File]::ReadAllBytes is used instead. Side effect: no text decoding happens at
      all, so target-file encoding is irrelevant and gotcha #30 (ANSI-vs-UTF8 mojibake) cannot
      reach the measurement. This script body is still ASCII-only per gotcha #30 / #37.
    - No self-reference hazard (W0.4 lesson): the test is STRUCTURAL - count 0x0D inside a
      byte-delimited region - not a string-grep, so it cannot match a quoted claim written
      about itself. It also lives in scripts/ and scans only .claude/{agents,commands,skills},
      so it is path-disjoint from its own scan set as well.
    - Counts come from globs, never hardcoded (spec v3 fix #9h). Every PASS/FAIL/GREEN word
      printed below is COMPUTED from a measured number (W0.4 meta-count lesson: never print a
      verdict label next to a number that says otherwise).
    - agents/README.md is intentionally IN scope. R2-C1 excludes README from the ROSTER count
      (it is not an agent); EOL policy is per-file, so README is governed like any other file.
      The two rules differ on purpose - do not "fix" one by copying the other.
    - Scope is deliberately NOT repo-wide. A repo-wide 'i/lf w/crlf' scan matches 188 files
      (131 of them EF Migrations); an acceptance of "count == 0" over that set would be a
      false-price factory, and mass rm+checkout is forbidden this session (spec v2 W0.2).
      The 188 stay INFORM-only and are not this script's business.

.PARAMETER RepoRoot
  Repo root. Default = resolved 1 level up from this script (scripts/ -> repo root).
  Every path is derived from it, so the script can be pointed at a temp tree for fault-injection.

.EXAMPLE
  powershell.exe -ExecutionPolicy Bypass -File scripts/agent-frontmatter-eol-check.ps1

.EXAMPLE
  powershell.exe -ExecutionPolicy Bypass -File scripts/agent-frontmatter-eol-check.ps1 -RepoRoot C:\Temp\eol-faultinject
#>
param(
    [string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
)

$ErrorActionPreference = 'Continue'

# ---------------------------------------------------------------------------
# Helpers - shape mirrors scripts/governance-detectors.ps1:43-67 so FLAG output
# from the two nets reads the same way.
# ---------------------------------------------------------------------------
$script:FlagCount = 0

function Write-Flag {
    param(
        [ValidateSet('HIGH', 'MED', 'LOW')] [string]$Severity,
        [string]$Where,        # file:line, or file
        [string]$Desc,
        [string]$Resolve
    )
    $color = switch ($Severity) { 'HIGH' { 'Red' } 'MED' { 'Yellow' } default { 'Gray' } }
    Write-Host ("[EOL] {0,-4} | {1} | {2} | resolve: {3}" -f $Severity, $Where, $Desc, $Resolve) -ForegroundColor $color
    $script:FlagCount++
}

function Write-Section($title) {
    Write-Host ''
    Write-Host ("===== $title =====") -ForegroundColor Cyan
}

# Make a path repo-relative for readable FLAG output (forward slashes).
function Rel($full) {
    $r = $full
    if ($full.StartsWith($RepoRoot, [StringComparison]::OrdinalIgnoreCase)) {
        $r = $full.Substring($RepoRoot.Length).TrimStart('\', '/')
    }
    return ($r -replace '\\', '/')
}

# ---------------------------------------------------------------------------
# Byte-level frontmatter reader (the whole point - see DESIGN NOTES).
#
# Splits on 0x0A only, then strips one optional trailing 0x0D per line, so the
# frontmatter delimiters are located identically in LF and CRLF files. The
# returned offsets are BYTE offsets, which is what lets us attribute every single
# CR to either the frontmatter block or the body.
# ---------------------------------------------------------------------------
function Measure-EolBytes {
    param([byte[]]$Bytes)

    if ($null -eq $Bytes) { $Bytes = New-Object byte[] 0 }
    $len = $Bytes.Length

    # Skip a UTF-8 BOM so a BOM'd file does not silently read as "no frontmatter".
    $start = 0
    $hasBom = $false
    if ($len -ge 3 -and $Bytes[0] -eq 0xEF -and $Bytes[1] -eq 0xBB -and $Bytes[2] -eq 0xBF) {
        $hasBom = $true
        $start = 3
    }

    # Locate the frontmatter block: line 0 must be exactly '---'; the block ends at
    # the next line that is exactly '---'. $fmEnd = byte offset one past that line.
    $fmEnd = -1
    $lineStart = $start
    $lineIdx = 0
    for ($i = $start; $i -le $len; $i++) {
        $atEof = ($i -eq $len)
        if (-not $atEof -and $Bytes[$i] -ne 0x0A) { continue }

        $endExcl = $i
        if ($endExcl -gt $lineStart -and $Bytes[$endExcl - 1] -eq 0x0D) { $endExcl-- }

        # Byte -> char is Latin-1 here; harmless because the only comparison is the
        # ASCII delimiter '---'. Multi-byte UTF-8 in frontmatter cannot false-match it.
        $sb = New-Object System.Text.StringBuilder
        for ($k = $lineStart; $k -lt $endExcl; $k++) { [void]$sb.Append([char]$Bytes[$k]) }
        $content = $sb.ToString().TrimEnd()

        if ($lineIdx -eq 0) {
            if ($content -ne '---') { break }          # no frontmatter block at all
        }
        elseif ($content -eq '---') {
            $fmEnd = if ($atEof) { $len } else { $i + 1 }
            break
        }

        if ($atEof) { break }                          # opening '---' but never closed
        $lineIdx++
        $lineStart = $i + 1
    }

    # Attribute every CR byte. With no frontmatter block, the whole file counts as body.
    $split = if ($fmEnd -ge 0) { $fmEnd } else { $start }
    $fmCr = 0
    $bodyCr = 0
    for ($i = $start; $i -lt $len; $i++) {
        if ($Bytes[$i] -eq 0x0D) {
            if ($i -lt $split) { $fmCr++ } else { $bodyCr++ }
        }
    }

    return [pscustomobject]@{
        HasFrontmatter = ($fmEnd -ge 0)
        HasBom         = $hasBom
        FmCr           = $fmCr
        BodyCr         = $bodyCr
        TotalCr        = ($fmCr + $bodyCr)
        Bytes          = $len
    }
}

function Measure-EolFile {
    param([string]$Path)
    try {
        $bytes = [System.IO.File]::ReadAllBytes($Path)
    }
    catch {
        Write-Host ("  [warn] cannot read bytes: {0} - {1}" -f (Rel $Path), $_.Exception.Message) -ForegroundColor DarkGray
        return $null
    }
    return (Measure-EolBytes $bytes)
}

# ---------------------------------------------------------------------------
# SYNTHETIC CONTROL - runs BEFORE the disk scan.
# Declared synthetic on purpose: W0.6 refuted CRLF-fatality, so there is no real
# dead file to use as a natural positive control (spec v2 (4)(a)).
# ---------------------------------------------------------------------------
Write-Section 'Synthetic control (DECLARED SYNTHETIC) - does the byte-reader have teeth?'

$ctrlCrlf = "---`r`nname: _synthetic-control`r`ndescription: synthetic positive control`r`n---`r`nbody line`r`n"
$ctrlLf   = "---`nname: _synthetic-control`ndescription: synthetic negative control`n---`nbody line`n"

$ctrlPos = Measure-EolBytes ([System.Text.Encoding]::ASCII.GetBytes($ctrlCrlf))
$ctrlNeg = Measure-EolBytes ([System.Text.Encoding]::ASCII.GetBytes($ctrlLf))

$posOk = ($ctrlPos.HasFrontmatter -and $ctrlPos.FmCr -gt 0)
$negOk = ($ctrlNeg.HasFrontmatter -and $ctrlNeg.FmCr -eq 0 -and $ctrlNeg.BodyCr -eq 0)
$controlOk = ($posOk -and $negOk)

$posLabel = if ($posOk) { 'PASS' } else { 'FAIL' }
$negLabel = if ($negOk) { 'PASS' } else { 'FAIL' }

Write-Host ("  (+) CRLF synthetic : fm={0} FmCr={1} BodyCr={2}  expect fm=True,FmCr>0  -> {3}" -f `
        $ctrlPos.HasFrontmatter, $ctrlPos.FmCr, $ctrlPos.BodyCr, $posLabel)
Write-Host ("  (-) LF   synthetic : fm={0} FmCr={1} BodyCr={2}  expect fm=True,FmCr=0  -> {3}" -f `
        $ctrlNeg.HasFrontmatter, $ctrlNeg.FmCr, $ctrlNeg.BodyCr, $negLabel)

if (-not $controlOk) {
    Write-Flag 'HIGH' 'scripts/agent-frontmatter-eol-check.ps1 (self)' `
        'self-broken: synthetic control FAILED - the byte-reader does not detect CR, or false-positives on LF. Every scan result below is MEANINGLESS.' `
        'fix Measure-EolBytes; do NOT trust a green scan until both controls PASS'
}
else {
    Write-Host '  [OK] reader detects CRLF and stays silent on LF - a green scan below is at least meaningful' -ForegroundColor Green
}
Write-Host '  (reminder: the control proves the READER works. It does NOT prove CRLF harms anything - S121 W0.6 measured that it does not.)' -ForegroundColor DarkGray

# ---------------------------------------------------------------------------
# Scoped scan - .gitattributes 'eol=lf' hygiene over the rule-set files.
# Scope per spec v2 W0.2 + R2. NOT repo-wide (see DESIGN NOTES).
# ---------------------------------------------------------------------------
Write-Section 'Scoped scan - .gitattributes eol=lf hygiene (.claude/{agents,commands,skills})'

$scopeSpecs = @(
    [pscustomobject]@{ Name = '.claude/agents';   Dir = (Join-Path $RepoRoot '.claude\agents');   Filter = '*.md';     Recurse = $false },
    [pscustomobject]@{ Name = '.claude/commands'; Dir = (Join-Path $RepoRoot '.claude\commands'); Filter = '*.md';     Recurse = $false },
    [pscustomobject]@{ Name = '.claude/skills';   Dir = (Join-Path $RepoRoot '.claude\skills');   Filter = 'SKILL.md'; Recurse = $true }
)

$totalFiles = 0
$totalFmCrFiles = 0
$totalBodyCrFiles = 0
$totalNoFm = 0
$totalCrBytes = 0

foreach ($spec in $scopeSpecs) {
    if (-not (Test-Path $spec.Dir)) {
        Write-Host ("  [skip] {0,-18} not present under RepoRoot" -f $spec.Name) -ForegroundColor DarkGray
        continue
    }

    $files = @(Get-ChildItem -Path $spec.Dir -Filter $spec.Filter -File -Recurse:$spec.Recurse -ErrorAction SilentlyContinue)
    $scopeScanned = 0
    $scopeFmCr = 0
    $scopeBodyCr = 0
    $scopeCrBytes = 0

    foreach ($f in $files) {
        $m = Measure-EolFile $f.FullName
        if ($null -eq $m) { continue }

        $scopeScanned++
        $totalFiles++
        $scopeCrBytes += $m.TotalCr
        $totalCrBytes += $m.TotalCr
        if (-not $m.HasFrontmatter) { $totalNoFm++ }

        if ($m.FmCr -gt 0) {
            $scopeFmCr++
            $totalFmCrFiles++
            Write-Flag 'MED' (Rel $f.FullName) `
                ("eol-frontmatter-cr: {0} CR (0x0D) byte(s) inside the frontmatter block, {1} more in body - violates .gitattributes '* text=auto eol=lf'. HYGIENE only: S121 W0.6 measured a fully-CRLF agent file spawning correctly, so this is NOT a known breakage." -f $m.FmCr, $m.BodyCr) `
                'rewrite the file with LF endings (git rm --cached <f> + git checkout -- <f>, or re-save as LF)'
        }
        elseif ($m.BodyCr -gt 0) {
            $scopeBodyCr++
            $totalBodyCrFiles++
            Write-Flag 'LOW' (Rel $f.FullName) `
                ("eol-body-cr: frontmatter is clean but the body carries {0} CR (0x0D) byte(s) - same '* text=auto eol=lf' policy, further from any reader hot-path." -f $m.BodyCr) `
                'rewrite the file with LF endings (hygiene / worktree-vs-blob consistency)'
        }
    }

    Write-Host ("  {0,-18} scanned={1,-3} frontmatter-CR-files={2,-3} body-only-CR-files={3,-3} CR-bytes={4}" -f `
            $spec.Name, $scopeScanned, $scopeFmCr, $scopeBodyCr, $scopeCrBytes)
}

# ---------------------------------------------------------------------------
# Summary. Every label below is computed from a measured number (W0.4 lesson).
# ---------------------------------------------------------------------------
Write-Section 'Summary'

Write-Host ("scoped set (counted by glob, never hardcoded): {0} file(s)" -f $totalFiles)
Write-Host ("  files with CR in frontmatter : {0}" -f $totalFmCrFiles)
Write-Host ("  files with CR in body only   : {0}" -f $totalBodyCrFiles)
Write-Host ("  total CR (0x0D) bytes in set : {0}" -f $totalCrBytes)
Write-Host ("  files with no parseable frontmatter block: {0}  (not a flag - body CR still counted)" -f $totalNoFm)

Write-Host ''
Write-Host ("TOTAL FLAGS: {0}   (scoped files={1}, CR bytes measured={2})" -f $script:FlagCount, $totalFiles, $totalCrBytes) -ForegroundColor Cyan

if (-not $controlOk) {
    Write-Host 'RESULT: UNTRUSTWORTHY - the synthetic control FAILED, so the scan above proves nothing.' -ForegroundColor Red
}
elseif ($script:FlagCount -gt 0) {
    Write-Host ("RESULT: {0} hygiene FLAG(s) above. Advisory only - exit code stays 0, nothing is known to be broken." -f $script:FlagCount) -ForegroundColor Yellow
}
elseif ($totalCrBytes -eq 0) {
    Write-Host 'RESULT: GREEN-BUT-VACUOUS - 0 CR bytes existed in the scoped set, so this run had nothing to catch.' -ForegroundColor Yellow
    Write-Host '        Do NOT bank this as an achievement: the scoped gate was ALREADY green with 0 work to do' -ForegroundColor DarkGray
    Write-Host '        (spec v2 W0.2 / R2-C4). The only thing a green run demonstrates is that the reader itself' -ForegroundColor DarkGray
    Write-Host '        works (synthetic control PASSED above). Its forward value is regression-catching, not this run.' -ForegroundColor DarkGray
}
else {
    Write-Host 'RESULT: GREEN - CR bytes are present in the set but none in a flagged position.' -ForegroundColor Green
}

Write-Host 'NOTE: HYGIENE/INFORM net. Exit 0 always. CRLF-fatality was REFUTED by the S121 W0.6 spawn-probe (a fully-CRLF agent spawned OK through all 4 stages); this check enforces the .gitattributes eol=lf policy and nothing stronger.' -ForegroundColor DarkGray

exit 0
