Payload/scripts/kaden/harness/harness-record-slice-verdict.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    Record a slice-review verdict artifact as journal truth and rebuild projection.

.DESCRIPTION
    Validates the slice-verdict YAML payload before it is written, writes the review
    artifact, and then records the slice-review feature phase through the existing
    harness-record-phase.ps1 path so the projection is rebuilt from journal truth.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$Feature,

    [Parameter(Mandatory = $true)]
    [string]$Slice,

    [Parameter(Mandatory = $true)]
    [string]$PayloadPath,

    [Parameter(Mandatory = $true)]
    [string]$VerdictArtifact,

    # Raw Task/subagent slice-reviewer YAML. Must exist under the feature root before recording.
    [Parameter(Mandatory = $true)]
    [string]$SourceArtifact,

    # Optional path to slice-review-<slice>-<date>.md; when omitted, validation still requires one on disk.
    [string]$NarrativeArtifact = '',

    [string]$FeatureRoot = '',

    [string]$RepoRoot = '',

    [ValidateSet('cursor', 'copilot', 'other')]
    [string]$ModelProvider = 'copilot',

    [string]$ModelSlug = 'unknown',

    [ValidateSet('primary', 'subagent')]
    [string]$ModelRole = 'primary',

    [string]$Agent = 'slice-reviewer',

    [string]$ParentAgent = '',

    [ValidateSet('cursor-task', 'copilot-agent', 'chat', 'harness-script')]
    [string]$Tool = 'harness-script',

    [string]$SourceInvocationId = '',

    [ValidateSet('host-id', 'artifact-attested')]
    [string]$SourceProvenance = 'host-id',

    [switch]$AllowStub,

    [switch]$AllowArbitraryFeatureRoot
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

Import-Module (Join-Path $PSScriptRoot 'lib\HarnessContract.psm1') -Force
Import-Module (Join-Path $PSScriptRoot 'lib\HarnessJournal.psm1') -Force
Import-Module (Join-Path $PSScriptRoot 'lib\HarnessGateProjection.psm1') -Force
Import-Module (Join-Path $PSScriptRoot 'lib\RepoRuntime.psm1') -Force
Test-HarnessContractModuleReady

if ([string]::IsNullOrWhiteSpace($RepoRoot)) {
    $RepoRoot = Get-HarnessRepoRoot -StartPath $PSScriptRoot
}

Initialize-RepoShellEnvironment -LogPrefix 'harness-record-slice-verdict' | Out-Null

$featureRootPath = Get-HarnessFeatureRoot -RepoRoot $RepoRoot -Feature $Feature -FeatureRootOverride $FeatureRoot -AllowArbitraryFeatureRoot:$AllowArbitraryFeatureRoot
if ($SourceProvenance -eq 'host-id' -and [string]::IsNullOrWhiteSpace($SourceInvocationId)) {
    throw 'slice-review with SourceProvenance host-id requires -SourceInvocationId.'
}
if ($SourceProvenance -eq 'artifact-attested' -and ($ModelProvider -ne 'copilot' -or -not [string]::IsNullOrWhiteSpace($SourceInvocationId))) {
    throw 'SourceProvenance artifact-attested is only valid for Copilot subagents without a host invocation id.'
}
Assert-HarnessPipelineSourceNotScratch -Path $NarrativeArtifact -RepoRoot $RepoRoot
if (-not (Test-Path -LiteralPath $PayloadPath)) {
    throw "Payload not found: $PayloadPath"
}
$payloadText = Get-Content -LiteralPath $PayloadPath -Raw
if ([string]::IsNullOrWhiteSpace($payloadText)) {
    throw 'Slice verdict payload is empty.'
}
Assert-HarnessSliceVerdictQualityGatesComplete -RepoRoot $RepoRoot -FeatureRoot $featureRootPath
$stampUtc = Get-HarnessIso8601UtcNow
$payloadText = Format-HarnessYamlTimestampField -Content $payloadText -Timestamp $stampUtc

# Raw source must exist (and live under .reviews/) before the canonical verdict is written.
$sourceFull = Resolve-HarnessPathUnderFeatureRoot -FeatureRoot $featureRootPath -RelativePath $SourceArtifact -ThrowOnEscape
if (-not (Test-Path -LiteralPath $sourceFull)) {
    throw "SourceArtifact not found: $SourceArtifact (save the slice-reviewer Task output verbatim under .reviews/ first)"
}
$sourceLeaf = Split-Path $sourceFull -Leaf
if ($sourceLeaf -notlike 'slice-reviewer-*') {
    throw "SourceArtifact leaf name must start with 'slice-reviewer-' (found '$sourceLeaf')"
}
$reviewsDir = Join-Path $featureRootPath '.reviews'
if (-not (Test-Path -LiteralPath $reviewsDir)) {
    New-Item -ItemType Directory -Path $reviewsDir -Force | Out-Null
}
$sourceUnderReviews = Join-Path $reviewsDir $sourceLeaf
if ($sourceFull -ne $sourceUnderReviews) {
    Copy-Item -LiteralPath $sourceFull -Destination $sourceUnderReviews -Force
}

if (-not [string]::IsNullOrWhiteSpace($NarrativeArtifact)) {
    $narrativeFull = Resolve-HarnessPathUnderFeatureRoot -FeatureRoot $featureRootPath -RelativePath $NarrativeArtifact -ThrowOnEscape
    if (-not (Test-Path -LiteralPath $narrativeFull)) {
        throw "NarrativeArtifact not found: $NarrativeArtifact"
    }
    $narrativeLeaf = Split-Path $narrativeFull -Leaf
    if ($narrativeLeaf -notmatch '^slice-review-') {
        throw "NarrativeArtifact leaf name must start with 'slice-review-' (found '$narrativeLeaf')"
    }
    $narrativeDest = Join-Path $featureRootPath $narrativeLeaf
    if ($narrativeFull -ne $narrativeDest) {
        Copy-Item -LiteralPath $narrativeFull -Destination $narrativeDest -Force
    }
}
elseif (-not (Find-HarnessSliceReviewNarrative -FeatureRoot $featureRootPath -SliceId $Slice)) {
    throw "Missing slice-review-$Slice-<YYYY-MM-DD>.md under feature root (or pass -NarrativeArtifact). YAML alone is not durable."
}

Assert-HarnessSliceReviewAllowlistOnDisk -FeatureRoot $featureRootPath -SliceId $Slice -VerdictArtifact $VerdictArtifact -AllowStub:$AllowStub

$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('harness-slice-verdict-validate-' + [guid]::NewGuid().ToString('N'))
$tempArtifact = Resolve-HarnessPathUnderFeatureRoot -FeatureRoot $tempRoot -RelativePath $VerdictArtifact -ThrowOnEscape
New-Item -ItemType Directory -Path (Split-Path $tempArtifact -Parent) -Force | Out-Null
[System.IO.File]::WriteAllText($tempArtifact, $payloadText, [System.Text.UTF8Encoding]::new($false))
# Temp root also needs the raw source + narrative so validation mirrors the real feature root.
$tempReviews = Join-Path $tempRoot '.reviews'
New-Item -ItemType Directory -Path $tempReviews -Force | Out-Null
Copy-Item -LiteralPath $sourceUnderReviews -Destination (Join-Path $tempReviews $sourceLeaf) -Force
$narrativeOnDisk = Find-HarnessSliceReviewNarrative -FeatureRoot $featureRootPath -SliceId $Slice
if ($narrativeOnDisk) {
    Copy-Item -LiteralPath $narrativeOnDisk.FullName -Destination (Join-Path $tempRoot $narrativeOnDisk.Name) -Force
}
elseif (-not [string]::IsNullOrWhiteSpace($NarrativeArtifact)) {
    $nFull = Resolve-HarnessPathUnderFeatureRoot -FeatureRoot $featureRootPath -RelativePath $NarrativeArtifact -ThrowOnEscape
    Copy-Item -LiteralPath $nFull -Destination (Join-Path $tempRoot (Split-Path $nFull -Leaf)) -Force
}
try {
    $findings = @(Invoke-HarnessSliceReviewVerdictValidation -FeatureRoot $tempRoot -SliceId $Slice -NotBeforeUtc '')
    if ($findings.Count -gt 0) {
        $messages = @($findings | ForEach-Object { $_.message })
        throw "Slice verdict payload failed validation: $($messages -join '; ')"
    }
}
finally {
    Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
}

$fullArtifactPath = Resolve-HarnessPathUnderFeatureRoot -FeatureRoot $featureRootPath -RelativePath $VerdictArtifact -ThrowOnEscape
New-Item -ItemType Directory -Path (Split-Path $fullArtifactPath -Parent) -Force | Out-Null
[System.IO.File]::WriteAllText($fullArtifactPath, $payloadText, [System.Text.UTF8Encoding]::new($false))

$resolvedSlug = Get-HarnessResolvedModelSlug -ModelSlug $ModelSlug
$actor = New-HarnessJournalActor -Agent $Agent -ParentAgent $(if ($ParentAgent) { $ParentAgent } else { $null }) `
    -Tool $Tool -Provider $ModelProvider -Slug $resolvedSlug -Role $ModelRole
$activityId = New-HarnessJournalActivityId
$activity = New-HarnessJournalActivity -Id $activityId -Type 'pipeline.phase' -Name 'slice-review' -Role 'instant'
Add-HarnessJournalEvent -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice `
    -Kind 'PhaseStatusChanged' -Activity $activity -Actor $actor -Outcome $null `
    -Summary 'feature phase slice-review status=complete' `
    -Payload ([pscustomobject]@{
        feature_phase         = 'slice-review'
        status                = 'complete'
        artifacts             = @($VerdictArtifact)
        source_provenance     = $SourceProvenance
        source_invocation_id  = $SourceInvocationId
    }) `
    -Refs ([pscustomobject]@{
        amends_event_id = $null
        pair_event_id   = $null
        artifact        = $VerdictArtifact
        command         = '.\scripts\kaden\harness-record-slice-verdict.ps1'
    }) | Out-Null

Update-HarnessGateProjectionFromJournal -FeatureRoot $featureRootPath -Feature $Feature -RepoRoot $RepoRoot -Write | Out-Null
Sync-HarnessOwnSliceNextArtifact -FeatureRoot $featureRootPath -Feature $Feature -Slice $Slice -RepoRoot $RepoRoot | Out-Null

Remove-HarnessStagedPayloadIfEligible -CandidatePath $PayloadPath -RepoRoot $RepoRoot -FeatureRoot $featureRootPath
Remove-HarnessStagedPayloadIfEligible -CandidatePath $sourceFull -RepoRoot $RepoRoot -FeatureRoot $featureRootPath

[pscustomobject]@{
    operation         = 'record-slice-verdict'
    feature           = $Feature
    slice             = $Slice
    artifact          = $VerdictArtifact
    canonical_path    = $fullArtifactPath
    status            = 'PASS'
} | ConvertTo-Json -Depth 10