Audit follow-up: P0/P1/P2 fixes, dynamic cost & actions, CI, DocC#2
Merged
Conversation
Three correctness/forward-compat fixes from the port audit:
B1. BooleanWorldState.init(bits:mask:) now sanitizes inputs with `bits & mask`
to maintain the invariant that no bit is set outside the mask. Previously
a raw construction like `(bits: 0xFF, mask: 0x0F)` left phantom bits that
could falsely satisfy conditions on those bit positions.
B2. RichWorldState.applying(.add:) / .applying(.subtract:) now trap with a
descriptive preconditionFailure when the target fact is missing or
non-numeric, instead of silently overwriting it with the delta. Misuse
(e.g. .add on a .bool fact) was a common modeling-bug source masked by
the old fallback.
A1. All public types now declare Sendable conformance: WorldState protocol
(with Sendable Conditions/Effects), GOAPAction protocol, BasicAction,
GOAPGoal, GOAPPlanner, BooleanWorldState, RichWorldState, StateValue,
StateCondition, StateEffect, and conditional Sendable on PriorityQueue.
Builds clean under -strict-concurrency=complete.
Tests: +4 (B1 invariant + phantom-bit satisfaction; B2 positive set-then-add
flow; new SendableConformanceTests file as a compile-time conformance gate).
44/44 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four improvements from the second pass of the port audit:
F5. GOAPPlanner now keeps a closed set with reopen-on-better-g. Stale frontier
entries are discarded on dequeue instead of re-expanded against a stale
gScore; if successor relaxation later finds a cheaper path to a closed
state, it is removed from the closed set so the search can revisit it.
Net: fewer expansions on typical scenarios, no correctness regression
against the inadmissible-heuristic edge cases.
A2. Removed the misleading `fullyDefined:` parameter from
`BooleanWorldState.facts(_:)`. Closed-world semantics now require an
explicit `mask = .max` after construction, which is honest about the
"treat every unspecified bit as false" assumption.
A3. Added RawRepresentable overloads on BooleanWorldState (facts, set, clear,
get) for any `RawValue == Int` enum. Callers can drop magic indices in
favour of `enum Fact: Int { case hasGun, gunLoaded, enemyDead }` — see
the new README quick-start and `testEnumFactOverloads`.
F1. New `GOAPPlan<Action>` value type returned by the planner. Carries the
action sequence, the full trajectory of intermediate states (length =
actions.count + 1), a computed totalCost, and isEmpty/count helpers.
Replaces the raw `[Action]?` return, including in the multi-goal tuple.
Pre-1.0 API break; tests and README updated.
Tests: 44 → 47 (testPlanCarriesTotalCostAndStates,
testEmptyPlanWhenStartSatisfiesGoal, testEnumFactOverloads). README rewritten
to use enum facts and to show plan.actions / plan.totalCost / plan.states.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four small cleanups from the second-pass audit:
N1. BooleanWorldState.bits/mask are now `public private(set)`. The clamp in
init(bits:mask:) was previously bypassable by direct field assignment
(`s.bits = .max`), reintroducing phantom-bit satisfaction. All mutation
now goes through `init`, `set`, or `clear`, all of which preserve the
invariant `bits & ~mask == 0`.
N2. SendableConformanceTests now includes GOAPPlan<BasicAction<Boolean>> and
GOAPPlan<BasicAction<Rich>>. Previous test missed the new value type.
N3. testEnumFactOverloads extended to cover `clear<F: RawRepresentable>`.
N4. README's multi-goal example used Fact.hasGun for an "eatFood" goal, which
was semantically confusing. Replaced with a dedicated `enum Survival`
that makes the priority comparison read naturally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three feature additions covering P2 audit items F2, F3-lite, and F6:
F2. Dynamic cost via `cost(in: State)`. Default implementation in the
GOAPAction protocol extension returns the static `cost`, so existing
actions need no change. Override `cost(in:)` for distance-dependent or
state-modulated weights. The planner now uses the contextual cost on
every relaxation; GOAPPlan.totalCost evaluates `cost(in:)` along the
trajectory so it stays accurate for dynamic-cost actions.
F3-lite. New `actionsFor: (State) -> [Action]` planner overload generates
the action set per-state. Enables permutation-style "go to room X" /
"attack target Y" actions without precomputing the full Cartesian
product. The closure runs on every expansion — keep it cheap or cache
internally. The existing `actions: [Action]` API becomes a thin wrapper
that emits the same array for every state.
F6. New GoalSelectionStrategy enum (.priority | .maxUtility) added to the
multi-goal `plan(from:goals:actions:selectingBy:)` overload. Default
stays .priority (first-achievable-by-priority). .maxUtility computes
plans for every goal and returns the one maximising
`priority - plan.totalCost` — mountain-goap style, lets a cheap
low-priority goal beat an expensive high-priority one.
README updated with feature blurbs for each. Tests in next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T2. PropertyBasedTests runs 300 random GOAP problems with a deterministic
SplitMix64 RNG (seed 0xC0FFEE_BEEF_F00D for reproducible CI failures).
For each problem whose plan succeeded, it checks four invariants:
1. plan.states.count == plan.actions.count + 1
2. plan.states.first == start and plan.states.last satisfies the goal
3. each step's preconditions hold and the recorded next state matches
states[i].applying(actions[i].effects)
4. plan.totalCost equals the sum of cost(in:) along the trajectory
Also smoke-checks that the generator produces a meaningful mix of
planned vs. unplannable problems (>20% planned, >0 nil).
T3. PerformanceTests captures order-of-magnitude latency via XCTMeasure:
- testMediumBooleanPlan: 12-fact, ~10-step chain with distractors
- testWideShallowBooleanPlan: 30 candidate actions, 1-step plan
- testRichWorldStateNumericPlan: heal-until-full numeric scenario
Debug-build baseline on Apple Silicon: ~40-190µs per plan. Not a fail
gate; meant to make regressions visible during review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T6. .github/workflows/ci.yml runs on push/PR to main with two jobs:
- macos-15 with Swift 6: debug build, tests, strict-concurrency
build, release build.
- ubuntu-latest in swift:6.0 container: same matrix minus the perf
suite (XCTMeasure isn't supported on swift-corelibs-xctest, so
the filter excludes PerformanceTests on Linux). Confirms the
cross-platform claim in the README is actually checked.
T5. Sources/SwiftGOAP/SwiftGOAP.docc/SwiftGOAP.md is a minimal DocC
catalog: module overview, quick-start example, and a Topics section
grouping the public symbols (World state, Planning, Supporting).
SPM auto-generates the rest from /// comments. Hosts cleanly on
GitHub Pages or Apple's docs.
README now carries a CI status badge linking to the workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #1 — applies every actionable finding from the two-pass port audit. Pre-1.0 API changes included; the 47→54-test suite stays green and the package builds clean under
-strict-concurrency=complete.What changed
Six commits, grouped by audit priority:
P0 (
0166014) — correctness + Swift 6 readinessBooleanWorldState.init(bits:mask:)sanitizes inputs withbits & maskso phantom bits outside the mask can't fake-satisfy conditions.RichWorldState.applying(.add/.subtract)traps with a descriptivepreconditionFailurewhen the fact is missing or non-numeric, instead of silently overwriting it.Sendable—WorldState+Conditions/Effectsassociated types,GOAPAction,BasicAction,GOAPGoal,GOAPPlanner,BooleanWorldState,RichWorldState,StateValue,StateCondition,StateEffect, conditionalSendableonPriorityQueue.P1 (
1434573) — API hardeningfullyDefined:parameter fromBooleanWorldState.facts(_:).RawRepresentableoverloads onfacts/set/get/clear— defineenum Fact: Int { case hasGun, … }and drop magic indices.GOAPPlan<Action>value type carries actions, intermediate states trajectory, computedtotalCost. Replaces[Action]?return. Pre-1.0 break; tests + README updated.P2 re-audit (
2a7780d)BooleanWorldState.bits/maskarepublic private(set)— invariant can't be bypassed by direct assignment.SendableConformanceTestsnow coversGOAPPlan.clear<F: RawRepresentable>.Fact.hasGunas an "eat" goal).P2 features (
76b2037)GOAPAction.cost(in: State) -> Intwith default = staticcost. Override for distance-dependent or state-modulated cost.GOAPPlan.totalCostwalks the trajectory and uses contextual cost.actionsFor: (State) -> [Action]planner overload generates the action set per-state — enables permutation-style "go to room R" actions without precomputing the Cartesian product.GoalSelectionStrategyenum (.priority|.maxUtility)..maxUtilitypicks the goal maximisingpriority - plan.totalCost, so a cheap low-priority goal can beat an expensive high-priority one.P2 tests (
c27ac80)PropertyBasedTests— 300 random GOAP problems, deterministic SplitMix64 RNG (seed0xC0FFEE_BEEF_F00D). Checks trajectory invariants, precondition replay, total-cost consistency.PerformanceTests— XCTMeasure baselines for medium/wide/numeric scenarios. Debug build: ~40–190µs per plan on Apple Silicon. Not a fail gate, just regression visibility.Infrastructure (
39880de).github/workflows/ci.yml— macOS-15 + Linux (swift:6.0) matrix, debug/release/strict-concurrency builds + tests. Perf suite filtered out on Linux (XCTMeasure not in swift-corelibs-xctest).SwiftGOAP.docccatalog with module overview, quick-start, and Topics grouping.Numbers
Reviewer notes
cost(in:)default impl usesextension GOAPAction { public func cost(in state: State) -> Int { cost } }— adding acost(in:)to your action type just overrides this naturally; no protocol break for existing conformers.GOAPPlanchange ripples through every test and the README. If you'd rather keep[Action]?as the primary return, the wrapper is trivial — buttotalCost/statesmake plan validation noticeably easier.Test plan
swift test— 54 / 54 passingswift build -Xswiftc -strict-concurrency=complete— cleanswift build -c release— cleanxcrun docc preview(manual)🤖 Generated with Claude Code