201 lines
14 KiB
PowerShell
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."
|