Payload/scripts/kaden/harness/lib/HarnessReportImmutability.ps1
|
# Report immutability: turn-open marker + registered gate-report write denial. # Dot-sourced into HarnessContract.psm1 so Export-ModuleMember -Function * keeps these helpers exported. # Do not Import-Module this file standalone without Contract helpers already loaded. function Get-HarnessTurnOpenMarkerPath { param([Parameter(Mandatory = $true)][string]$FeatureRoot) return Join-HarnessPath -Root $FeatureRoot -Child 'journal/turn-open.json' } function Get-HarnessTurnOpenMarkerDenyReason { return 'journal/turn-open.json is recorder-only. Turn scripts write and clear it; do not edit the marker with agent editor tools.' } function New-HarnessTurnOpenMarkerDocument { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an in-memory marker document - no persistent state change.')] param( [Parameter(Mandatory = $true)][string]$Feature, [Parameter(Mandatory = $true)][string]$Slice, [Parameter(Mandatory = $true)][int]$ActivitySeq, [string]$OpenedAt = '' ) $opened = if ([string]::IsNullOrWhiteSpace($OpenedAt)) { Get-HarnessIso8601UtcNow } else { $OpenedAt } return [pscustomobject]@{ schema = 'harness.turn-open/v1' feature = $Feature slice = $Slice activity_seq = $ActivitySeq opened_at = $opened } } function Test-HarnessTurnOpenMarkerDocument { <# .SYNOPSIS Pure schema check for harness.turn-open/v1 marker documents. #> param([AllowNull()]$Document) if ($null -eq $Document) { return $false } $schema = [string](Get-HarnessProperty -Object $Document -Name 'schema' -Default '') if ($schema -ne 'harness.turn-open/v1') { return $false } $feature = [string](Get-HarnessProperty -Object $Document -Name 'feature' -Default '') $slice = [string](Get-HarnessProperty -Object $Document -Name 'slice' -Default '') $seqRaw = Get-HarnessProperty -Object $Document -Name 'activity_seq' -Default $null $opened = [string](Get-HarnessProperty -Object $Document -Name 'opened_at' -Default '') if ([string]::IsNullOrWhiteSpace($feature) -or [string]::IsNullOrWhiteSpace($slice) -or [string]::IsNullOrWhiteSpace($opened)) { return $false } if ($null -eq $seqRaw) { return $false } try { [void][int]$seqRaw } catch { return $false } return $true } function Read-HarnessTurnOpenMarker { param([Parameter(Mandatory = $true)][string]$FeatureRoot) $path = Get-HarnessTurnOpenMarkerPath -FeatureRoot $FeatureRoot if (-not (Test-Path -LiteralPath $path)) { return $null } try { $raw = [System.IO.File]::ReadAllText($path) $doc = ConvertFrom-HarnessJson -Json $raw } catch { return $null } if (-not (Test-HarnessTurnOpenMarkerDocument -Document $doc)) { return $null } return $doc } function Write-HarnessTurnOpenMarker { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Non-interactive harness write - agents and hooks must not prompt.')] param( [Parameter(Mandatory = $true)][string]$FeatureRoot, [Parameter(Mandatory = $true)][string]$Feature, [Parameter(Mandatory = $true)][string]$Slice, [Parameter(Mandatory = $true)][int]$ActivitySeq, [string]$OpenedAt = '' ) $journalDir = Join-HarnessPath -Root $FeatureRoot -Child 'journal' if (-not (Test-Path -LiteralPath $journalDir)) { New-Item -ItemType Directory -Path $journalDir -Force | Out-Null } $doc = New-HarnessTurnOpenMarkerDocument -Feature $Feature -Slice $Slice -ActivitySeq $ActivitySeq -OpenedAt $OpenedAt $path = Get-HarnessTurnOpenMarkerPath -FeatureRoot $FeatureRoot $json = $doc | ConvertTo-Json -Compress -Depth 4 [System.IO.File]::WriteAllText($path, $json + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false)) return $path } function Clear-HarnessTurnOpenMarker { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Non-interactive harness write - agents and hooks must not prompt.')] param([Parameter(Mandatory = $true)][string]$FeatureRoot) $path = Get-HarnessTurnOpenMarkerPath -FeatureRoot $FeatureRoot if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force -ErrorAction Stop } } function Get-HarnessOpenTurnActivityStartedSeq { param( [Parameter(Mandatory = $true)][string]$FeatureRoot, [Parameter(Mandatory = $true)][string]$Slice ) if (-not (Get-Command Get-HarnessJournalEvents -ErrorAction SilentlyContinue)) { return $null } if (-not (Test-HarnessSliceTurnOpen -FeatureRoot $FeatureRoot -Slice $Slice)) { return $null } $events = @(Get-HarnessJournalEvents -FeatureRoot $FeatureRoot | Where-Object { [string]$_.slice -eq $Slice } | Sort-Object { [int](Get-HarnessProperty -Object $_ -Name 'seq' -Default 0) }) $latestStart = $null foreach ($evt in $events) { $kind = [string](Get-HarnessProperty -Object $evt -Name 'kind' -Default '') $activity = Get-HarnessProperty -Object $evt -Name 'activity' if ($kind -eq 'ActivityStarted' -and (Test-HarnessJournalTurnBoundaryActivity -Activity $activity -Name 'start')) { $latestStart = $evt } elseif ($kind -eq 'ActivityStopped' -and (Test-HarnessJournalTurnBoundaryActivity -Activity $activity -Name 'stop')) { $latestStart = $null } } if ($null -eq $latestStart) { return $null } return [int](Get-HarnessProperty -Object $latestStart -Name 'seq' -Default 0) } function Test-HarnessTurnOpenMarkerAgreesWithJournal { <# .SYNOPSIS True when a valid marker projects an open journal turn (same slice + activity_seq). Missing marker is not disagreement (legacy fixtures / mid-write); stale or mismatched marker fails closed. #> param( [Parameter(Mandatory = $true)][string]$FeatureRoot, [AllowNull()]$Marker = $null ) if ($null -eq $Marker) { $Marker = Read-HarnessTurnOpenMarker -FeatureRoot $FeatureRoot } if ($null -eq $Marker) { return $false } if (-not (Test-HarnessTurnOpenMarkerDocument -Document $Marker)) { return $false } $slice = [string]$Marker.slice $markerSeq = [int]$Marker.activity_seq if (-not (Test-HarnessSliceTurnOpen -FeatureRoot $FeatureRoot -Slice $slice)) { return $false } $journalSeq = Get-HarnessOpenTurnActivityStartedSeq -FeatureRoot $FeatureRoot -Slice $slice if ($null -eq $journalSeq) { return $false } return ($markerSeq -eq [int]$journalSeq) } function Test-HarnessFeatureOwnershipTurnOpenForEnforcement { <# .SYNOPSIS Whether write enforcement treats this feature root as having an open ownership turn. Marker present must agree with the journal (fail closed on disagreement). Unreadable or schema-invalid marker files also fail closed. Marker absent falls back to journal open-turn state (fixture / rollout compatibility). #> param([Parameter(Mandatory = $true)][string]$FeatureRoot) $markerPath = Get-HarnessTurnOpenMarkerPath -FeatureRoot $FeatureRoot if (Test-Path -LiteralPath $markerPath) { $marker = Read-HarnessTurnOpenMarker -FeatureRoot $FeatureRoot if ($null -eq $marker) { return $false } return (Test-HarnessTurnOpenMarkerAgreesWithJournal -FeatureRoot $FeatureRoot -Marker $marker) } $open = @(Get-HarnessOpenTurnSliceId -FeatureRoot $FeatureRoot) return ($open.Count -gt 0) } function Get-HarnessFeatureRootsWithJournal { <# .SYNOPSIS List feature roots under RepoRoot that have a journal ledger, optionally including FeatureRoot. #> param( [string]$RepoRoot = '', [string]$FeatureRoot = '' ) $roots = [System.Collections.Generic.List[string]]::new() if (-not [string]::IsNullOrWhiteSpace($FeatureRoot) -and (Test-Path -LiteralPath $FeatureRoot)) { [void]$roots.Add($FeatureRoot) } if (-not [string]::IsNullOrWhiteSpace($RepoRoot)) { $featuresRoot = Join-HarnessPath -Root $RepoRoot -Child 'docs/harness/features' if (Test-Path -LiteralPath $featuresRoot) { Get-ChildItem -LiteralPath $featuresRoot -Directory -ErrorAction SilentlyContinue | ForEach-Object { $journal = Join-HarnessPath -Root $_.FullName -Child 'journal/events.jsonl' if (Test-Path -LiteralPath $journal) { [void]$roots.Add($_.FullName) } } } } return @($roots | Select-Object -Unique) } function Test-HarnessAnyOwnershipTurnOpenForEnforcement { param( [string]$RepoRoot = '', [string]$FeatureRoot = '' ) # Fixture callers pass -FeatureRoot; do not mix in live RepoRoot journals. $roots = if (-not [string]::IsNullOrWhiteSpace($FeatureRoot) -and (Test-Path -LiteralPath $FeatureRoot)) { @($FeatureRoot) } else { @(Get-HarnessFeatureRootsWithJournal -RepoRoot $RepoRoot) } foreach ($root in $roots) { if (Test-HarnessFeatureOwnershipTurnOpenForEnforcement -FeatureRoot $root) { return $true } } return $false } function Get-HarnessFeatureRelativeReportPath { <# .SYNOPSIS Map a repo-relative edit path to feature slug + feature-relative path for report-tree classes. #> param( [AllowNull()][string]$Path, [string]$RepoRoot = '', [string]$FeatureRoot = '' ) $n = ConvertTo-HarnessNormalizedRepoPath -Path $Path -RepoRoot $RepoRoot if ([string]::IsNullOrWhiteSpace($n)) { return $null } if ($n -match '^docs/harness/features/([^/]+)/(.+)$') { return [pscustomobject]@{ FeatureSlug = $Matches[1] RelativePath = ($Matches[2] -replace '\\', '/') NormalizedPath = $n } } if (-not [string]::IsNullOrWhiteSpace($FeatureRoot) -and (Test-Path -LiteralPath $FeatureRoot)) { $rootFull = [System.IO.Path]::GetFullPath($FeatureRoot).TrimEnd('\', '/') $candidate = $Path if (-not [System.IO.Path]::IsPathRooted($candidate)) { if (-not [string]::IsNullOrWhiteSpace($RepoRoot)) { $candidate = Join-HarnessPath -Root $RepoRoot -Child ($n -replace '/', [System.IO.Path]::DirectorySeparatorChar) } else { $candidate = Join-HarnessPath -Root $FeatureRoot -Child ($n -replace '/', [System.IO.Path]::DirectorySeparatorChar) } } try { $candFull = [System.IO.Path]::GetFullPath($candidate) } catch { return $null } $prefix = $rootFull + [System.IO.Path]::DirectorySeparatorChar if ($candFull.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { $rel = $candFull.Substring($prefix.Length) -replace '\\', '/' $slug = Split-Path -Leaf $FeatureRoot return [pscustomobject]@{ FeatureSlug = $slug RelativePath = $rel NormalizedPath = ('docs/harness/features/' + $slug + '/' + $rel) } } } return $null } function Test-HarnessDecisionBookRelativePath { param([AllowNull()][string]$RelativePath) if ([string]::IsNullOrWhiteSpace($RelativePath)) { return $false } $rel = ($RelativePath -replace '\\', '/').TrimStart('/') return ($rel -eq 'quality/disposition-register.json' -or $rel -eq 'security/disposition-register.json' -or $rel -eq 'residuals.md') } function Test-HarnessDecisionBookPath { param( [AllowNull()][string]$Path, [string]$RepoRoot = '', [string]$FeatureRoot = '' ) $mapped = Get-HarnessFeatureRelativeReportPath -Path $Path -RepoRoot $RepoRoot -FeatureRoot $FeatureRoot if ($null -eq $mapped) { return $false } return (Test-HarnessDecisionBookRelativePath -RelativePath $mapped.RelativePath) } function Test-HarnessReportTreeRelativePath { <# .SYNOPSIS True for gate-report tree paths (quality/security/.reviews/slice-review narrative), excluding decision books. #> param([AllowNull()][string]$RelativePath) if ([string]::IsNullOrWhiteSpace($RelativePath)) { return $false } $rel = ($RelativePath -replace '\\', '/').TrimStart('/') if (Test-HarnessDecisionBookRelativePath -RelativePath $rel) { return $false } if ($rel -match '^(quality|security|\.reviews)/') { return $true } if ($rel -match '^slice-review-[^/]+\.md$') { return $true } return $false } function Test-HarnessReportTreeEditPath { param( [AllowNull()][string]$Path, [string]$RepoRoot = '', [string]$FeatureRoot = '' ) $mapped = Get-HarnessFeatureRelativeReportPath -Path $Path -RepoRoot $RepoRoot -FeatureRoot $FeatureRoot if ($null -eq $mapped) { return $false } return (Test-HarnessReportTreeRelativePath -RelativePath $mapped.RelativePath) } function Get-HarnessSliceReviewNarrativeSiblingRelativePath { <# .SYNOPSIS Discover slice-review narrative siblings for pair-freeze (FeatureRoot I/O by design). .DESCRIPTION Default enumerator walks FeatureRoot for dated and legacy narrative names. Pass -EnumerateFiles to inject names for unit tests without seeding disk. #> param( [Parameter(Mandatory = $true)][string]$FeatureRoot, [Parameter(Mandatory = $true)][string]$SliceToken, [scriptblock]$EnumerateFiles = $null ) $names = if ($null -ne $EnumerateFiles) { @(& $EnumerateFiles $FeatureRoot $SliceToken) } else { $discovered = [System.Collections.Generic.List[string]]::new() Get-ChildItem -LiteralPath $FeatureRoot -Filter ("slice-review-$SliceToken-*.md") -File -ErrorAction SilentlyContinue | ForEach-Object { [void]$discovered.Add($_.Name) } $legacyName = "slice-review-$SliceToken.md" $legacy = Join-Path $FeatureRoot $legacyName if (Test-Path -LiteralPath $legacy) { [void]$discovered.Add($legacyName) } @($discovered) } return @($names | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | ForEach-Object { [string]$_ }) } function Add-HarnessRegisteredArtifactCandidate { <# .SYNOPSIS Add one journal-harvested artifact path (and pair-freeze siblings) to a custody HashSet. #> param( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.HashSet[string]]$Paths, [Parameter(Mandatory = $true)][AllowEmptyString()][string]$RawArtifact, [Parameter(Mandatory = $true)][string]$FeatureRoot, [scriptblock]$EnumerateNarrativeSiblings = $null ) if ([string]::IsNullOrWhiteSpace($RawArtifact)) { return } $rel = ($RawArtifact -replace '\\', '/').TrimStart('/') if ($rel -match '^docs/harness/features/[^/]+/(.+)$') { $rel = $Matches[1] } if ([string]::IsNullOrWhiteSpace($rel)) { return } [void]$Paths.Add($rel) if ($rel -match '^quality/(.+)\.md$' -and $rel -notmatch '-data\.md$') { [void]$Paths.Add(('quality/' + $Matches[1] + '-data.json')) } if ($rel -match '^\.reviews/slice-verdict-(.+)\.yaml$') { $sliceToken = $Matches[1] $enumArgs = @{ FeatureRoot = $FeatureRoot SliceToken = $sliceToken } if ($null -ne $EnumerateNarrativeSiblings) { $enumArgs['EnumerateFiles'] = $EnumerateNarrativeSiblings } foreach ($sibling in @(Get-HarnessSliceReviewNarrativeSiblingRelativePath @enumArgs)) { [void]$Paths.Add($sibling) } } } function Get-HarnessRegisteredArtifactRelativePathSet { <# .SYNOPSIS Build the set of feature-relative paths under recorder custody for write denial. .DESCRIPTION Harvests artifact paths from journal rows. Pair freeze is applied via Add-HarnessRegisteredArtifactCandidate. #> param([Parameter(Mandatory = $true)][string]$FeatureRoot) $paths = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) if (-not (Get-Command Get-HarnessJournalEvents -ErrorAction SilentlyContinue)) { return , $paths } foreach ($evt in @(Get-HarnessJournalEvents -FeatureRoot $FeatureRoot)) { $candidates = [System.Collections.Generic.List[string]]::new() $refArt = [string](Get-HarnessProperty -Object (Get-HarnessProperty -Object $evt -Name 'refs') -Name 'artifact' -Default '') if (-not [string]::IsNullOrWhiteSpace($refArt)) { [void]$candidates.Add($refArt) } $payload = Get-HarnessProperty -Object $evt -Name 'payload' if ($null -ne $payload) { $pArt = [string](Get-HarnessProperty -Object $payload -Name 'artifact' -Default '') if (-not [string]::IsNullOrWhiteSpace($pArt)) { [void]$candidates.Add($pArt) } $vArt = [string](Get-HarnessProperty -Object $payload -Name 'verdict_artifact' -Default '') if (-not [string]::IsNullOrWhiteSpace($vArt)) { [void]$candidates.Add($vArt) } foreach ($a in @((Get-HarnessProperty -Object $payload -Name 'artifacts' -Default @()))) { if (-not [string]::IsNullOrWhiteSpace([string]$a)) { [void]$candidates.Add([string]$a) } } $cycle = Get-HarnessProperty -Object $payload -Name 'cycle' if ($null -ne $cycle) { $cArt = [string](Get-HarnessProperty -Object $cycle -Name 'verdict_artifact' -Default '') if ([string]::IsNullOrWhiteSpace($cArt)) { $cArt = [string](Get-HarnessProperty -Object $cycle -Name 'artifact' -Default '') } if (-not [string]::IsNullOrWhiteSpace($cArt)) { [void]$candidates.Add($cArt) } } } foreach ($raw in $candidates) { Add-HarnessRegisteredArtifactCandidate -Paths $paths -RawArtifact $raw -FeatureRoot $FeatureRoot } } return , $paths } function Test-HarnessRegisteredGateReportPath { param( [AllowNull()][string]$Path, [string]$RepoRoot = '', [string]$FeatureRoot = '' ) $mapped = Get-HarnessFeatureRelativeReportPath -Path $Path -RepoRoot $RepoRoot -FeatureRoot $FeatureRoot if ($null -eq $mapped) { return $false } if (-not (Test-HarnessReportTreeRelativePath -RelativePath $mapped.RelativePath)) { return $false } $root = $FeatureRoot if ([string]::IsNullOrWhiteSpace($root) -and -not [string]::IsNullOrWhiteSpace($RepoRoot)) { $root = Join-HarnessPath -Root $RepoRoot -Child ('docs/harness/features/' + $mapped.FeatureSlug) } if ([string]::IsNullOrWhiteSpace($root) -or -not (Test-Path -LiteralPath $root)) { return $false } $set = Get-HarnessRegisteredArtifactRelativePathSet -FeatureRoot $root if ($null -eq $set) { return $false } return [bool]$set.Contains(($mapped.RelativePath -replace '\\', '/')) } function Get-HarnessRegisteredReportDenyReason { return 'Registered gate reports are frozen after recorder custody (ADR-0015). Specialist re-issue: write a new report identity, then re-persist through the sanctioned recorder (harness-persist-quality / harness-record-review / harness-record-slice-verdict).' } function Test-HarnessShellCommandIsSanctionedIntegrityWriter { <# .SYNOPSIS True when the shell command is a harness-invoke of a sanctioned recorder/writer that may touch registered reports or gate-file-provenance.json. #> param([AllowNull()][string]$Command) if ([string]::IsNullOrWhiteSpace($Command)) { return $false } if (-not (Test-HarnessCommandStartsWithInvokeFrontDoor -Command $Command)) { return $false } return [bool]($Command -match '(?i)(?:harness-persist-quality|harness-record-review|harness-record-slice-verdict|harness-record-phase|harness-turn-start|harness-turn-stop|init-harness-feature)\.ps1\b') } function Test-HarnessShellCommandTargetsFrozenIntegrityPath { <# .SYNOPSIS True when a shell/beforeShellExecution command targets a registered gate report or journal/gate-file-provenance.json (dual-write launder vector F-2026-10-20-005). .DESCRIPTION Sanctioned harness-invoke recorders are excluded. Path match is substring-based on registered relative paths and the provenance stamp leaf. #> param( [AllowNull()][string]$Command, [string]$RepoRoot = '', [string]$FeatureRoot = '' ) if ([string]::IsNullOrWhiteSpace($Command)) { return $false } if (Test-HarnessShellCommandIsSanctionedIntegrityWriter -Command $Command) { return $false } if ($Command -match '(?i)(?:^|[\s''"`;/\\])(?:[\w./\\-]*[/\\])?journal[/\\]gate-file-provenance\.json\b') { return $true } $roots = [System.Collections.Generic.List[string]]::new() if (-not [string]::IsNullOrWhiteSpace($FeatureRoot) -and (Test-Path -LiteralPath $FeatureRoot)) { [void]$roots.Add($FeatureRoot) } elseif (-not [string]::IsNullOrWhiteSpace($RepoRoot)) { foreach ($r in @(Get-HarnessFeatureRootsWithJournal -RepoRoot $RepoRoot)) { [void]$roots.Add($r) } } $cmdNorm = $Command -replace '\\', '/' foreach ($root in $roots) { $set = Get-HarnessRegisteredArtifactRelativePathSet -FeatureRoot $root if ($null -eq $set -or $set.Count -eq 0) { continue } $slug = '' if (Get-Command Get-HarnessFeatureSlugFromRoot -ErrorAction SilentlyContinue) { $slug = [string](Get-HarnessFeatureSlugFromRoot -FeatureRoot $root) } foreach ($rel in @($set)) { $relNorm = ([string]$rel) -replace '\\', '/' if ([string]::IsNullOrWhiteSpace($relNorm)) { continue } if ($cmdNorm.IndexOf($relNorm, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) { return $true } if (-not [string]::IsNullOrWhiteSpace($slug)) { $prefixed = "docs/harness/features/$slug/$relNorm" if ($cmdNorm.IndexOf($prefixed, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) { return $true } } $full = (Join-HarnessPath -Root $root -Child $relNorm) -replace '\\', '/' if ($cmdNorm.IndexOf($full, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) { return $true } } } return $false } function Get-HarnessUnregisteredDraftDenyReason { param( [string]$Feature = '<slug>', [string]$Slice = '<id>' ) $start = Get-HarnessTurnStartInvokeCommand -Feature $Feature -Slice $Slice -Phase 'RED' return "Unregistered report-tree drafts require an open ownership turn. Start first: $start" } |