Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .claude/verify-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 classincluding EnforcementUnevidencedto 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",
Expand All @@ -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-<Concept> 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",
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/games/04-engine-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,7 @@ refuses a week that still has unanswered responses.

### 5.4.1 Opportunity Lifecycle

<!-- lifecycle-Opportunity:declared:start -->
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
Expand Down Expand Up @@ -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.
<!-- lifecycle-Opportunity:declared:end -->

### 5.4.2 Scheduled Event Lifecycle

<!-- lifecycle-ScheduledEvent:declared:start -->
**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`.
Expand Down Expand Up @@ -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.
<!-- lifecycle-ScheduledEvent:declared:end -->

### 5.5 Goal State

Expand Down
10 changes: 10 additions & 0 deletions tools/Read-SpecSet.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 58 additions & 1 deletion tools/Read-SpecSet.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class SpecDeclaration {
[string] $DocumentPath
[int] $Line
[string[]] $Members = @()
[string[]] $TypeRefs = @()
}

class SpecReference {
Expand Down Expand Up @@ -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 = @()
}
}

Expand Down Expand Up @@ -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++
}
Expand Down Expand Up @@ -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)
Expand All @@ -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 }
Expand All @@ -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++) {
Expand Down Expand Up @@ -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) }
}
Loading
Loading