scripts/internal/continuous-co-review/review-blackboard-writer.ps1
|
$ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest function ConvertTo-ContinuousCoReviewIsoTimestamp { param( [datetime] $Timestamp = [datetime]::UtcNow ) return $Timestamp.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ', [System.Globalization.CultureInfo]::InvariantCulture) } function Get-ContinuousCoReviewObjectProperty { param( [Parameter(Mandatory)] [AllowNull()] $Object, [Parameter(Mandatory)] [string] $Name ) if ($null -eq $Object) { return $null } if (Test-ReviewerContractPropertyExists -Object $Object -Name $Name) { return Get-ReviewerContractPropertyValue -Object $Object -Name $Name } return $null } function ConvertTo-ContinuousCoReviewDurableJson { param( [Parameter(Mandatory)] [AllowNull()] $InputObject ) return ($InputObject | ConvertTo-Json -Depth 100) } function Write-ContinuousCoReviewIdempotentJson { param( [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [AllowNull()] $InputObject ) $json = ConvertTo-ContinuousCoReviewDurableJson -InputObject $InputObject if (Test-Path -LiteralPath $Path -PathType Leaf) { $existingJson = Get-Content -LiteralPath $Path -Raw $existingCanonical = ConvertTo-ReviewerContractCanonicalJson -InputObject (ConvertFrom-ReviewerContractJson -Json $existingJson) $newCanonical = ConvertTo-ReviewerContractCanonicalJson -InputObject $InputObject if ($existingCanonical -ne $newCanonical) { throw "Durable review artifact already exists with different content: $Path" } return } $parent = Split-Path -Parent $Path New-Item -ItemType Directory -Path $parent -Force | Out-Null Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 -NoNewline } function New-ContinuousCoReviewDispositionFromFinding { param( [Parameter(Mandatory)] $Finding, [Parameter(Mandatory)] [string] $RecordedAt ) $findingId = Get-ContinuousCoReviewObjectProperty -Object $Finding -Name 'finding_id' $resolution = Get-ContinuousCoReviewObjectProperty -Object $Finding -Name 'resolution' return [pscustomobject][ordered]@{ disposition_id = "disposition-$findingId-open" finding_id = $findingId state = Get-ContinuousCoReviewObjectProperty -Object $Finding -Name 'disposition' rationale = Get-ContinuousCoReviewObjectProperty -Object $resolution -Name 'rationale' fix_evidence_ref = Get-ContinuousCoReviewObjectProperty -Object $resolution -Name 'fix_evidence_ref' review_round = 0 actor_role = 'reviewer' recorded_at = $RecordedAt } } function ConvertTo-ContinuousCoReviewDispositionRecord { param( [Parameter(Mandatory)] $Disposition, [Parameter(Mandatory)] [string] $RecordedAt ) $reviewRound = Get-ContinuousCoReviewObjectProperty -Object $Disposition -Name 'review_round' if ($null -eq $reviewRound) { $reviewRound = 0 } $recordedAt = Get-ContinuousCoReviewObjectProperty -Object $Disposition -Name 'recorded_at' if ([string]::IsNullOrWhiteSpace([string] $recordedAt)) { $recordedAt = $RecordedAt } return [pscustomobject][ordered]@{ disposition_id = Get-ContinuousCoReviewObjectProperty -Object $Disposition -Name 'disposition_id' finding_id = Get-ContinuousCoReviewObjectProperty -Object $Disposition -Name 'finding_id' state = Get-ContinuousCoReviewObjectProperty -Object $Disposition -Name 'state' rationale = Get-ContinuousCoReviewObjectProperty -Object $Disposition -Name 'rationale' fix_evidence_ref = Get-ContinuousCoReviewObjectProperty -Object $Disposition -Name 'fix_evidence_ref' review_round = [int] $reviewRound actor_role = Get-ContinuousCoReviewObjectProperty -Object $Disposition -Name 'actor_role' recorded_at = [string] $recordedAt } } function New-ContinuousCoReviewBlackboardThread { param( [Parameter(Mandatory)] [string] $CheckpointId, [Parameter(Mandatory)] $FindingsResult, [AllowNull()] $DispositionTrail, [AllowNull()] [string] $EscalationRef, [datetime] $CreatedAt = [datetime]::UtcNow ) $createdAtText = ConvertTo-ContinuousCoReviewIsoTimestamp -Timestamp $CreatedAt $runId = Get-ContinuousCoReviewObjectProperty -Object $FindingsResult -Name 'run_id' if ([string]::IsNullOrWhiteSpace([string] $runId)) { throw 'FindingsResult.run_id is required to write a review blackboard thread.' } $findingIds = @( foreach ($finding in @($FindingsResult.findings)) { Get-ContinuousCoReviewObjectProperty -Object $finding -Name 'finding_id' } ) $dispositions = @() if ($null -ne $DispositionTrail) { $dispositions = @( foreach ($disposition in @($DispositionTrail)) { ConvertTo-ContinuousCoReviewDispositionRecord -Disposition $disposition -RecordedAt $createdAtText } ) } if (@($dispositions).Count -eq 0) { $dispositions = @( foreach ($finding in @($FindingsResult.findings)) { New-ContinuousCoReviewDispositionFromFinding -Finding $finding -RecordedAt $createdAtText } ) } $summary = if (@($findingIds).Count -eq 0) { 'Inline reviewer returned no findings; blackboard thread records no-op result.' } else { "Inline reviewer returned $(@($findingIds).Count) finding(s); disposition trail persisted by orchestrator." } return [pscustomobject][ordered]@{ schema_version = '1.0' thread_id = "thread-$runId" run_id = $runId checkpoint_id = $CheckpointId findings = @($findingIds) dispositions = @($dispositions) resolution_summary = $summary escalation_ref = $EscalationRef created_at = $createdAtText updated_at = $createdAtText } } function New-ContinuousCoReviewRedactedEvidenceRecord { param( [Parameter(Mandatory)] [string] $RunId, [Parameter(Mandatory)] [string] $FindingsRef, [Parameter(Mandatory)] [string] $ThreadRef, [Parameter(Mandatory)] [string] $CreatedAt ) return [pscustomobject][ordered]@{ schema_version = '1.0' run_id = $RunId findings_result_ref = $FindingsRef review_thread_ref = $ThreadRef temporary_bundle_policy = 'not-persisted-by-blackboard-writer' raw_provider_transcripts = 'not-stored' raw_prompts = 'not-stored' secret_or_environment_capture = 'not-stored' created_at = $CreatedAt } } function Assert-ContinuousCoReviewBlackboardNoDrift { param( [Parameter(Mandatory)] [string] $RunRoot, [Parameter(Mandatory)] $FindingsResult, [Parameter(Mandatory)] $ReviewThread ) $existingFindingsPath = Join-Path $RunRoot 'findings-result.json' if (Test-Path -LiteralPath $existingFindingsPath -PathType Leaf) { $existingFindings = ConvertFrom-ReviewerContractJson -Json (Get-Content -LiteralPath $existingFindingsPath -Raw) if ((ConvertTo-ReviewerContractCanonicalJson -InputObject $existingFindings) -ne (ConvertTo-ReviewerContractCanonicalJson -InputObject $FindingsResult)) { throw "Durable review artifact already exists with different content: $existingFindingsPath" } } $existingThreadPath = Join-Path $RunRoot 'review-thread.json' if (Test-Path -LiteralPath $existingThreadPath -PathType Leaf) { $existingThread = ConvertFrom-ReviewerContractJson -Json (Get-Content -LiteralPath $existingThreadPath -Raw) if ((ConvertTo-ReviewerContractCanonicalJson -InputObject $existingThread) -ne (ConvertTo-ReviewerContractCanonicalJson -InputObject $ReviewThread)) { throw "Durable review artifact already exists with different content: $existingThreadPath" } } } function Write-ContinuousCoReviewBlackboardThread { param( [Parameter(Mandatory)] [string] $RepoRoot, [Parameter(Mandatory)] [string] $CheckpointId, [Parameter(Mandatory)] $FindingsResult, [AllowNull()] $DispositionTrail, [AllowNull()] [string] $EscalationRef, [string] $SchemaRoot, [datetime] $CreatedAt = [datetime]::UtcNow ) $resolvedRepoRoot = (Resolve-Path -LiteralPath $RepoRoot).Path if ($SchemaRoot) { Assert-ReviewerContractObject -ContractName 'FindingsResult' -SchemaRoot $SchemaRoot -InputObject $FindingsResult | Out-Null } $thread = New-ContinuousCoReviewBlackboardThread ` -CheckpointId $CheckpointId ` -FindingsResult $FindingsResult ` -DispositionTrail $DispositionTrail ` -EscalationRef $EscalationRef ` -CreatedAt $CreatedAt if ($SchemaRoot) { Assert-ReviewerContractObject -ContractName 'ReviewThread' -SchemaRoot $SchemaRoot -InputObject $thread | Out-Null } $runId = $FindingsResult.run_id $runRoot = Join-Path $resolvedRepoRoot ".specrew/review/inline/$runId" New-Item -ItemType Directory -Path $runRoot -Force | Out-Null Assert-ContinuousCoReviewBlackboardNoDrift -RunRoot $runRoot -FindingsResult $FindingsResult -ReviewThread $thread $findingsPath = Join-Path $runRoot 'findings-result.json' $threadPath = Join-Path $runRoot 'review-thread.json' $evidencePath = Join-Path $runRoot 'redacted-evidence.json' Write-ContinuousCoReviewIdempotentJson -Path $findingsPath -InputObject $FindingsResult Write-ContinuousCoReviewIdempotentJson -Path $threadPath -InputObject $thread $createdAtText = ConvertTo-ContinuousCoReviewIsoTimestamp -Timestamp $CreatedAt $redactedEvidence = New-ContinuousCoReviewRedactedEvidenceRecord ` -RunId $runId ` -FindingsRef 'findings-result.json' ` -ThreadRef 'review-thread.json' ` -CreatedAt $createdAtText Write-ContinuousCoReviewIdempotentJson -Path $evidencePath -InputObject $redactedEvidence return [pscustomobject][ordered]@{ schema_version = '1.0' run_id = $runId checkpoint_id = $CheckpointId blackboard_root = $runRoot findings_result_path = $findingsPath review_thread_path = $threadPath redacted_evidence_path = $evidencePath review_thread = $thread } } |