Files
ARCADIA-Studio/harness/experiments/source-cli-independent-20260911/probe.ps1
T

201 lines
14 KiB
PowerShell

param(
[Parameter(Mandatory = $true)][string]$Repository,
[ValidatePattern('^[a-z0-9-]+$')][string]$RunName = 'verified'
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$repoRoot = [IO.Path]::GetFullPath($Repository)
$utf8 = [Text.UTF8Encoding]::new($false, $true)
$exe = Join-Path $repoRoot '.local/source-cli-20260911/verified/arcadia.exe'
$expectedExeHash = 'BAD169F8BE78DCFA74B32714569C886DD2132727080684FC2BA618D5822E3559'
$runRoot = Join-Path $repoRoot ('.local/source-cli-independent-20260911/' + $RunName)
if (Test-Path -LiteralPath $runRoot) { throw "Refusing to replace prior evidence: $runRoot" }
$checks = [Collections.Generic.List[string]]::new()
$operations = [Collections.Generic.List[object]]::new()
function Assert-That([bool]$condition, [string]$label) {
if (!$condition) { throw "Independent contract failed: $label" }
$checks.Add($label)
}
function Put([string]$path, [string]$value) {
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($path)) | Out-Null
[IO.File]::WriteAllText($path, $value, $utf8)
}
function Read-Exact([string]$path) { [IO.File]::ReadAllText($path, $utf8) }
function Text-Hash([string]$text) {
[Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($utf8.GetBytes($text)))
}
function Invoke-ArcadiaProbe([string[]]$words) {
$info = [Diagnostics.ProcessStartInfo]::new($exe)
$info.UseShellExecute = $false
$info.CreateNoWindow = $true
$info.RedirectStandardOutput = $true
$info.RedirectStandardError = $true
$info.StandardOutputEncoding = $utf8
$info.StandardErrorEncoding = $utf8
foreach ($word in $words) { $info.ArgumentList.Add($word) }
$info.ArgumentList.Add('--json')
$process = [Diagnostics.Process]::new()
$process.StartInfo = $info
$process.Start() | Out-Null
$output = $process.StandardOutput.ReadToEndAsync()
$errorText = $process.StandardError.ReadToEndAsync()
if (!$process.WaitForExit(60000)) {
$process.Kill($true)
throw 'CLI probe exceeded 60 seconds'
}
$raw = $output.GetAwaiter().GetResult()
$err = $errorText.GetAwaiter().GetResult()
$report = $raw | ConvertFrom-Json -Depth 100
Assert-That ($process.ExitCode -eq $report.exit_code) 'process exit agrees with JSON'
Assert-That ($err -ceq '') 'JSON invocation emits no mixed stderr'
$operations.Add([ordered]@{
Arguments = @($words | ForEach-Object { $_.Replace($runRoot, '<fixture>') })
Exit = $process.ExitCode
Status = $report.status
Changes = @($report.changes).Count
Diagnostics = @($report.diagnostics).Count
StdoutSHA256 = Text-Hash $raw
})
$process.Dispose()
return $report
}
Assert-That ((Get-FileHash -LiteralPath $exe).Hash -ceq $expectedExeHash) 'immutable CLI binary pin'
[IO.Directory]::CreateDirectory($runRoot) | Out-Null
# Predict every changed header/reference before the first mutating invocation.
# This fixture has explicit UUIDs for all seven Project::identities families.
$sourcePath = Join-Path $repoRoot 'harness/experiments/textual-authoring-20260911/observed/stabilized.arcadia'
$source = Read-Exact $sourcePath
$sourceHash = Text-Hash $source
$aliases = @(
[pscustomobject]@{ Family = 'project'; Kind = 'project'; Old = 'alarm_response'; New = 'alarm_suite'; References = 0 },
[pscustomobject]@{ Family = 'element'; Kind = 'oa_entity'; Old = 'maintenance'; New = 'maintenance_staff'; References = 1 },
[pscustomobject]@{ Family = 'relation'; Kind = 'interaction'; Old = 'alarm_message'; New = 'alarm_payload'; References = 1 },
[pscustomobject]@{ Family = 'diagram'; Kind = 'diagram'; Old = 'response_view'; New = 'response_map'; References = 2 },
[pscustomobject]@{ Family = 'placement'; Kind = 'node'; Old = 'detector_card'; New = 'detector_tile'; References = 0 },
[pscustomobject]@{ Family = 'artifact'; Kind = 'artifact'; Old = 'response_procedure'; New = 'response_handbook'; References = 0 },
[pscustomobject]@{ Family = 'trace'; Kind = 'trace'; Old = 'assessment_refinement'; New = 'assessment_alignment'; References = 0 }
)
$literal = ($aliases | ForEach-Object { '@' + $_.Old + ';' }) -join ' '
$source = $source.Replace('Numerical annotations are descriptive.', 'Numerical annotations are descriptive. Literal aliases: ' + $literal)
$source += "`r`n// Retain these comment references: $literal`r`n"
$expected = $source
$predictions = [Collections.Generic.List[object]]::new()
foreach ($case in $aliases) {
$header = $case.Kind + ' ' + $case.Old + ' '
Assert-That (([regex]::Matches($expected, [regex]::Escape($header))).Count -eq 1) ($case.Family + ' has one literal declaration header')
$next = $expected.Replace($header, $case.Kind + ' ' + $case.New + ' ')
# The known live references occupy these exact fields; prose/comments are excluded.
$field = switch ($case.Family) { 'element' { 'source' }; 'relation' { 'exchange' }; 'diagram' { 'diagram' }; default { $null } }
if ($null -ne $field) {
$oldReference = $field + ' @' + $case.Old + ';'
Assert-That (([regex]::Matches($next, [regex]::Escape($oldReference))).Count -eq $case.References) ($case.Family + ' live-reference count')
$next = $next.Replace($oldReference, $field + ' @' + $case.New + ';')
}
$predictions.Add([pscustomobject]@{ Case = $case; Before = $expected; After = $next })
$expected = $next
}
$aliasEntry = Join-Path $runRoot 'aliases/project.arcadia'
Put $aliasEntry $source
Put (Join-Path $PSScriptRoot 'alias-input.arcadia') $source
Put (Join-Path $PSScriptRoot 'alias-expected-final.arcadia') $expected
$baseline = Invoke-ArcadiaProbe @('check', $aliasEntry)
Assert-That ($baseline.exit_code -eq 0) 'all-family baseline is admitted before mutation'
Assert-That ($baseline.identities_to_stabilize -eq 0) 'all-family baseline has no missing identities'
Assert-That (([regex]::Matches($source, ' id "[0-9a-f-]{36}"')).Count -eq 40) 'all-family baseline contains exactly 40 authored UUIDs'
foreach ($prediction in $predictions) {
$case = $prediction.Case
$preview = Invoke-ArcadiaProbe @('rename-alias', $aliasEntry, $case.Old, $case.New)
Assert-That ($preview.exit_code -eq 0 -and !$preview.written -and $preview.status -ceq 'preview') ($case.Family + ' preview succeeds without write')
Assert-That ((Read-Exact $aliasEntry) -ceq $prediction.Before) ($case.Family + ' preview preserves exact input bytes')
Assert-That (@($preview.changes).Count -eq 1) ($case.Family + ' reports exactly one changed file')
Assert-That ($preview.changes[0].path -ceq 'project.arcadia') ($case.Family + ' preserves file ownership')
Assert-That ($preview.changes[0].before -ceq $prediction.Before) ($case.Family + ' reports exact captured text')
Assert-That ($preview.changes[0].after -ceq $prediction.After) ($case.Family + ' changes only independently predicted header/live-reference tokens')
Assert-That ($preview.identities_to_stabilize -eq 0 -and $null -eq $preview.migration) ($case.Family + ' invents no identity or migration')
$written = Invoke-ArcadiaProbe @('rename-alias', $aliasEntry, $case.Old, $case.New, '--write')
Assert-That ($written.exit_code -eq 0 -and $written.written -and $written.status -ceq 'written') ($case.Family + ' write succeeds')
Assert-That ($written.changes[0].after -ceq $prediction.After) ($case.Family + ' stable separate write invocation has exact preview text')
Assert-That ((Read-Exact $aliasEntry) -ceq $prediction.After) ($case.Family + ' persisted text equals independent expectation')
}
$afterCheck = Invoke-ArcadiaProbe @('check', $aliasEntry)
Assert-That ($afterCheck.exit_code -eq 0) 'all-family final source is freshly admitted'
$mtime = [IO.File]::GetLastWriteTimeUtc($aliasEntry)
$noop = Invoke-ArcadiaProbe @('stabilize', $aliasEntry, '--write')
Assert-That ($noop.exit_code -eq 0 -and $noop.status -ceq 'unchanged' -and !$noop.written -and @($noop.changes).Count -eq 0) 'stable all-family source is a true no-op'
Assert-That ([IO.File]::GetLastWriteTimeUtc($aliasEntry) -eq $mtime) 'no-op preserves source modification time'
Assert-That ((Read-Exact $aliasEntry) -ceq $expected) 'no-op preserves every source byte'
Assert-That (@(Get-ChildItem -LiteralPath (Split-Path -Parent $aliasEntry) -File -Recurse -Force).Count -eq 1) 'writes leave no unexpected physical file'
# Two warnings in one CRLF/Unicode include expose the shifted-second-span trap.
$rawEntry = Join-Path $runRoot 'raw/project.arcadia'
$workPath = Join-Path $runRoot 'raw/work.arcadia'
$entryText = 'arcadia 7; project diagnostic_case "Diagnostics" id "11000000-0000-4000-8000-000000000001" { include "work.arcadia"; }' + "`r`n"
$rawWork = "// Пролог 😀`r`n" + 'sa_function first "航海" {}' + "`r`n" + 'sa_function second "Deuxième" {}' + "`r`n"
Put $rawEntry $entryText
Put $workPath $rawWork
function Check-Warnings($report, [string]$rawText, [bool]$explicit) {
Assert-That ($report.exit_code -eq 0) 'partial unallocated functions remain valid'
$warnings = @($report.diagnostics | Where-Object { $_.code -ceq 'unallocated_leaf' })
Assert-That ($warnings.Count -eq 2) 'exactly two raw unallocated-leaf diagnostics without stabilized duplicates'
$index = 0
foreach ($alias in @('first', 'second')) {
$start = $utf8.GetByteCount($rawText.Substring(0, $rawText.IndexOf('sa_function ' + $alias, [StringComparison]::Ordinal)))
$warning = @($warnings | Where-Object { $_.location.start -eq $start })
Assert-That ($warning.Count -eq 1) ($alias + ' warning starts at original UTF-8 declaration offset')
Assert-That ($warning[0].coordinate_space -ceq 'input' -and $warning[0].location.path -ceq 'work.arcadia') ($alias + ' warning identifies raw include')
Assert-That ($warning[0].location.line -eq ($index + 2) -and $warning[0].location.column -eq 1) ($alias + ' CRLF/Unicode line and scalar column')
$bytes = $utf8.GetBytes($rawText)
$location = $warning[0].location
Assert-That ($location.end -gt $start -and $location.end -le $bytes.Length) ($alias + ' warning ends inside raw input')
$slice = $utf8.GetString($bytes, $start, $location.end - $start)
Assert-That ($slice.Contains('sa_function ' + $alias)) ($alias + ' raw diagnostic slice identifies intended declaration')
$expectedId = if ($explicit) { '11000000-0000-4000-8000-00000000000' + ($index + 2) } else { $null }
Assert-That ($warning[0].object_id -ceq $expectedId) ($alias + ' diagnostic identity has correct authored/provisional status')
$index++
}
}
$rawCheck = Invoke-ArcadiaProbe @('check', $rawEntry)
Check-Warnings $rawCheck $rawWork $false
$rawPreview = Invoke-ArcadiaProbe @('stabilize', $rawEntry)
Check-Warnings $rawPreview $rawWork $false
Assert-That ((Read-Exact $rawEntry) -ceq $entryText -and (Read-Exact $workPath) -ceq $rawWork) 'raw diagnostics and preview retain both exact source files'
Assert-That (@($rawPreview.changes).Count -eq 1 -and $rawPreview.changes[0].path -ceq 'work.arcadia') 'only missing-ID include is proposed for stabilization'
$candidateText = $rawPreview.changes[0].after
Assert-That ($utf8.GetByteCount($candidateText.Substring(0, $candidateText.IndexOf('sa_function second'))) -gt $utf8.GetByteCount($rawWork.Substring(0, $rawWork.IndexOf('sa_function second')))) 'fixture genuinely shifts second declaration in proposed candidate'
$authoredWork = $rawWork.Replace('first "航海" {', 'first "航海" id "11000000-0000-4000-8000-000000000002" {').Replace('second "Deuxième" {', 'second "Deuxième" id "11000000-0000-4000-8000-000000000003" {')
Put $workPath $authoredWork
$authoredCheck = Invoke-ArcadiaProbe @('lint', $rawEntry)
Check-Warnings $authoredCheck $authoredWork $true
Assert-That ((Read-Exact $workPath) -ceq $authoredWork) 'authored-ID lint is read-only'
# A non-BMP symbol before the bad reference distinguishes scalar columns from UTF-16 units.
$invalidWork = "// Пролог 😀`r`n" + 'sa_function invalid "Deuxième 🚨" { parent @absent; }' + "`r`n"
Put $workPath $invalidWork
$invalid = Invoke-ArcadiaProbe @('stabilize', $rawEntry, '--write')
Assert-That ($invalid.exit_code -eq 1 -and !$invalid.written -and @($invalid.changes).Count -eq 0) 'invalid raw Unicode source is refused before any candidate write'
$referenceIndex = $invalidWork.IndexOf('@absent', [StringComparison]::Ordinal)
$referenceStart = $utf8.GetByteCount($invalidWork.Substring(0, $referenceIndex))
$errorAt = @($invalid.diagnostics | Where-Object { $_.severity -ceq 'error' -and $_.location.start -eq $referenceStart })
Assert-That ($errorAt.Count -eq 1) 'unresolved reference uses exact raw UTF-8 byte start'
$linePrefix = $invalidWork.Substring($invalidWork.IndexOf("`n") + 1, $referenceIndex - $invalidWork.IndexOf("`n") - 1)
$scalarColumn = @($linePrefix.ToCharArray() | Where-Object { ![char]::IsLowSurrogate($_) }).Count + 1
Assert-That ($errorAt[0].location.line -eq 2 -and $errorAt[0].location.column -eq $scalarColumn) 'non-BMP prefix uses Unicode scalar column rather than UTF-16 units'
Assert-That ($errorAt[0].coordinate_space -ceq 'input' -and $null -eq $errorAt[0].object_id) 'invalid input exposes neither candidate coordinate nor ephemeral identity'
Assert-That ((Read-Exact $workPath) -ceq $invalidWork -and (Read-Exact $rawEntry) -ceq $entryText) 'invalid write retains complete input bundle bytes'
Assert-That ((Get-FileHash -LiteralPath $exe).Hash -ceq $expectedExeHash) 'CLI pin remains unchanged after probes'
$result = [ordered]@{
Reviewer = 'Codex-9e8r6t /root/behavior_audit'
RecordedUtc = [DateTime]::UtcNow.ToString('O')
BinarySHA256 = $expectedExeHash
InitialPublishedFixtureSHA256 = $sourceHash
AliasInputSHA256 = Text-Hash $source
AliasExpectedFinalSHA256 = Text-Hash $expected
CheckCount = $checks.Count
Checks = $checks.ToArray()
Operations = $operations.ToArray()
Limits = 'Independent CLI process/source-byte checks only; no production compile, full Project comparison, concurrency proof, Linux or native GUI claim.'
}
Put (Join-Path $PSScriptRoot 'result.json') ($result | ConvertTo-Json -Depth 100)
Write-Output "PASS: $($checks.Count) independent assertions across $($operations.Count) frozen CLI invocations."