Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
df2ea0f
docs: author AGENTS.md/CLAUDE.md, rewrite README
CoderGamester Jul 28, 2026
4eb8a8c
docs: add Tests/ agent guide with shared ADMIT/RCR conventions
CoderGamester Aug 2, 2026
8144a0e
test: RCR-backfill 18 of 89 statechart tests (validation, transition,…
CoderGamester Aug 2, 2026
bf08195
test: RCR-backfill 7 ChoiceState tests
CoderGamester Aug 2, 2026
995de23
test: RCR-backfill 14 Wait/TaskWait tests
CoderGamester Aug 2, 2026
186b595
docs: add UNFALSIFIABLE exemption and per-class verdicts to RCR (shar…
CoderGamester Aug 2, 2026
4faca33
test: strengthen the TaskWait D2 overclaim, reclassify the event-miss…
CoderGamester Aug 2, 2026
620f21c
docs: record the first trustworthy coverage baseline (runtime 84.1%)
CoderGamester Aug 2, 2026
11c92d8
test: RCR-backfill the 8 StatechartLeave tests
CoderGamester Aug 2, 2026
46d32dc
test: RCR-backfill the 17 StatechartNest tests
CoderGamester Aug 2, 2026
7d07d3e
style: prefix StatechartWaitTest's activity field with underscore
CoderGamester Aug 2, 2026
e919e9d
docs: add the A6 ENVIRONMENT admission criterion
CoderGamester Aug 2, 2026
a2d1827
test: RCR-backfill the final 25 statechart tests - package complete a…
CoderGamester Aug 2, 2026
b985954
docs: add the SHARED-PATH verdict class to the shared section 2
CoderGamester Aug 3, 2026
593e8db
docs: record the verified coverage baseline and the OPEN rows the aud…
CoderGamester Aug 3, 2026
51d3b20
docs: add CLOSED as a §13 state and require re-derivation to close a row
CoderGamester Aug 4, 2026
5523434
docs: align XML doc comments with AGENTS.md §6.6
CoderGamester Aug 4, 2026
d0a6b8c
docs+test: Smoke RCR exemption, the three unannotated states, and the…
CoderGamester Aug 4, 2026
cffed19
docs: document the remaining public surface, per AGENTS.md §6.6
CoderGamester Aug 4, 2026
62557dc
docs: normalise two doc-comment indent characters in Statechart.cs
CoderGamester Aug 4, 2026
f201a2a
docs: refine Statechart 0.9.5 release changelog
CoderGamester Aug 4, 2026
acfcba1
ci: add release-preflight check for develop->master PRs
CoderGamester Aug 4, 2026
b93a357
ci: add release-preflight check for develop->master PRs
CoderGamester Aug 4, 2026
b525a5b
fix: restore CRLF line endings in CHANGELOG.md
CoderGamester Aug 4, 2026
7bcc397
build: exclude .github/ from the published tarball
CoderGamester Aug 4, 2026
28ec448
ci: add release-preflight check for develop->master PRs
CoderGamester Aug 4, 2026
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
84 changes: 84 additions & 0 deletions .github/workflows/release-preflight.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Release preflight — required status check for develop -> master PRs.
#
# Installed into each package repo by:
# release.py install-preflight <package>
#
# WHY THIS EXISTS
# These are solo repos: GitHub refuses to let a PR author review their own PR
# (HTTP 422 "Review cannot be requested from pull request author"), so a human
# review gate is unavailable. This gives a real blocking gate instead — the same
# package-local gates the release driver enforces, run before the merge rather
# than after it.
#
# THIS FILE MUST STAY OUT OF THE PUBLISHED TARBALL
# `.github/` IS packed by default — the published googlesheetimporter 0.7.2 asset
# still contains `package/.github/workflows/openai.yml`. Each package therefore
# lists `.github/` in its `.gitignore`, which Unity's packer uses as its
# pack-ignore list; git keeps tracking the file regardless, since .gitignore does
# not untrack existing paths. Measured on a real clone: 434 -> 433 entries,
# `.github` 1 -> 0, `Runtime` unchanged.
#
# Do NOT verify this on a copy with `.git` removed: the packer behaves differently
# without a repo and reports `.github` as excluded when it is not. `G24` also
# surfaces it as an unexpected added file if the ignore line is ever dropped.
#
# WHAT IT DOES NOT CHECK
# Only gates decidable from the package directory plus the base ref, so the check
# needs no token, no submodules and no network: G7 (bare SemVer), G8/G9 (CHANGELOG
# heading matches package.json and is newest+highest), G10 (date sane), G11
# (version advances past master), G15 (the PR touches both files). Remote-state
# gates (G0-G6, G12-G14) and the whole tarball chain (G20-G27) run locally in
# `release.py preflight` / `pack` before the PR is opened.

name: release-preflight

on:
pull_request:
branches: [master]

permissions:
contents: read

concurrency:
group: release-preflight-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
preflight:
runs-on: ubuntu-latest
steps:
# `lfs: true` is load-bearing, not a nicety. The default (false) leaves every
# LFS-tracked file as a ~130-byte pointer stub, which would make G28 fail on
# every PR. With it on, G28 becomes a genuine check that the LFS objects are
# FETCHABLE from the remote — the exact failure that hit uiservice, where the
# working tree held stubs while the published 1.2.1 had real content.
- name: Check out the package
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
persist-credentials: false

# The gate logic is shared rather than vendored into six repos, so there is
# one source of truth. `ref` is explicit: actions/checkout defaults to the
# target repo's DEFAULT branch (master), where the tooling does not exist
# yet — omitting it fails with "No such file or directory". Retarget this to
# master once the skill is merged there.
- name: Check out the release tooling
uses: actions/checkout@v4
with:
repository: CoderGamester/Frameworks
ref: develop
path: .release-tooling
sparse-checkout: .claude/skills/unity-package-release/scripts
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Preflight
run: |
python3 .release-tooling/.claude/skills/unity-package-release/scripts/release.py \
preflight-pr --path . --base "origin/${{ github.base_ref }}"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,6 @@ crashlytics-build.properties

# Tests audit history (unity-tests-audit skill -- local developer state, never committed)
.audit-history.md

# CI config: tracked in git, excluded from the published UPM tarball
.github/
90 changes: 90 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# GameLovers.Statechart - AI Agent Guide

> **Companion files**: `CLAUDE.md` wraps this file for Claude Code — edit `AGENTS.md`, not `CLAUDE.md`. `README.md` is the user-facing entry point.

## 1. Package Overview
- **Package**: `com.gamelovers.statechart`
- **Unity**: 2022.3+ (the only package in the family with a floor below 6000.0 — verify before assuming 6000.0-only APIs are safe here)
- **Dependencies** (see `package.json`)
- `com.cysharp.unitask` (2.5.10): `ITaskWaitState.WaitingFor(Func<UniTask>)` overload

Hierarchical State Machine (HFSM / Statechart, per the [UML spec](http://www.omg.org/spec/UML) and [statecharts.github.io](https://statecharts.github.io/what-is-a-statechart.html)) — states can nest, split into parallel regions, and run async waits, all defined once in a constructor setup closure with no further runtime mutation. Smallest package in the family: no `Editor/` assembly, no `Samples~/`, single `Runtime/` assembly.

## 2. Runtime Architecture (high level)

### The chart itself
- **`Statechart`** (`Runtime/Statechart.cs`, implements `IStatechart : IStateMachineDebug`) is the entry point. Construct with `new Statechart(Action<IStateFactory> setup)` — the setup closure runs immediately and defines every state and transition; there is no API to add states after construction. Throws `MissingMemberException` if the setup never calls `factory.Initial(...)`.
- `Run()` — starts/resumes execution from wherever the chart is anchored. No-op if already running.
- `Trigger(IStatechartEvent trigger)` — processes an event with run-to-completion semantics. **No-op if the chart isn't running** (`Pause()`d or never `Run()` called) — a common "my event did nothing" cause.
- `Pause()` — stops processing; `Run()` resumes from the same point.
- `Reset()` — jumps back to the initial state. Does **not** implicitly pause or resume; if the chart was waiting on an event, it needs a fresh `Run()` to continue after reset.
- `LogsEnabled` (from `IStateMachineDebug`) — per-chart debug logging toggle; `IState.LogsEnabled` is the same toggle per-state.
- `CurrentState` (string name) is exposed **only under `#if UNITY_EDITOR`** — do not reference it from a runtime code path that also needs to compile for a player build.
- In `#if UNITY_EDITOR || DEBUG`, every state's `Validate()` runs once at construction — this is where the "missing transition target" / "transition loop" exceptions below come from.
- **`IStateFactory`** (`Runtime/IStateFactory.cs`) is passed into the setup closure — one factory instance per region (top-level chart, and one more per `Nest`/`Split` sub-region). Each factory method takes a `name` string (debug-only identity) and returns the state's own narrow interface:

| Factory method | Returns | Purpose |
|---|---|---|
| `Initial(name)` | `IInitialState` | Entry point of the region. Exactly one per region. |
| `Final(name)` | `IFinalState` | Marks the region complete. |
| `State(name)` | `ISimpleState` | Blocks until an `Event(...)` transition fires. |
| `Transition(name)` | `ITransitionState` | Non-blocking; falls through to its target immediately. |
| `Nest(name)` | `INestState` | Opens one new nested (sequential) region. |
| `Choice(name)` | `IChoiceState` | Non-blocking; picks among `Transition().Condition(...)` branches. |
| `Wait(name)` | `IWaitState` | Blocks on `IWaitActivity` completion (and/or an event). |
| `TaskWait(name)` | `ITaskWaitState` | Blocks on a `Task`/`UniTask`; cannot process events while waiting. |
| `Split(name)` | `ISplitState` | Opens two-or-more new nested **parallel** regions. |
| `Leave(name)` | `ILeaveState` | Like `Final`, but its transition targets a state in an **ancestor** region, jumping out of the current `Nest`/`Split` by exactly one layer. |

### State capability interfaces (`Runtime/IState.cs`)
Every concrete state interface above is composed from these narrower capability interfaces — check which ones a state implements to know what it can do:
- `IStateEnter.OnEnter(Action)` / `IStateExit.OnExit(Action)` — lifecycle callbacks.
- `IStateTransition.Transition()` → `ITransition` — unconditional transition (used by `Initial`/`Transition`/`Leave` states).
- `IStateEvent.Event(IStatechartEvent)` → `ITransition` — event-triggered transition (used by `State`/`Nest`/`Wait`/`Split` states).
- `ITransition.OnTransition(Action)` (chainable) + `.Target(IState)` (terminal — every transition must call this or `Statechart`'s validation throws).
- `ITransitionCondition : ITransition` adds `.Condition(Func<bool>)` — used exclusively by `IChoiceState.Transition()`; a choice with a failing condition does not transition (evaluate carefully — a choice state with no satisfied condition stalls the chart there).

### Nesting and parallelism
- **`INestState.Nest(Action<IStateFactory> | NestedStateData)`** opens a new sequential sub-region; its returned `ITransition` fires once that sub-region reaches its `Final`. The `NestedStateData` overload (`Setup` + `ExecuteExit` + `ExecuteFinal` bools) controls whether the *parent* nest state's own `OnExit` and the *sub-region's* `IFinalState.OnEnter` actually run when leaving via a `Leave` state from inside vs. via normal completion — the plain `Action<IStateFactory>` overload defaults both to `true`.
- **`ISplitState.Split(params Action<IStateFactory>[] | NestedStateData[])`** opens N parallel sub-regions simultaneously; its `ITransition` fires only once **all** sub-regions reach their own `Final`.
- **`ILeaveState`** exits a `Nest`/`Split` region early, targeting a state in the parent region directly — bypasses the nest/split's own completion transition. Can only jump one region layer (an inner `Leave` inside a doubly-nested region still only reaches its immediate parent, not the top level).

### Waiting states
- **`IWaitState.WaitingFor(Action<IWaitActivity>)`** — the action receives an `IWaitActivity` (`Runtime/IWaitActivity.cs`); call `.Complete()` when the awaited work finishes, or `.Split()` first to fan out into multiple sub-activities whose *own* `.Complete()` calls all must return true before the parent completes. A `Wait` state also still processes `Event(...)` transitions (checked after `WaitingFor` since concurrency needs `WaitingFor` resolved first) — the doc comment on `IWaitState` explicitly notes this ordering. If a wait state is the active state when an ancestor `Nest`/`Split` exits, it force-completes itself and all inner activities rather than leaving them dangling.
- **`ITaskWaitState.WaitingFor(Func<Task> | Func<UniTask>)`** — blocks on true async work. Unlike `IWaitState`, **cannot process `Event(...)` transitions while waiting** (no `IStateEvent` in its interface composition) — if an ancestor region exits mid-wait, the exit itself is paused and any events that arrive during the wait are queued rather than dropped, to avoid a concurrency bottleneck.

### Events
- **`IStatechartEvent`** (`Runtime/StatechartEvent.cs`) — equality is by an auto-incrementing `uint Id` assigned at construction, **not** by `Name`. Two `new StatechartEvent("Jump")` instances are never equal to each other; keep one instance per logical event and reuse it across every `.Event(theSameInstance)` call site that should respond to it.

## 3. Key Directories / Files
- **Public interfaces** (root of `Runtime/`): `IState.cs` (all state-capability + concrete-state interfaces + `NestedStateData`), `ITransition.cs`, `IStateFactory.cs`, `IWaitActivity.cs`, `Statechart.cs` (+ `IStatechart`/`IStateMachineDebug`), `StatechartEvent.cs` (+ `IStatechartEvent`).
- **`Runtime/Internal/*`** — concrete state implementations (`InitialState`, `FinalState`, `SimpleState`, `TransitionState`, `NestState`, `SplitState`, `ChoiceState`, `WaitState`, `TaskWaitState`, `LeaveState`), `StateFactory`, `Transition`, `InnerStateData`, `StatechartUtils`. All `internal` — not part of the public surface; consumers only ever see the interfaces above.
- **Tests**: `Tests/Editor/*` (one asmdef, `GameLovers.Statechart.Editor.Tests`) — `StatechartTest.cs` (core lifecycle + validation-exception coverage), `StatechartStateTest.cs`, `StatechartTransitionTest.cs`, `StatechartChoiceTest.cs`, `StatechartNestTest.cs`, `StatechartSplitTest.cs`, `StatechartWaitTest.cs`, `StatechartTaskWaitTest.cs`, `StatechartLeaveTest.cs`, `StatechartNestSplit_IntegrationTest.cs`, `IMockCaler.cs` (mocked-callback interface used across the suite via NSubstitute). Before reading, editing, or creating any file in `Tests/`, you **MUST** read [`Tests/AGENTS.md`](Tests/AGENTS.md) first.
- **No `Editor/` assembly, no `Samples~/`, no `docs/`** — this package's entire surface is the interfaces above; there is no editor tooling and nothing to import as a sample.

## 4. Important Behaviors / Gotchas
- **Setup-time validation, not always-on**: the exceptions below (`MissingMemberException`, `InvalidOperationException`) only fire from the `#if UNITY_EDITOR || DEBUG` validation pass in the `Statechart` constructor — a malformed chart in a release build without `DEBUG` defined will not be caught the same way. Always validate in-editor / in tests before shipping a chart's setup.
- **`Trigger` is a no-op unless running**: calling `Trigger(...)` before the first `Run()`, or after `Pause()`, silently does nothing — it does not queue the event for later.
- **Event identity is per-instance, not per-name**: see `IStatechartEvent` above — a fresh `new StatechartEvent("X")` never equals a previously created `"X"` event. Store event instances as fields/constants, not locals recreated per call.
- **`ITaskWaitState` cannot receive events while waiting**; `IWaitState` can, but only after its `WaitingFor` activities resolve first in a concurrent scenario. Picking the wrong one of these two for a state that needs to react to an event mid-wait is a common design mistake.
- **`ILeaveState` only jumps one region layer** — from inside a doubly-nested region, a `Leave` still only reaches the immediate parent, not further up.
- **`CurrentState` is Editor/DEBUG-only surface** on `Statechart` — don't wire game logic to it.

## 5. Coding Standards (Unity 6 / C# 9.0)
- **C#**: C# 9.0 syntax; explicit namespaces (`GameLovers.StatechartMachine` for Runtime, `GameLoversEditor.StatechartMachine.Tests` for Tests); no global usings.
- **Assemblies**: `Runtime/GameLovers.Statechart.asmdef` has no Editor/UnityEditor reference — this package has no Editor assembly at all. Keep everything under `Runtime/Internal/` truly `internal`; the public surface is exactly the interfaces in section 2/3.
- **Async**: `Cysharp.Threading.Tasks` (UniTask) only appears in `ITaskWaitState.WaitingFor(Func<UniTask>)` — the plain-`Task` overload exists for consumers who don't want the UniTask dependency in their own state-setup code (the package dependency itself is unconditional either way).

## 6. External Package Sources (for API lookups)
- UniTask: `Library/PackageCache/com.cysharp.unitask/`

## 7. Common change workflows
- **Add a new state type**: define its public capability-composed interface in `Runtime/IState.cs`, add the corresponding `internal` implementation under `Runtime/Internal/`, and add the factory method to `IStateFactory` + `Runtime/Internal/StateFactory.cs`. Add test coverage under `Tests/Editor/` following the existing `Statechart<Type>Test.cs` naming.
- **Change validation behavior**: the per-state `Validate()` calls happen in `Statechart`'s constructor under `#if UNITY_EDITOR || DEBUG` — keep new validation failures as exceptions thrown from state `Validate()` implementations under `Runtime/Internal/`, matching the existing `MissingMemberException`/`InvalidOperationException` pattern.

## 8. Update Policy
Update this file when:
- Public API changes (`IStatechart`, any state-capability interface in `IState.cs`, `IStateFactory`, `ITransition`, `IWaitActivity`, `IStatechartEvent`)
- Validation/exception behavior changes in the `Statechart` constructor's setup pass
- Nesting/splitting/leaving semantics change (region completion rules, `NestedStateData` flag behavior)
- Dependencies in `package.json` change (cross-check this file and `README.md` for stale references)
7 changes: 7 additions & 0 deletions AGENTS.md.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this package will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [0.9.5] - 2026-08-04

**Changed**:
- Replaced the Unity Package Starter Kit README with accurate Statechart usage and API guidance based on the package's actual runtime surface.
- Improved public API documentation and automated coverage of existing state creation, transition, nested/split, waiting, and validation behavior; this release does not change runtime behavior.

## [0.9.4] - 2026-06-26

**Fixed**
Expand Down
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Claude Code Guide — Statechart

This package's contributor/agent guide lives in `AGENTS.md`.
Claude Code will automatically import it below.

@AGENTS.md

## Claude-Specific Notes

- Treat `AGENTS.md` as the source of truth.
- If anything in this file appears to conflict with `AGENTS.md`, prefer `AGENTS.md`.
- For user-facing usage, see `README.md`.
7 changes: 7 additions & 0 deletions CLAUDE.md.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading