From 8aca602497650bd9c295f7c250941006a7bc1fb1 Mon Sep 17 00:00:00 2001 From: Ben Richards Date: Mon, 24 Aug 2026 18:45:55 +0300 Subject: [PATCH 1/2] =?UTF-8?q?S6=20=E2=80=94=20Everything=20the=20game=20?= =?UTF-8?q?keeps=20in=20state=20is=20counted,=20and=20told=20what=20it=20m?= =?UTF-8?q?ust=20say?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Derives the concept set from GameState's 17 fields and the field-type declarations they reach; the 14 non-primitive types found there (Opportunity, ScheduledEvent, StatusEffect, GoalState, PlayerState, and nine others) are the state-bearing concepts requiring a lifecycle- region. ResolutionDebugInfo correctly falls outside the set, since it never travels through GameState. Wraps §5.4.1 and §5.4.2 as lifecycle-Opportunity and lifecycle-ScheduledEvent, both held. The concept check counts a region as stating both boundaries of a lifecycle when its body carries at least two single-word bold-period labels ("**Generation.**", "**Resolution.**") — a structural test, not a word list, so it needs no vocabulary of which words mean creation versus retirement. The gate lands red on purpose: 12 of the 14 derived concepts have no lifecycle- region yet, and the run exits 1. S7 writes the missing regions and clears it. --- docs/docs/games/04-engine-specification.md | 4 + tools/Read-SpecSet.Tests.ps1 | 10 ++ tools/Read-SpecSet.ps1 | 59 ++++++++++- tools/Test-SpecSet.Tests.ps1 | 111 ++++++++++++++++++++- tools/Test-SpecSet.ps1 | 43 +++++++- 5 files changed, 223 insertions(+), 4 deletions(-) diff --git a/docs/docs/games/04-engine-specification.md b/docs/docs/games/04-engine-specification.md index 61aa29f..93b41d7 100644 --- a/docs/docs/games/04-engine-specification.md +++ b/docs/docs/games/04-engine-specification.md @@ -798,6 +798,7 @@ refuses a week that still has unanswered responses. ### 5.4.1 Opportunity Lifecycle + Revision 2 had `Opportunity` in state, `"opportunity"` in `RewardType`, `generatedOpportunities: string[]` on two outcome types, an `opportunities` entry in `END_WEEK_SYSTEM_ORDER`, and a hidden `weight` field — with **no definition type for @@ -846,9 +847,11 @@ finite and contested. If holding an unexpired offer reserved the slot, the rival never take it and the scarcity model would be decorative. Instead the offer evaporates with a visible message — which is the moment the rival stops being a background simulation and becomes something the player feels. + ### 5.4.2 Scheduled Event Lifecycle + **Creation.** `EventOutcome.scheduledEvents: Array<{ eventId, inWeeks }>` produces a `ScheduledEvent` with `scheduledWeek = currentWeek + inWeeks`, inheriting `chainId` and `chainStep` from the emitting event. Recorded as `event_scheduled`. @@ -881,6 +884,7 @@ which cancels the hearing. Explicit, recorded, and inspectable in history. > a standalone, it fires even after you move out. The fix is to put it in a chain, so > that moving out can end the chain. Stated here so it is a known constraint rather > than a surprise. + ### 5.5 Goal State diff --git a/tools/Read-SpecSet.Tests.ps1 b/tools/Read-SpecSet.Tests.ps1 index 2270cf7..dbe030a 100644 --- a/tools/Read-SpecSet.Tests.ps1 +++ b/tools/Read-SpecSet.Tests.ps1 @@ -18,6 +18,16 @@ Describe 'Read-SpecSetIndex' { ($index.Declarations | Where-Object QualifiedName -eq 'GameMode').Members | Should -Be @('classic', 'open_life', 'challenge') ($index.Declarations | Where-Object QualifiedName -eq 'PlayerState.skills').IsClosed | Should -BeFalse } + It 'S6.1: derives the concept set from GameState''s 17 fields and the declarations reachable from them' { + $index = Read-SpecSetIndex -CorpusPath (Join-Path (Split-Path -Parent $PSScriptRoot) 'docs/docs/games') + ($index.Declarations | Where-Object { $_.QualifiedName -eq 'GameState' -and $_.Owner -eq '' }).Members.Count | Should -Be 17 + $index.Concepts | Should -Contain 'Opportunity' + $index.Concepts | Should -Contain 'ScheduledEvent' + $index.Concepts | Should -Contain 'StatusEffect' + $index.Concepts | Should -Contain 'GoalState' + $index.Concepts | Should -Contain 'PlayerState' + $index.Concepts | Should -Not -Contain 'ResolutionDebugInfo' + } It 'extracts the NeedState and AttributeState mirror obligations from §3.1' { $index = Read-SpecSetIndex -CorpusPath (Join-Path (Split-Path -Parent $PSScriptRoot) 'docs/docs/games') $index.MirrorObligations.Count | Should -Be 2 diff --git a/tools/Read-SpecSet.ps1 b/tools/Read-SpecSet.ps1 index 9ecdcc9..27f87ed 100644 --- a/tools/Read-SpecSet.ps1 +++ b/tools/Read-SpecSet.ps1 @@ -18,6 +18,7 @@ class SpecDeclaration { [string] $DocumentPath [int] $Line [string[]] $Members = @() + [string[]] $TypeRefs = @() } class SpecReference { @@ -59,12 +60,20 @@ class ProvisionalSite { [int] $Line } +class ConceptLifecycle { + [string] $ConceptName + [string] $DocumentPath + [int] $Line + [int] $LabelCount +} + function New-SpecSetIndexFailure { param([string] $Reason, [string] $Path, [int] $Line = 0) [pscustomobject]@{ State = 'NotEvaluated'; Reason = $Reason; Detail = $Path; Line = $Line Documents = @(); Declarations = @(); References = @() MirrorObligations = @(); ProvisionalEntries = @(); ProvisionalSites = @(); Lifecycles = @() + Concepts = @() } } @@ -115,6 +124,13 @@ function Get-FenceDeclarations { $name = $member.Groups[1].Value $d.Members += $name $field = [SpecDeclaration]::new(); $field.QualifiedName = "$($d.QualifiedName).$name"; $field.Owner = $d.QualifiedName; $field.Form = 'Field'; $field.IsClosed = $false; $field.DocumentPath = $DocumentPath; $field.Line = $StartLine + $i + if (-not $member.Value.TrimEnd().EndsWith(')')) { + $colonIndex = $Lines[$i].IndexOf(':') + if ($colonIndex -ge 0) { + $typeText = [regex]::Replace($Lines[$i].Substring($colonIndex + 1), '//.*$', '') + $field.TypeRefs = @([regex]::Matches($typeText, '\b[A-Z][A-Za-z0-9_]*\b') | ForEach-Object { $_.Value } | Select-Object -Unique) + } + } $rows.Add($field) $i++ } @@ -278,6 +294,43 @@ function Get-MirrorObligationFromRegion { $obligation } +function Get-ConceptLifecycleFromRegion { + param([Parameter(Mandatory)][object] $Region) + + if ($Region.Id -notlike 'lifecycle-*') { return $null } + $lifecycle = [ConceptLifecycle]::new() + $lifecycle.ConceptName = $Region.Id.Substring('lifecycle-'.Length) + $lifecycle.DocumentPath = $Region.DocumentPath + $lifecycle.Line = $Region.Line + # A label is a single capitalised word wholly inside its own bold span, ending in a + # period — "**Generation.**", "**Cancellation.**" — never a bolded sentence or phrase + # ("**Why explicit decline exists.**", "**The `opportunities` system**"). This is a + # shape test, not a word list: it does not need to know which word means creation and + # which means retirement, only that the region states two distinct boundaries. + $lifecycle.LabelCount = @([regex]::Matches($Region.Body, '\*\*[A-Z][A-Za-z]*\.\*\*')).Count + $lifecycle +} + +function Get-SpecSetConcepts { + param([Parameter(Mandatory)][AllowEmptyCollection()][object[]] $Declarations) + + $gameState = @($Declarations | Where-Object { $_.QualifiedName -eq 'GameState' -and $_.Owner -eq '' -and $_.Form -eq 'Interface' }) + if ($gameState.Count -eq 0) { return ,@() } + + $topLevelNames = [System.Collections.Generic.HashSet[string]]::new() + foreach ($d in @($Declarations | Where-Object { $_.Owner -eq '' })) { $topLevelNames.Add($d.QualifiedName) | Out-Null } + + $concepts = [System.Collections.Generic.List[string]]::new() + foreach ($fieldName in $gameState[0].Members) { + $field = @($Declarations | Where-Object { $_.QualifiedName -eq "GameState.$fieldName" -and $_.Form -eq 'Field' } | Select-Object -First 1) + if ($field.Count -eq 0) { continue } + foreach ($ref in $field[0].TypeRefs) { + if ($topLevelNames.Contains($ref) -and -not $concepts.Contains($ref)) { $concepts.Add($ref) } + } + } + ,@($concepts) +} + function Read-SpecSetIndex { [CmdletBinding()] param([Parameter(Mandatory)][string] $CorpusPath) @@ -290,6 +343,7 @@ function Read-SpecSetIndex { $provisionalEntries = [System.Collections.Generic.List[object]]::new() $provisionalSites = [System.Collections.Generic.List[object]]::new() $registerRegions = [System.Collections.Generic.List[object]]::new() + $lifecycles = [System.Collections.Generic.List[object]]::new() $references = [System.Collections.Generic.List[object]]::new() foreach ($file in @(Get-ChildItem -LiteralPath $root -File -Filter '*.md' | Sort-Object Name)) { try { $text = [System.IO.File]::ReadAllText($file.FullName, [System.Text.UTF8Encoding]::new($false)) } catch { return New-SpecSetIndexFailure -Reason 'UnreadableDocument' -Path $file.FullName } @@ -307,6 +361,8 @@ function Read-SpecSetIndex { $site = Get-ProvisionalSiteFromRegion -Region $region if ($site) { $provisionalSites.Add($site) } if ($region.Id -eq 'provisional-register') { $registerRegions.Add($region) } + $lifecycle = Get-ConceptLifecycleFromRegion -Region $region + if ($lifecycle) { $lifecycles.Add($lifecycle) } } $lines = $text -replace "`r`n", "`n" -split "`n"; $inFence = $false; $fence = @(); $start = 0 for ($i = 0; $i -lt $lines.Count; $i++) { @@ -346,5 +402,6 @@ function Read-SpecSetIndex { $declarations.Add($aliasField) } } - [pscustomobject]@{ State = 'Indexed'; Reason = $null; Detail = ''; Line = 0; Documents = @($documents); Declarations = @($declarations); References = @($references); MirrorObligations = @($mirrorObligations); ProvisionalEntries = @($provisionalEntries); ProvisionalSites = @($provisionalSites); Lifecycles = @() } + $concepts = Get-SpecSetConcepts -Declarations $declarations + [pscustomobject]@{ State = 'Indexed'; Reason = $null; Detail = ''; Line = 0; Documents = @($documents); Declarations = @($declarations); References = @($references); MirrorObligations = @($mirrorObligations); ProvisionalEntries = @($provisionalEntries); ProvisionalSites = @($provisionalSites); Lifecycles = @($lifecycles); Concepts = @($concepts) } } diff --git a/tools/Test-SpecSet.Tests.ps1 b/tools/Test-SpecSet.Tests.ps1 index 054368c..2c0625a 100644 --- a/tools/Test-SpecSet.Tests.ps1 +++ b/tools/Test-SpecSet.Tests.ps1 @@ -136,7 +136,19 @@ Describe 'S4.6: held, failed, unchecked and unresolvable sum to the index totals Describe 'S4.7: a clean run still names the unresolvable count' { It 'includes the unresolvable count in the report even when State is Valid, and never implies those subjects were checked' { - $result = & (Join-Path $PSScriptRoot 'Test-SpecSet.ps1') -Quiet + # A fixture rather than the real corpus: the real corpus carries concept findings + # (S6 lands with that gate red on purpose, cleared by S7), which would make this + # State-independent report assertion depend on unrelated, still-open work. + $corpus = Join-Path $TestDrive 'unresolvable-valid'; New-Item -ItemType Directory -Path $corpus | Out-Null + $table = @' +| Area | Call made | Reason | Settles when | +|---|---|---|---| +| Test area | A call | A reason | A condition | +'@ + $content = "# Fixture`n`n`n$table`n`n`nSite: x`n`nSee ``engine/01-vision.md`` § 1 @ ``bc74a62a2a0a57c5fd82f337712868b6877bbc6a``." + Set-Content -LiteralPath (Join-Path $corpus '01-fixture.md') -Value $content -NoNewline + + $result = & (Join-Path $PSScriptRoot 'Test-SpecSet.ps1') -CorpusPath $corpus -Quiet $result.State | Should -Be 'Valid' $result.Counts.Unresolvable | Should -BeGreaterThan 0 $line = Write-SpecSetReport -Result $result @@ -227,6 +239,103 @@ Describe 'S5.4: a provisional-site region with no matching register row is a fin } } +Describe 'S6: the concept lifecycle check' { + BeforeAll { + $script:Index = Read-SpecSetIndex -CorpusPath (Join-Path (Split-Path -Parent $PSScriptRoot) 'docs/docs/games') + } + + It 'S6.2: §5.4.1 and §5.4.2 are wrapped as lifecycle-Opportunity and lifecycle-ScheduledEvent, and both are held' { + $findings = Get-ConceptFindings -Index $script:Index + $findings | Where-Object Subject -In @('Opportunity', 'ScheduledEvent') | Should -BeNullOrEmpty + } + + It 'S6.3: every derived concept with no lifecycle- region produces exactly one finding naming it, and the run exits 1' { + $findings = Get-ConceptFindings -Index $script:Index + $missing = @($script:Index.Concepts | Where-Object { $_ -notin @('Opportunity', 'ScheduledEvent') }) + $missing.Count | Should -BeGreaterThan 0 + foreach ($concept in $missing) { + @($findings | Where-Object { $_.CheckId -eq 'concept' -and $_.Subject -eq $concept }).Count | Should -Be 1 + } + @($findings | Where-Object CheckId -eq 'concept').Count | Should -Be $missing.Count + + $result = & (Join-Path $PSScriptRoot 'Test-SpecSet.ps1') -Quiet + $result.State | Should -Be 'Invalid' + (Get-SpecSetExitCode -State $result.State) | Should -Be 1 + } +} + +Describe 'S6.4: deleting the Resolution half of §5.4.1 produces a creation-without-retirement finding' { + BeforeAll { + $script:FixtureRoot = Join-Path $TestDrive 'games-lifecycle' + Copy-Item -Recurse -Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'docs/docs/games') -Destination $script:FixtureRoot + $script:EnginePath = Join-Path $script:FixtureRoot '04-engine-specification.md' + $script:OriginalText = Get-Content -LiteralPath $script:EnginePath -Raw + } + + It 'is held while §5.4.1 states both Generation and Resolution' { + $index = Read-SpecSetIndex -CorpusPath $script:FixtureRoot + (Get-ConceptFindings -Index $index) | Where-Object Subject -eq 'Opportunity' | Should -BeNullOrEmpty + } + + It 'raises exactly one concept finding for Opportunity once the Resolution paragraph is removed' { + $lines = $script:OriginalText -split "`n" + $startIdx = [Array]::FindIndex([string[]]$lines, [Predicate[string]] { param($l) $l.StartsWith('**Resolution.**') }) + $endIdx = [Array]::FindIndex([string[]]$lines, [Predicate[string]] { param($l) $l.StartsWith('**The `opportunities` system**') }) + $startIdx | Should -BeGreaterThan 0 + $endIdx | Should -BeGreaterThan $startIdx + $mutatedLines = $lines[0..($startIdx - 1)] + $lines[$endIdx..($lines.Count - 1)] + $mutated = $mutatedLines -join "`n" + $mutated | Should -Not -Be $script:OriginalText + Set-Content -LiteralPath $script:EnginePath -Value $mutated -NoNewline + + $index = Read-SpecSetIndex -CorpusPath $script:FixtureRoot + $findings = Get-ConceptFindings -Index $index + $opportunityFindings = @($findings | Where-Object Subject -eq 'Opportunity') + $opportunityFindings.Count | Should -Be 1 + $opportunityFindings[0].Detail | Should -Match 'only one boundary' + } + + It 'returns to clean once Resolution is restored' { + Set-Content -LiteralPath $script:EnginePath -Value $script:OriginalText -NoNewline + $index = Read-SpecSetIndex -CorpusPath $script:FixtureRoot + (Get-ConceptFindings -Index $index) | Where-Object Subject -eq 'Opportunity' | Should -BeNullOrEmpty + } +} + +Describe 'S6.5: a lifecycle- region naming something outside the derived concept set is a finding' { + It 'raises exactly one finding naming the out-of-set concept, rather than silently ignoring the region' { + $corpus = Join-Path $TestDrive 'lifecycle-outside-set'; New-Item -ItemType Directory -Path $corpus | Out-Null + $content = @' +# Fixture + +```typescript +interface GameState { + player: PlayerState; +} +type PlayerState = ActorState; +interface ActorState { + name: string; +} +``` + + +**Creation.** Made at game start. + +**Retirement.** Never retired. + +'@ + Set-Content -LiteralPath (Join-Path $corpus '01-fixture.md') -Value $content -NoNewline + + $index = Read-SpecSetIndex -CorpusPath $corpus + $index.State | Should -Be 'Indexed' + $index.Concepts | Should -Not -Contain 'ActorState' + $findings = Get-ConceptFindings -Index $index + $outsideFindings = @($findings | Where-Object Subject -eq 'ActorState') + $outsideFindings.Count | Should -Be 1 + $outsideFindings[0].Detail | Should -Match 'outside the derived concept set' + } +} + Describe 'S3.1/S3.5: the mirror check holds against the real corpus as it stands' { It 'raises no mirror findings anywhere in the real corpus, including §3.5 skills' { $index = Read-SpecSetIndex -CorpusPath (Join-Path (Split-Path -Parent $PSScriptRoot) 'docs/docs/games') diff --git a/tools/Test-SpecSet.ps1 b/tools/Test-SpecSet.ps1 index f65aded..e6392bf 100644 --- a/tools/Test-SpecSet.ps1 +++ b/tools/Test-SpecSet.ps1 @@ -86,6 +86,41 @@ function Get-ProvisionalFindings { } return ,@($findings) } +function Get-ConceptFindings { + param([Parameter(Mandatory)][object] $Index) + + $findings = [System.Collections.Generic.List[object]]::new() + $lifecycleByName = @{} + foreach ($lifecycle in $Index.Lifecycles) { + if (-not $lifecycleByName.ContainsKey($lifecycle.ConceptName)) { $lifecycleByName[$lifecycle.ConceptName] = $lifecycle } + } + + foreach ($concept in $Index.Concepts) { + if (-not $lifecycleByName.ContainsKey($concept)) { + $f = [SpecFinding]::new() + $f.CheckId = 'concept'; $f.Subject = $concept; $f.DocumentPath = ''; $f.Line = 0 + $f.Detail = "$concept is a state-bearing concept with no lifecycle-$concept region." + $findings.Add($f) + continue + } + $lifecycle = $lifecycleByName[$concept] + if ($lifecycle.LabelCount -lt 2) { + $f = [SpecFinding]::new() + $f.CheckId = 'concept'; $f.Subject = $concept; $f.DocumentPath = $lifecycle.DocumentPath; $f.Line = $lifecycle.Line + $f.Detail = "lifecycle-$concept in $($lifecycle.DocumentPath) states only one boundary of the lifecycle; it must state both what creates $concept and what retires it." + $findings.Add($f) + } + } + foreach ($lifecycle in $Index.Lifecycles) { + if ($lifecycle.ConceptName -notin $Index.Concepts) { + $f = [SpecFinding]::new() + $f.CheckId = 'concept'; $f.Subject = $lifecycle.ConceptName; $f.DocumentPath = $lifecycle.DocumentPath; $f.Line = $lifecycle.Line + $f.Detail = "lifecycle-$($lifecycle.ConceptName) in $($lifecycle.DocumentPath) names a concept outside the derived concept set." + $findings.Add($f) + } + } + return ,@($findings) +} function Get-ReferenceResolutions { param([Parameter(Mandatory)][object] $Index) @@ -157,6 +192,10 @@ function Get-SpecSetBucketCounts { $provisionalFailed = @($Index.ProvisionalEntries | Where-Object { $_.Area -in $provisionalFailedAreas }).Count $provisionalTotal = $Index.ProvisionalEntries.Count + $conceptFindingSubjects = @($Findings | Where-Object CheckId -eq 'concept' | ForEach-Object Subject) + $conceptFailed = @($Index.Concepts | Where-Object { $_ -in $conceptFindingSubjects }).Count + $conceptTotal = $Index.Concepts.Count + $referenceHeld = @($ReferenceResolutions | Where-Object Status -eq 'Held').Count $referenceFailed = @($ReferenceResolutions | Where-Object { $_.Status -eq 'Failed' -or ($_.Status -eq 'Unresolvable' -and $_.Finding) }).Count $referenceUnresolvable = @($ReferenceResolutions | Where-Object Status -eq 'Unresolvable').Count @@ -165,7 +204,7 @@ function Get-SpecSetBucketCounts { [pscustomobject]@{ MirrorObligations = [pscustomobject]@{ Held = $mirrorTotal - $mirrorFailed; Failed = $mirrorFailed; Unchecked = 0; Unresolvable = 0; Total = $mirrorTotal } ProvisionalEntries = [pscustomobject]@{ Held = $provisionalTotal - $provisionalFailed; Failed = $provisionalFailed; Unchecked = 0; Unresolvable = 0; Total = $provisionalTotal } - Concepts = [pscustomobject]@{ Held = 0; Failed = 0; Unchecked = 0; Unresolvable = 0; Total = 0 } + Concepts = [pscustomobject]@{ Held = $conceptTotal - $conceptFailed; Failed = $conceptFailed; Unchecked = 0; Unresolvable = 0; Total = $conceptTotal } References = [pscustomobject]@{ Held = $referenceHeld; Failed = $referenceFailed; Unchecked = 0; Unresolvable = $referenceUnresolvable; Total = $referenceTotal } } } @@ -180,7 +219,7 @@ function Invoke-SpecSetCheck { } } - $findings = @((Get-MirrorFindings -Index $Index) + (Get-ProvisionalFindings -Index $Index) + (Get-ReferenceFindings -Index $Index)) + $findings = @((Get-MirrorFindings -Index $Index) + (Get-ProvisionalFindings -Index $Index) + (Get-ConceptFindings -Index $Index) + (Get-ReferenceFindings -Index $Index)) $unresolvable = Get-ReferenceUnresolvable -Index $Index $resolutions = Get-ReferenceResolutions -Index $Index $buckets = Get-SpecSetBucketCounts -Index $Index -Findings $findings -ReferenceResolutions $resolutions From a3d67c8f44e2fe56f08af0f0dbceac04d545a2c8 Mon Sep 17 00:00:00 2001 From: Ben Richards Date: Mon, 24 Aug 2026 19:16:07 +0300 Subject: [PATCH 2/2] =?UTF-8?q?Record=20verify=20report=20for=20S6=20?= =?UTF-8?q?=E2=80=94=205=20of=207=20gates=20pass;=20spec-set=20concept=20g?= =?UTF-8?q?ate=20red=20on=20purpose=20(S7=20clears=20it),=20S18.6=20pre-ex?= =?UTF-8?q?isting=20failure=20tracked=20as=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/verify-report.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude/verify-report.json b/.claude/verify-report.json index 8e222f1..0bcd9c8 100644 --- a/.claude/verify-report.json +++ b/.claude/verify-report.json @@ -3,12 +3,12 @@ { "name": "Parse-check PowerShell scripts", "status": "Passed", - "detail": "Parsed every tools/*.ps1 with [System.Management.Automation.Language.Parser]::ParseFile — all scripts parse cleanly, no syntax errors." + "detail": "Parsed every *.ps1 with [System.Management.Automation.Language.Parser]::ParseFile — 33 files checked, all parsed cleanly, no syntax errors." }, { "name": "Run Pester tests", "status": "Failed", - "detail": "Invoke-Pester -Path tools -Output Detailed -PassThru: Tests Passed: 323, Failed: 1, Skipped: 0, Total: 324. The one failure — Test-DesignState.Tests.ps1 'S18.6: EnforcementUnevidenced rejects this repository's own superseded decision once its SupersededBy line is removed, and clears once it is restored' — asserts Expected 1, but got 0 at $strippedResult.ExitCode. Confirmed pre-existing and unrelated to this branch: it reproduces identically on main at commit cbc5a60 with none of this branch's changes checked out. Cause: design/FROZEN.md now exists (added at 407bcea, before this slice started) and the freeze downgrades every blocking divergence class — including EnforcementUnevidenced — to reported rather than blocking (design/20-contract.md § The freeze), so ExitCode is 0 instead of the 1 this test still expects. The test was written before the freeze existed and was never updated for it." + "detail": "Invoke-Pester -Path tools -Output Detailed -PassThru: Tests Passed: 330, Failed: 1, Skipped: 0, Total: 331, Duration 00:03:22.96. The one failure — Test-DesignState.Tests.ps1 'S18.6: EnforcementUnevidenced rejects this repository's own superseded decision once its SupersededBy line is removed, and clears once it is restored' — asserts 'Expected 1, but got 0.' This is the same pre-existing failure recorded in the S4 verify report (commit 9e25508, 'Record verify report for S4 — 6 of 7 gates pass; S18.6 pre-existing failure tracked as #38'): design/FROZEN.md downgrades every blocking divergence class, including EnforcementUnevidenced, to reported rather than blocking (design/20-contract.md § The freeze), so ExitCode is 0 instead of the 1 this test still expects. The test predates the freeze and was never updated for it. Tracked as issue #38 and unrelated to this branch's changes." }, { "name": "Validate the core/companion split", @@ -18,17 +18,17 @@ { "name": "Check the design state against the tree", "status": "Passed", - "detail": "State: Valid. Findings: 0. CouldNotEvaluate: 0. Freeze active: 0 blocking finding(s) downgraded to reported. Largest closure: unit/script/test-specset, 7258 bytes (ceiling 16384). Exit code: 0." + "detail": "State: Valid. Findings: 0. Reported: 12, all class MirrorStale/WorkStateDivergence on already-merged work items (work/8 through work/36), downgraded to reported by the active design freeze — none blocking. CouldNotEvaluate: 0. Largest closure: unit/script/test-specset, 7258 bytes (ceiling 16384). Exit code: 0." }, { "name": "Check the spec set", - "status": "Passed", - "detail": "Spec-set: Valid; 8 documents; 936 declarations; 2 mirror obligations checked; 8 references unresolvable (cross-repository, not checked). Findings: 0. Unchecked: 0. Commit: 3f214beb175da1a0ebd8469fe3316bf9133ac855. WorkingTree: Clean. Exit code: 0." + "status": "Failed", + "detail": "Spec-set: Invalid; 8 documents; 936 declarations; 2 mirror obligations checked; 8 references unresolvable (cross-repository, not checked, expected — SubZeroDev.GameEngine references). Findings: 12, all CheckId 'concept' — RngState, GameStatus, CalendarState, PlayerState, EconomyState, WorldState, StatusEffect, PendingEventResponse, GoalState, HistoryEntry, LoggedAction, GameMetadata are each a state-bearing concept with no lifecycle- region. This is the intended, on-purpose result of this slice (S6): S6 adds the 'concept' check itself and wraps only 2 of the 14 derived concepts (Opportunity, ScheduledEvent) with lifecycle regions; S7 clears the remaining 12. Commit: 8aca602497650bd9c295f7c250941006a7bc1fb1. WorkingTree: Dirty (untracked .claude/session-costs.tsv, unrelated to this branch's tracked changes). Exit code: 1." }, { "name": "docs.ps1 -BuildOnly (Docusaurus image build)", "status": "DidNotRun", - "reason": "Docker Desktop is not running locally (`docker info` failed) — this repository's docs build needs Docker and is not CI-gated (no `# verification: true` flag on any docs.ps1-related step in .github/workflows/verify.yml), so it was skipped rather than reported as passed." + "reason": "Docker Desktop is not running locally (`docker info` failed) — this repository's docs build needs Docker and is not CI-gated (no `# verification: true` flag on any docs.ps1-related step in .github/workflows/*.yml), so it was skipped rather than reported as passed." }, { "name": "git diff --check",