scripts/internal/feature-claims.ps1

<#
.SYNOPSIS
    Feature-claim management for Specrew multi-session foundation (F-051 Iteration 2a).
 
.DESCRIPTION
    Maintains the committed, append-only-shared claims file `.squad/active-features.yml`
    (FR-012 through FR-016). Unlike the gitignored session lock, claims ARE committed - this
    is the cross-machine coordination surface (drift D-003). `claimed_by` carries only the
    coarse `user@machine` token (NOT the rich local fingerprint), per FR-043.
 
    Reuses the shared atomic-write, yaml-list, and time helpers. Corrupt/missing files
    degrade to empty. Dot-source to use.
#>


Set-StrictMode -Version Latest

. (Join-Path $PSScriptRoot 'atomic-write.ps1')
. (Join-Path $PSScriptRoot 'yaml-list.ps1')
. (Join-Path $PSScriptRoot 'specrew-time.ps1')

$script:SpecrewActiveFeaturesTopKey = 'claims'

function Get-FeatureClaimsPath {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)
    return (Join-Path $ProjectRoot '.squad/active-features.yml')
}

function Get-SpecrewCoarseIdentity {
    <# Coarse user@machine identity for a committed claim (no localhash; FR-043). #>
    return ('{0}@{1}' -f [System.Environment]::UserName, [System.Environment]::MachineName)
}

function Read-FeatureClaims {
    param([Parameter(Mandatory = $true)][string]$ProjectRoot)
    return @(Read-SpecrewYamlList -Path (Get-FeatureClaimsPath -ProjectRoot $ProjectRoot) -TopKey $script:SpecrewActiveFeaturesTopKey)
}

function Write-FeatureClaims {
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [AllowEmptyCollection()][AllowNull()][object[]]$Claims
    )
    $content = ConvertTo-SpecrewYamlList -TopKey $script:SpecrewActiveFeaturesTopKey -Entries $Claims
    Write-SpecrewFileAtomic -Path (Get-FeatureClaimsPath -ProjectRoot $ProjectRoot) -Content $content
}

function Add-FeatureClaim {
    <# Upsert a claim for a feature at the specify boundary; one claim per feature (FR-013). #>
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [Parameter(Mandatory = $true)][string]$FeatureId,
        [string]$ClaimedBy = (Get-SpecrewCoarseIdentity),
        [string]$BranchName = $FeatureId,
        [string]$NowUtc = (Get-SpecrewUtcNow)
    )
    $claims = @(Read-FeatureClaims -ProjectRoot $ProjectRoot)
    $existing = $claims | Where-Object { $_['feature_id'] -eq $FeatureId } | Select-Object -First 1
    if ($null -ne $existing) {
        $existing['last_refresh_time'] = $NowUtc   # upsert: refresh, do not duplicate
    }
    else {
        $claims = @($claims) + ,([ordered]@{
                feature_id        = $FeatureId
                claimed_by        = $ClaimedBy
                claim_start_time  = $NowUtc
                last_refresh_time = $NowUtc
                branch_name       = $BranchName
            })
    }
    Write-FeatureClaims -ProjectRoot $ProjectRoot -Claims $claims
}

function Update-FeatureClaim {
    <#
    Advance last_refresh_time monotonically at every boundary (FR-014, SC-008). If the claim is
    missing but ClaimedBy is supplied (an active session), re-add it (FR-014 reconciliation /
    manual-removal Edge Case). Missing + no ClaimedBy = no-op.
    #>

    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [Parameter(Mandatory = $true)][string]$FeatureId,
        [string]$ClaimedBy,
        [string]$BranchName = $FeatureId,
        [string]$NowUtc = (Get-SpecrewUtcNow)
    )
    $claims = @(Read-FeatureClaims -ProjectRoot $ProjectRoot)
    $existing = $claims | Where-Object { $_['feature_id'] -eq $FeatureId } | Select-Object -First 1
    if ($null -ne $existing) {
        $now = ConvertTo-SpecrewUtc -Value $NowUtc
        $cur = ConvertTo-SpecrewUtc -Value ([string]$existing['last_refresh_time'])
        if ($null -eq $cur -or ($null -ne $now -and $now -gt $cur)) {
            $existing['last_refresh_time'] = $NowUtc   # monotonic: only advance
        }
        Write-FeatureClaims -ProjectRoot $ProjectRoot -Claims $claims
    }
    elseif (-not [string]::IsNullOrEmpty($ClaimedBy)) {
        Add-FeatureClaim -ProjectRoot $ProjectRoot -FeatureId $FeatureId -ClaimedBy $ClaimedBy -BranchName $BranchName -NowUtc $NowUtc
    }
}

function Remove-FeatureClaim {
    <# Remove the claim for a feature (at feature-closeout when merged); no-op if absent (FR-016). #>
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [Parameter(Mandatory = $true)][string]$FeatureId
    )
    $claims = @(Read-FeatureClaims -ProjectRoot $ProjectRoot)
    $kept = @($claims | Where-Object { $_['feature_id'] -ne $FeatureId })
    if ($kept.Count -ne $claims.Count) {
        Write-FeatureClaims -ProjectRoot $ProjectRoot -Claims $kept
    }
}

function Test-FeatureClaimConflict {
    <# Return a claim on the same feature held by a DIFFERENT developer, else $null (FR-015). #>
    param(
        [Parameter(Mandatory = $true)][string]$ProjectRoot,
        [Parameter(Mandatory = $true)][string]$FeatureId,
        [string]$ClaimedBy = (Get-SpecrewCoarseIdentity)
    )
    $claims = @(Read-FeatureClaims -ProjectRoot $ProjectRoot)
    return ($claims | Where-Object { $_['feature_id'] -eq $FeatureId -and $_['claimed_by'] -ne $ClaimedBy } | Select-Object -First 1)
}