Payload/scripts/kaden/harness/lib/HarnessGateProjection.psm1
|
#Requires -Version 5.1 <# .SYNOPSIS Rebuild schema 1.2 gate projection (.review-loop.json) from the journal. .DESCRIPTION Folds journal/events.jsonl into the review-loop vocabulary gates already speak (phases, slices, cycles, sensors, ownership). DNA kinds do not clear gates; OwnershipChanged cannot elevate to review_ready. Call Update-HarnessGateProjectionFromJournal -Write or harness-journal.ps1 -RebuildProjection. See docs/kaden/harness-journal.md / ADR 0003. #> Set-StrictMode -Version Latest function Get-HarnessGateProjectionSeed { param( [Parameter(Mandatory = $true)][string]$FeatureRoot, [Parameter(Mandatory = $true)][string]$Feature, [string]$ProfileName = 'standard', [string]$RepoRoot = '' ) $loopPath = Join-Path $FeatureRoot '.review-loop.json' if (Test-Path $loopPath) { $data = ConvertFrom-HarnessJson -Json (Get-Content $loopPath -Raw) return (Repair-HarnessReviewLoopShapes -LoopData $data) } if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = Get-HarnessRepoRoot -StartPath $FeatureRoot } $templatePath = Join-Path $RepoRoot '.github\templates\review-loop.json' if (-not (Test-Path $templatePath)) { throw "Review loop template not found: $templatePath" } $seed = ConvertFrom-HarnessJson -Json (Get-Content $templatePath -Raw) $seed.feature = $Feature $seed.profile = $ProfileName $seed.started = [DateTime]::UtcNow.ToString('yyyy-MM-dd') $seed.updated = Get-HarnessIso8601UtcNow $seed.current_phase = 'implementation' $seed.current_slice = $null return $seed } function Initialize-HarnessProjectionSlice { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'ProjectionSlice is the aggregate noun for a gate-projection slice shell.')] param( [Parameter(Mandatory = $true)]$LoopData, [Parameter(Mandatory = $true)][string]$SliceId, [string[]]$StoryIds = @() ) $impl = Get-ImplementationPhase -LoopData $LoopData if (-not $impl) { throw 'implementation phase missing from projection seed' } # Normalize collapsed one-element slices object -> array before lookup. $impl.slices = @(ConvertTo-HarnessObjectArray -Value $impl.slices) $slice = @($impl.slices) | Where-Object { (Get-HarnessProperty -Object $_ -Name 'id') -eq $SliceId } | Select-Object -First 1 if ($slice) { $LoopData.current_slice = $SliceId return $slice } # Adopt a corrupt shell (id null/empty, but has phases) instead of stacking duplicates. $orphan = @($impl.slices) | Where-Object { $candidate = $_ ($null -ne $candidate) ` -and ($candidate.PSObject.Properties.Name -contains 'phases') ` -and [string]::IsNullOrWhiteSpace([string](Get-HarnessProperty -Object $candidate -Name 'id')) } | Select-Object -First 1 if ($orphan) { $orphan.id = $SliceId if ([string]::IsNullOrWhiteSpace([string](Get-HarnessProperty -Object $orphan -Name 'owner'))) { $orphan.owner = 'atdd-developer' } if ([string]::IsNullOrWhiteSpace([string](Get-HarnessProperty -Object $orphan -Name 'ownership_status'))) { $orphan.ownership_status = 'in_progress' } if ($null -eq (Get-HarnessProperty -Object $orphan -Name 'story_ids')) { $orphan.story_ids = $StoryIds } $LoopData.current_slice = $SliceId $LoopData.current_phase = 'implementation' return $orphan } $phaseTemplate = @( @('RED', 'GREEN', 'MUTATE', 'REFACTOR', 'COMMIT') | ForEach-Object { [pscustomobject]@{ id = $_ status = 'pending' skip_reason_code = $null skip_rationale = $null evidence_artifact = $null git = $null completed_at = $null invocation = $null } } ) $newSlice = [pscustomobject]@{ id = $SliceId owner = 'atdd-developer' ownership_status = 'in_progress' story_ids = $StoryIds phases = $phaseTemplate cycles = @() sensors = @() handoffs = @() human_gate = [pscustomobject]@{ required = $true verdict = 'pending' actor = 'human' decided_at = $null notes = $null } } $list = [System.Collections.ArrayList]@() foreach ($s in @($impl.slices)) { if ($null -ne $s) { [void]$list.Add($s) } } [void]$list.Add($newSlice) $impl.slices = @(ConvertTo-HarnessObjectArray -Value $list.ToArray()) if ($impl.status -eq 'pending') { $impl.status = 'in_progress' } if (-not $impl.started_at) { $impl.started_at = Get-HarnessIso8601UtcNow } $LoopData.current_slice = $SliceId $LoopData.current_phase = 'implementation' return $newSlice } function ConvertTo-HarnessInvocationFromActor { param($Actor) if ($null -eq $Actor) { return $null } $model = Get-HarnessProperty -Object $Actor -Name 'model' return [pscustomobject]@{ agent = (Get-HarnessProperty -Object $Actor -Name 'agent' -Default 'unknown') model = [pscustomobject]@{ provider = (Get-HarnessProperty -Object $model -Name 'provider' -Default 'other') slug = (Get-HarnessProperty -Object $model -Name 'slug' -Default 'unknown') role = (Get-HarnessProperty -Object $model -Name 'role' -Default 'primary') } parent_agent = (Get-HarnessProperty -Object $Actor -Name 'parent_agent') tool = (Get-HarnessProperty -Object $Actor -Name 'tool') } } function Add-HarnessProjectionJournalItem { <# .SYNOPSIS Fold one journaled list item (review cycle, handoff) into a projection list, dedup by journal event id. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'In-memory projection fold - not a user-facing state cmdlet.')] param( [Parameter(Mandatory = $true)]$Target, [Parameter(Mandatory = $true)][ValidateSet('cycles', 'handoffs')][string]$ListProperty, [Parameter(Mandatory = $true)]$Item, [Parameter(Mandatory = $true)][string]$EventId ) $existingItems = @(Get-HarnessProperty -Object $Target -Name $ListProperty -Default @()) # Full-journal replay runs on every projection -Write; without a dedup key the # same single journal event gets re-appended on every later rebuild. $alreadyPresent = @($existingItems | Where-Object { (Get-HarnessProperty -Object $_ -Name 'journal_event_id') -eq $EventId }) if ($alreadyPresent.Count -gt 0) { return } if (-not ($Item.PSObject.Properties.Name -contains 'journal_event_id')) { $Item | Add-Member -NotePropertyName 'journal_event_id' -NotePropertyValue $EventId -Force } else { $Item.journal_event_id = $EventId } $list = [System.Collections.ArrayList]@() foreach ($existing in $existingItems) { [void]$list.Add($existing) } [void]$list.Add($Item) $Target | Add-Member -NotePropertyName $ListProperty -NotePropertyValue $list.ToArray() -Force } $script:VerdictGatedFeaturePhases = @{ # Map: feature phase -> sanctioned recorder that owns its completion. # A phase not in this map is not verdict-gated HERE; its completion folds per the # pre-existing feature-phase fold behavior (late-registration stamping still applies). 'spec-review' = 'record-review' } function Test-HarnessVerdictGatedFeaturePhaseCompletion { <# .SYNOPSIS True when a feature-phase completion row may fold for a verdict-gated phase. .DESCRIPTION Verdict-gated phases (see $script:VerdictGatedFeaturePhases) complete only through their sanctioned recorder AND only when the backing APPROVED cycle for the same verdict artifact is already folded. Manual journal rows, command impersonation, and forged cycles cannot complete the phase. #> [OutputType([bool])] param( $Payload, $Refs, [Parameter(Mandatory = $true)][string]$FeaturePhase, [Parameter(Mandatory = $true)][string]$Status, $PhaseRow ) if (-not $script:VerdictGatedFeaturePhases.ContainsKey($FeaturePhase)) { return $true } if ($Status -ne 'complete') { return $true } $recorder = [string]$script:VerdictGatedFeaturePhases[$FeaturePhase] if (-not (Test-HarnessSanctionedRecorderCommand -Refs $Refs -Recorder $recorder)) { return $false } $completionVerdictArtifact = [string](Get-HarnessProperty -Object $Payload -Name 'verdict_artifact') $backingCycles = @(Get-HarnessProperty -Object $PhaseRow -Name 'cycles' -Default @() | Where-Object { [string](Get-HarnessProperty -Object $_ -Name 'verdict') -eq 'APPROVED' -and [string](Get-HarnessProperty -Object $_ -Name 'verdict_artifact') -eq $completionVerdictArtifact }) return $backingCycles.Count -gt 0 } function Add-HarnessFeaturePhaseLateRegistrationStamp { <# .SYNOPSIS Stamp late-registration honesty fields onto a feature phase row (security F-3). .DESCRIPTION Defense in depth: fields stamp only for events recorded through the sanctioned phase recorder, not arbitrary journal rows. #> param( $PhaseRow, $Payload, $Refs ) if (-not (Test-HarnessSanctionedRecorderCommand -Refs $Refs -Recorder 'record-phase')) { return } $lateRegistration = [bool](Get-HarnessProperty -Object $Payload -Name 'late_registration') if (-not $lateRegistration) { return } $PhaseRow | Add-Member -NotePropertyName late_registration -NotePropertyValue $true -Force foreach ($lateKey in @('external_origin', 'occurred_around', 'context_note')) { $PhaseRow | Add-Member -NotePropertyName $lateKey -NotePropertyValue (Get-HarnessProperty -Object $Payload -Name $lateKey) -Force } } function Add-HarnessProjectionFeaturePhaseEvent { <# .SYNOPSIS Fold one feature-phase PhaseStatusChanged event into the gate projection. #> param( [Parameter(Mandatory = $true)]$LoopData, [Parameter(Mandatory = $true)]$Payload, [Parameter(Mandatory = $true)][string]$Status, # Untyped on purpose: PS 7.6+ ConvertFrom-Json yields [DateTime] for ISO timestamps, # and a [string] cast would culture-format it. Assign through to the projection. $OccurredAt, $Refs ) $fp = Get-HarnessLoopPhase -LoopData $LoopData -PhaseId ([string](Get-HarnessProperty -Object $Payload -Name 'feature_phase')) if (-not $fp) { return } if (-not (Test-HarnessVerdictGatedFeaturePhaseCompletion -Payload $Payload -Refs $Refs ` -FeaturePhase ([string](Get-HarnessProperty -Object $Payload -Name 'feature_phase')) -Status $Status -PhaseRow $fp)) { return } $fp.status = $Status if ($Status -eq 'in_progress' -and -not $fp.started_at) { $fp.started_at = $OccurredAt } if ($Status -eq 'complete') { $fp.completed_at = $OccurredAt $arts = Get-HarnessProperty -Object $Payload -Name 'artifacts' if ($arts) { $fp.artifacts = @($arts) } } Add-HarnessFeaturePhaseLateRegistrationStamp -PhaseRow $fp -Payload $Payload -Refs $Refs $LoopData.current_phase = [string](Get-HarnessProperty -Object $Payload -Name 'feature_phase') } function Add-HarnessProjectionSlicePhaseEvent { <# .SYNOPSIS Fold one slice-phase PhaseStatusChanged event into the gate projection. #> param( [Parameter(Mandatory = $true)]$LoopData, [Parameter(Mandatory = $true)][string]$SliceId, [Parameter(Mandatory = $true)]$Payload, [Parameter(Mandatory = $true)][string]$Status, # Untyped on purpose: PS 7.6+ ConvertFrom-Json yields [DateTime] for ISO timestamps, # and a [string] cast would culture-format it. Assign through to the projection. $OccurredAt, $Actor, $Refs ) $slice = Initialize-HarnessProjectionSlice -LoopData $LoopData -SliceId $SliceId $phaseId = [string](Get-HarnessProperty -Object $Payload -Name 'phase') $sp = $slice.phases | Where-Object { $_.id -eq $phaseId } | Select-Object -First 1 if ($sp) { $sp.status = $Status if ($Status -eq 'complete' -or $Status -eq 'skipped') { $sp.completed_at = $OccurredAt } else { $sp.completed_at = $null } $ev = Get-HarnessProperty -Object $Payload -Name 'evidence_artifact' if (-not $ev) { $ev = Get-HarnessProperty -Object $Refs -Name 'artifact' } if ($ev) { $sp | Add-Member -NotePropertyName evidence_artifact -NotePropertyValue $ev -Force } $skipCode = Get-HarnessProperty -Object $Payload -Name 'skip_reason_code' $skipRat = Get-HarnessProperty -Object $Payload -Name 'skip_rationale' if ($Status -eq 'skipped') { $sp | Add-Member -NotePropertyName skip_reason_code -NotePropertyValue $skipCode -Force $sp | Add-Member -NotePropertyName skip_rationale -NotePropertyValue $skipRat -Force } elseif ($Status -eq 'complete') { $sp | Add-Member -NotePropertyName skip_reason_code -NotePropertyValue $null -Force $sp | Add-Member -NotePropertyName skip_rationale -NotePropertyValue $null -Force } $inv = ConvertTo-HarnessInvocationFromActor -Actor $Actor if ($inv) { $sp | Add-Member -NotePropertyName invocation -NotePropertyValue $inv -Force } $testHashes = Get-HarnessProperty -Object $Payload -Name 'test_file_hashes' if ($null -ne $testHashes -and $Status -eq 'complete') { $sp | Add-Member -NotePropertyName test_file_hashes -NotePropertyValue $testHashes -Force } $testSnapshots = Get-HarnessProperty -Object $Payload -Name 'test_file_snapshots' if ($null -ne $testSnapshots -and $Status -eq 'complete') { $sp | Add-Member -NotePropertyName test_file_snapshots -NotePropertyValue $testSnapshots -Force } } $impl = Get-ImplementationPhase -LoopData $LoopData if ($impl -and $impl.status -eq 'pending') { $impl.status = 'in_progress' } if ($impl -and -not $impl.started_at) { $impl.started_at = $OccurredAt } $LoopData.current_phase = 'implementation' $LoopData.current_slice = $SliceId } function Register-HarnessProjectionSensorEntry { <# .SYNOPSIS Build, supersede, timestamp, and register one sensor row (SensorRunRecorded / GateCheckRun tail). #> param( [Parameter(Mandatory = $true)]$Slice, [Parameter(Mandatory = $true)]$Event, # Untyped on purpose: PS 7.6+ ConvertFrom-Json yields [DateTime] for ISO timestamps; # the entry timestamp is assigned through unchanged. $OccurredAt, [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Name, [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Type, [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Verdict, [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Artifact, [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Command, [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Summary, [Parameter(Mandatory = $true)][int]$ExitCode, [AllowEmptyString()][string]$SkipReasonCode = '', [AllowEmptyString()][string]$SkipRationale = '' ) $refs = Get-HarnessProperty -Object $Event -Name 'refs' $supersedesEventId = Get-HarnessProperty -Object $refs -Name 'supersedes_event_id' if ($supersedesEventId) { foreach ($existing in @($Slice.sensors | Where-Object { (Get-HarnessProperty -Object $_ -Name 'journal_event_id') -eq $supersedesEventId })) { $existing.authoritative = $false $existing.superseded_by_event_id = $Event.event_id } } $actor = Get-HarnessProperty -Object $Event -Name 'actor' $inv = ConvertTo-HarnessInvocationFromActor -Actor $actor if (-not $inv) { $inv = Format-HarnessModelInvocation -Agent 'harness-sensor.ps1' } $entry = Format-HarnessSensorLoopRecord -Invocation $inv -Name $Name -Type $Type -Verdict $Verdict ` -EvidenceArtifact $Artifact ` -EvidenceSummary $Summary -ExitCode $ExitCode -Command $Command ` -SkipReasonCode $SkipReasonCode ` -SkipRationale $SkipRationale ` -JournalEventId $Event.event_id ` -SupersedesEventId $supersedesEventId # Preserve occurred_at from journal for wall-clock fidelity in projection. $entry.timestamp = $OccurredAt Register-HarnessSensorEntry -Slice $Slice -SensorEntry $entry } function Add-HarnessJournalEventToProjection { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'In-memory projection fold - not a user-facing state cmdlet.')] param( [Parameter(Mandatory = $true)]$LoopData, [Parameter(Mandatory = $true)]$Event ) $kind = [string]$Event.kind $sliceId = Get-HarnessProperty -Object $Event -Name 'slice' $payload = Get-HarnessProperty -Object $Event -Name 'payload' $occurredAt = Get-HarnessProperty -Object $Event -Name 'occurred_at' -Default (Get-HarnessIso8601UtcNow) $actor = Get-HarnessProperty -Object $Event -Name 'actor' $refs = Get-HarnessProperty -Object $Event -Name 'refs' $outcome = Get-HarnessProperty -Object $Event -Name 'outcome' switch ($kind) { 'PhaseStatusChanged' { $phaseId = Get-HarnessProperty -Object $payload -Name 'phase' $status = Get-HarnessProperty -Object $payload -Name 'status' $featurePhase = Get-HarnessProperty -Object $payload -Name 'feature_phase' if ($featurePhase -and -not [string]::IsNullOrWhiteSpace((Get-HarnessJournalEventAmendsId -JournalEvent $Event))) { break } if ($featurePhase) { Add-HarnessProjectionFeaturePhaseEvent -LoopData $LoopData -Payload $payload -Status $status -OccurredAt $occurredAt -Refs $refs } elseif ($sliceId -and $phaseId -and $status) { Add-HarnessProjectionSlicePhaseEvent -LoopData $LoopData -SliceId $sliceId -Payload $payload -Status $status -OccurredAt $occurredAt -Actor $actor -Refs $refs } } 'SensorRunRecorded' { if (-not $sliceId) { break } $slice = Initialize-HarnessProjectionSlice -LoopData $LoopData -SliceId $sliceId $name = Get-HarnessProperty -Object $payload -Name 'name' $type = Get-HarnessProperty -Object $payload -Name 'type' -Default 'computational' $verdict = if ($outcome) { $outcome } else { Get-HarnessProperty -Object $payload -Name 'verdict' -Default 'FAIL' } $artifact = Get-HarnessProperty -Object $refs -Name 'artifact' if (-not $artifact) { $artifact = Get-HarnessProperty -Object $payload -Name 'evidence_artifact' } $command = Get-HarnessProperty -Object $refs -Name 'command' if (-not $command) { $command = Get-HarnessProperty -Object $payload -Name 'command' } $exitCode = Get-HarnessProperty -Object $payload -Name 'exit_code' -Default 0 $summary = Get-HarnessProperty -Object $Event -Name 'summary' -Default '' Register-HarnessProjectionSensorEntry -Slice $slice -Event $Event -OccurredAt $occurredAt ` -Name ([string]$name) -Type ([string]$type) -Verdict ([string]$verdict) ` -Artifact $(if ($artifact) { [string]$artifact } else { '' }) ` -Command $(if ($command) { [string]$command } else { '' }) ` -Summary ([string]$summary) -ExitCode ([int]$exitCode) ` -SkipReasonCode ([string](Get-HarnessProperty -Object $payload -Name 'skip_reason_code' -Default '')) ` -SkipRationale ([string](Get-HarnessProperty -Object $payload -Name 'skip_rationale' -Default '')) } 'GateCheckRun' { # Gate checks are also journaled as SensorRunRecorded by emitters; when only GateCheckRun # is present, project a validate-review-loop sensor row for completeness history. # Skip artifact-less rows (e.g. non-strict dogfood PASS) - they poison Strict sensor checks. if (-not $sliceId) { break } $artifact = Get-HarnessProperty -Object $refs -Name 'artifact' if (-not $artifact) { $artifact = Get-HarnessProperty -Object $payload -Name 'evidence_artifact' } if ([string]::IsNullOrWhiteSpace([string]$artifact)) { break } $slice = Initialize-HarnessProjectionSlice -LoopData $LoopData -SliceId $sliceId $verdict = if ($outcome) { $outcome } else { 'FAIL' } $command = Get-HarnessProperty -Object $refs -Name 'command' if (-not $command) { $command = '.\scripts\kaden\harness\validate-review-loop.ps1' } Register-HarnessProjectionSensorEntry -Slice $slice -Event $Event -OccurredAt $occurredAt ` -Name 'validate-review-loop' -Type 'computational' -Verdict ([string]$verdict) ` -Artifact ([string]$artifact) -Command ([string]$command) ` -Summary (Get-HarnessProperty -Object $Event -Name 'summary' -Default "gate check $verdict") ` -ExitCode $(if ($verdict -eq 'PASS') { 0 } else { 1 }) } 'OwnershipChanged' { if (-not $sliceId) { break } $slice = Initialize-HarnessProjectionSlice -LoopData $LoopData -SliceId $sliceId $status = Get-HarnessProperty -Object $payload -Name 'ownership_status' # Journal may advise non-clearance statuses only. review_ready stays evidence-gated # (validators / sensors / verdicts) - never forgeable from OwnershipChanged alone. $allowedFromJournal = @('in_progress', 'blocked', 'pending') if ($status -and ($allowedFromJournal -contains [string]$status)) { $slice.ownership_status = [string]$status } } 'StoryIdsAssigned' { if (-not $sliceId) { break } # Lookup only. Initialize-HarnessProjectionSlice also sets current_slice, which would # retarget the feature to whichever historical slice was backfilled last. $impl = Get-ImplementationPhase -LoopData $LoopData if (-not $impl) { break } $impl.slices = @(ConvertTo-HarnessObjectArray -Value $impl.slices) $slice = @($impl.slices) | Where-Object { (Get-HarnessProperty -Object $_ -Name 'id') -eq $sliceId } | Select-Object -First 1 if (-not $slice) { $slice = Initialize-HarnessProjectionSlice -LoopData $LoopData -SliceId $sliceId } $incoming = @(@(Get-HarnessProperty -Object $payload -Name 'story_ids') | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }) if ($incoming.Count -gt 0) { $existing = @(@($slice.story_ids) | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }) # Union, order-preserving, de-duplicated -- never drops an id a prior event assigned. $merged = [System.Collections.ArrayList]@() foreach ($id in (@($existing) + @($incoming))) { if (-not ($merged -contains [string]$id)) { [void]$merged.Add([string]$id) } } $slice.story_ids = @($merged.ToArray()) } } 'ReviewerCycleRecorded' { $cycle = Get-HarnessProperty -Object $payload -Name 'cycle' if (-not $cycle) { break } if ($sliceId) { $slice = Initialize-HarnessProjectionSlice -LoopData $LoopData -SliceId $sliceId Add-HarnessProjectionJournalItem -Target $slice -ListProperty 'cycles' -Item $cycle -EventId $Event.event_id } else { # Feature-level review cycle (spec-review): fold into the named # feature phase, but only from the sanctioned review recorder - a hand-authored # cycle row is manual compensation, not evidence. $featureCyclePhase = [string](Get-HarnessProperty -Object $cycle -Name 'feature_phase') if (-not $featureCyclePhase -or -not (Test-HarnessSanctionedRecorderCommand -Refs $refs -Recorder 'record-review')) { break } $featureCyclePhaseRow = Get-HarnessLoopPhase -LoopData $LoopData -PhaseId $featureCyclePhase if ($featureCyclePhaseRow) { Add-HarnessProjectionJournalItem -Target $featureCyclePhaseRow -ListProperty 'cycles' -Item $cycle -EventId $Event.event_id } } } 'HandoffRecorded' { if (-not $sliceId) { break } $slice = Initialize-HarnessProjectionSlice -LoopData $LoopData -SliceId $sliceId $handoff = Get-HarnessProperty -Object $payload -Name 'handoff' if ($handoff) { # Same replay-without-dedup hazard as ReviewerCycleRecorded above. Add-HarnessProjectionJournalItem -Target $slice -ListProperty 'handoffs' -Item $handoff -EventId $Event.event_id } } 'HumanGateDecided' { if (-not $sliceId) { break } $slice = Initialize-HarnessProjectionSlice -LoopData $LoopData -SliceId $sliceId if ($slice.human_gate) { $verdict = Get-HarnessProperty -Object $payload -Name 'verdict' if ($verdict) { $slice.human_gate.verdict = $verdict } $slice.human_gate.decided_at = $occurredAt $notes = Get-HarnessProperty -Object $payload -Name 'notes' if ($null -ne $notes) { $slice.human_gate.notes = $notes } } } 'BlockerDeclared' { if (-not $sliceId) { break } $slice = Initialize-HarnessProjectionSlice -LoopData $LoopData -SliceId $sliceId $slice.ownership_status = 'blocked' } 'GateValidationPassed' { # Emitted by harness-sensor.ps1 when validate-review-loop -RequireReviewReady passes. # This makes review_ready durable across projection rebuilds: any later -Append sensor # call that rebuilds from journal will restore review_ready from this event instead of # reverting to in_progress (which OwnershipChanged cannot elevate past blocked/pending). if (-not $sliceId) { break } $gate = Get-HarnessProperty -Object $payload -Name 'gate' $flags = Get-HarnessProperty -Object $payload -Name 'flags' $flagsArr = if ($flags) { @($flags) } else { @() } if ($gate -eq 'validate-review-loop' -and $outcome -eq 'PASS' -and ($flagsArr -contains 'RequireReviewReady')) { $slice = Initialize-HarnessProjectionSlice -LoopData $LoopData -SliceId $sliceId $slice.ownership_status = 'review_ready' } } default { # ActivityStarted / ActivityStopped / ActivityAdjusted / EmissionGapMarked: # DNA and audit only - do not clear gates from narrative. } } $LoopData.updated = Get-HarnessIso8601UtcNow } function Update-HarnessGateProjectionFromJournal { [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, [string]$ProfileName = '', [string]$RepoRoot = '', [switch]$Write ) if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = Get-HarnessRepoRoot -StartPath $FeatureRoot } if ([string]::IsNullOrWhiteSpace($ProfileName)) { $ProfileName = (Get-HarnessProfileRecord -FeatureRoot $FeatureRoot).profile } $seed = Get-HarnessGateProjectionSeed -FeatureRoot $FeatureRoot -Feature $Feature -ProfileName $ProfileName -RepoRoot $RepoRoot # Preserve existing slice scaffold ids (e.g. renamed journal-gate-skeleton) while folding events. $events = @(Get-HarnessJournalEvents -FeatureRoot $FeatureRoot) # Reset sensors/cycles/phase completions that journal will re-derive; keep slice shells. $impl = Get-ImplementationPhase -LoopData $seed if ($impl -and $impl.slices) { foreach ($slice in @($impl.slices)) { $slice.sensors = @() # Do not wipe cycles/handoffs that predate journal cutover unless journal has replacements. # Skeleton: wipe sensors always (journal SensorRunRecorded / GateCheckRun rebuild them). foreach ($sp in @($slice.phases)) { # Leave phase rows; PhaseStatusChanged overwrites. } } } foreach ($evt in $events) { Add-HarnessJournalEventToProjection -LoopData $seed -Event $evt } if ($Write) { $loopPath = Join-Path $FeatureRoot '.review-loop.json' Update-ReviewLoopDocument -LoopPath $loopPath -LoopData $seed -RepoRoot $RepoRoot } return $seed } function Save-HarnessGateProjection { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Non-interactive harness write - agents and hooks must not prompt.')] param( [Parameter(Mandatory = $true)][string]$FeatureRoot, [Parameter(Mandatory = $true)]$LoopData, [string]$RepoRoot = '' ) if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = Get-HarnessRepoRoot -StartPath $FeatureRoot } $loopPath = Join-Path $FeatureRoot '.review-loop.json' Update-ReviewLoopDocument -LoopPath $loopPath -LoopData $LoopData -RepoRoot $RepoRoot } Export-ModuleMember -Function * |