183 lines
7.1 KiB
PowerShell
183 lines
7.1 KiB
PowerShell
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
$ProgressPreference = "SilentlyContinue"
|
|
|
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
|
Set-Location -LiteralPath $repoRoot
|
|
|
|
$tempRoot = Join-Path $env:TEMP ("void-state-boundary-normalization-fixture-" + [guid]::NewGuid().ToString("n"))
|
|
$stateFilePath = Join-Path $tempRoot "void-self-state.cc"
|
|
|
|
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
|
|
|
function Invoke-NodeChecked {
|
|
param([Parameter(Mandatory = $true)][string[]] $Arguments)
|
|
|
|
$output = & node @Arguments
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Node command failed: node $($Arguments -join ' ')"
|
|
}
|
|
return $output
|
|
}
|
|
|
|
function Write-OperationFile {
|
|
param(
|
|
[Parameter(Mandatory = $true)][hashtable] $Operation,
|
|
[Parameter(Mandatory = $true)][string] $Name
|
|
)
|
|
|
|
$path = Join-Path $tempRoot $Name
|
|
$Operation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $path -Encoding UTF8
|
|
return $path
|
|
}
|
|
|
|
function Write-NodeScriptFile {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string] $Script,
|
|
[Parameter(Mandatory = $true)][string] $Name
|
|
)
|
|
|
|
$path = Join-Path $tempRoot $Name
|
|
Set-Content -LiteralPath $path -Value $Script -Encoding UTF8
|
|
return $path
|
|
}
|
|
|
|
function Assert-NotFutureTimestamp {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string] $Timestamp,
|
|
[Parameter(Mandatory = $true)][string] $Label
|
|
)
|
|
|
|
$parsed = [DateTimeOffset]::Parse($Timestamp)
|
|
$now = [DateTimeOffset]::UtcNow.AddSeconds(2)
|
|
if ($parsed -gt $now) {
|
|
throw "$Label is still in the future: $Timestamp"
|
|
}
|
|
}
|
|
|
|
try {
|
|
$futureMemory = @{
|
|
operation = "record_short_term_memory"
|
|
memory = @{
|
|
memoryId = "fixture-future-short-term"
|
|
kind = "project_seam"
|
|
target = @{ kind = "repo"; id = "VoidBot"; label = "VoidBot" }
|
|
summary = "Model-authored timestamps must not become canonical state time."
|
|
claim = "The parent state service owns canonical timestamps for model-originated writes."
|
|
tension = "If child timestamps are trusted, projection order can imply events that have not happened."
|
|
actionImplication = "Stamp short-term memory writes at the typed state boundary."
|
|
anchorRefs = @(
|
|
@{
|
|
ref = "fixture:future-memory"
|
|
kind = "fixture"
|
|
summary = "Future timestamp normalization fixture."
|
|
}
|
|
)
|
|
evidenceRefs = @()
|
|
createdAt = "2999-01-01T00:00:00.000Z"
|
|
updatedAt = "2999-01-01T00:00:00.000Z"
|
|
tags = @("repo:VoidBot", "topic:state-boundary")
|
|
}
|
|
}
|
|
|
|
$targetlessCandidate = @{
|
|
operation = "queue_candidate_intervention"
|
|
intervention = @{
|
|
interventionId = "fixture-targetless-queued"
|
|
kind = "self_advocacy"
|
|
status = "queued"
|
|
target = @{ kind = "system"; id = "typed-state-boundary"; label = "Typed state boundary" }
|
|
summary = "Queued candidate without a delivery target should not project as deliverable."
|
|
draft = "A queued candidate needs an actual delivery target, or it is theater with a clipboard."
|
|
priority = 0.5
|
|
mustEventuallyShare = $false
|
|
createdAt = "2999-01-01T00:00:00.000Z"
|
|
updatedAt = "2999-01-01T00:00:00.000Z"
|
|
tags = @("topic:state-boundary")
|
|
}
|
|
}
|
|
|
|
foreach ($entry in @(
|
|
@{ Operation = $futureMemory; Name = "future-memory.json" },
|
|
@{ Operation = $targetlessCandidate; Name = "targetless-candidate.json" }
|
|
)) {
|
|
$path = Write-OperationFile -Operation $entry.Operation -Name $entry.Name
|
|
Invoke-NodeChecked -Arguments @(".\scripts\void-self-state.mjs", "apply-operation", "--canonical", $stateFilePath, "--operation-file", $path) | Out-Null
|
|
}
|
|
|
|
$stateJson = Invoke-NodeChecked -Arguments @("-e", "const core=require('./packages/core/dist/index.js'); core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]}).then((state)=>console.log(JSON.stringify(state))).catch((error)=>{ console.error(error); process.exit(1); })", $stateFilePath)
|
|
$state = $stateJson | ConvertFrom-Json
|
|
$memory = @($state.thoughtMemory.shortTerm | Where-Object { $_.memoryId -eq "fixture-future-short-term" })[0]
|
|
$candidate = @($state.candidateInterventions.interventions | Where-Object { $_.interventionId -eq "fixture-targetless-queued" })[0]
|
|
$legacyFaceAffectScriptText = @'
|
|
const core = require(`${process.cwd()}/packages/core/dist/index.js`);
|
|
const legacy = {
|
|
schemaVersion: 1,
|
|
needs: [],
|
|
socialBonds: [],
|
|
userStatuses: [{
|
|
readId: "fixture-legacy-user-status",
|
|
target: { kind: "person", id: "metacrat", label: "Metacrat" },
|
|
status: "consulted",
|
|
summary: "Legacy userStatuses should decode as statusReads.",
|
|
claim: "The live authority is statusReads; userStatuses is only a migration alias.",
|
|
tension: "A stale persisted key should not keep the Face affect organ offline.",
|
|
actionImplication: "Decode legacy state and rewrite through the current strict schema.",
|
|
intensity: 0.6,
|
|
anchorRefs: [{ ref: "fixture:legacy-userStatuses", kind: "fixture" }],
|
|
evidenceRefs: [],
|
|
createdAt: "2026-06-08T00:00:00.000Z",
|
|
updatedAt: "2026-06-08T00:00:00.000Z",
|
|
tags: ["fixture:legacy"]
|
|
}],
|
|
moodDimensions: [],
|
|
socialBiases: [],
|
|
stressResponses: [{
|
|
name: "legacy-startle",
|
|
value: 0.4,
|
|
updatedAt: "2026-06-08T00:00:00.000Z"
|
|
}],
|
|
doctrineStances: [],
|
|
updatedAt: "2026-06-08T00:00:00.000Z"
|
|
};
|
|
console.log(JSON.stringify(core.voidFaceAffectSchema.parse(legacy)));
|
|
'@
|
|
$legacyFaceAffectScript = Write-NodeScriptFile -Name "legacy-face-affect.cjs" -Script $legacyFaceAffectScriptText
|
|
$legacyFaceAffectJson = Invoke-NodeChecked -Arguments @($legacyFaceAffectScript)
|
|
$legacyFaceAffect = $legacyFaceAffectJson | ConvertFrom-Json
|
|
|
|
if ($memory.createdAt -eq "2999-01-01T00:00:00.000Z" -or $memory.updatedAt -eq "2999-01-01T00:00:00.000Z") {
|
|
throw "Short-term memory kept model-authored future timestamps."
|
|
}
|
|
Assert-NotFutureTimestamp -Timestamp $memory.createdAt -Label "short-term createdAt"
|
|
Assert-NotFutureTimestamp -Timestamp $memory.updatedAt -Label "short-term updatedAt"
|
|
|
|
if ($candidate.status -ne "deferred") {
|
|
throw "Targetless queued candidate was not deferred; status is $($candidate.status)."
|
|
}
|
|
if (-not @($candidate.tags).Contains("normalized:targetless-queued-to-deferred")) {
|
|
throw "Targetless queued candidate was deferred without a normalization tag."
|
|
}
|
|
Assert-NotFutureTimestamp -Timestamp $candidate.createdAt -Label "candidate createdAt"
|
|
Assert-NotFutureTimestamp -Timestamp $candidate.updatedAt -Label "candidate updatedAt"
|
|
|
|
if (@($legacyFaceAffect.statusReads).Count -ne 1) {
|
|
throw "Legacy userStatuses did not decode into one statusReads entry."
|
|
}
|
|
if ($legacyFaceAffect.PSObject.Properties.Name -contains "userStatuses") {
|
|
throw "Legacy userStatuses survived parsed Face affect state."
|
|
}
|
|
if ($legacyFaceAffect.PSObject.Properties.Name -contains "stressResponses") {
|
|
throw "Legacy stressResponses survived parsed Face affect state."
|
|
}
|
|
|
|
@{
|
|
status = "ok"
|
|
shortTermMemoryId = $memory.memoryId
|
|
candidateStatus = $candidate.status
|
|
candidateTags = @($candidate.tags)
|
|
legacyFaceAffectStatusReads = @($legacyFaceAffect.statusReads).Count
|
|
} | ConvertTo-Json -Compress
|
|
} finally {
|
|
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|