Skip to content

Audit follow-up: P0/P1/P2 fixes, dynamic cost & actions, CI, DocC#2

Merged
ultrainfinity merged 6 commits into
mainfrom
claude/magical-albattani-f144d1
May 14, 2026
Merged

Audit follow-up: P0/P1/P2 fixes, dynamic cost & actions, CI, DocC#2
ultrainfinity merged 6 commits into
mainfrom
claude/magical-albattani-f144d1

Conversation

@ultrainfinity
Copy link
Copy Markdown
Owner

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 readiness

  • B1: BooleanWorldState.init(bits:mask:) sanitizes inputs with bits & mask so phantom bits outside the mask can't fake-satisfy conditions.
  • B2: RichWorldState.applying(.add/.subtract) traps with a descriptive preconditionFailure when the fact is missing or non-numeric, instead of silently overwriting it.
  • A1: All public types declare SendableWorldState + Conditions/Effects associated types, GOAPAction, BasicAction, GOAPGoal, GOAPPlanner, BooleanWorldState, RichWorldState, StateValue, StateCondition, StateEffect, conditional Sendable on PriorityQueue.

P1 (1434573) — API hardening

  • F5: A* now keeps a closed set with reopen-on-better-g — stale frontier entries skipped, suboptimal closures revisited when a cheaper path appears.
  • A2: Removed the misleading fullyDefined: parameter from BooleanWorldState.facts(_:).
  • A3: RawRepresentable overloads on facts/set/get/clear — define enum Fact: Int { case hasGun, … } and drop magic indices.
  • F1: New GOAPPlan<Action> value type carries actions, intermediate states trajectory, computed totalCost. Replaces [Action]? return. Pre-1.0 break; tests + README updated.

P2 re-audit (2a7780d)

  • N1: BooleanWorldState.bits/mask are public private(set) — invariant can't be bypassed by direct assignment.
  • N2: SendableConformanceTests now covers GOAPPlan.
  • N3: Test added for clear<F: RawRepresentable>.
  • N4: Cleaned up multi-goal README example (no more Fact.hasGun as an "eat" goal).

P2 features (76b2037)

  • F2: GOAPAction.cost(in: State) -> Int with default = static cost. Override for distance-dependent or state-modulated cost. GOAPPlan.totalCost walks the trajectory and uses contextual cost.
  • F3-lite: New actionsFor: (State) -> [Action] planner overload generates the action set per-state — enables permutation-style "go to room R" actions without precomputing the Cartesian product.
  • F6: GoalSelectionStrategy enum (.priority | .maxUtility). .maxUtility picks the goal maximising priority - plan.totalCost, so a cheap low-priority goal can beat an expensive high-priority one.

P2 tests (c27ac80)

  • T2: PropertyBasedTests — 300 random GOAP problems, deterministic SplitMix64 RNG (seed 0xC0FFEE_BEEF_F00D). Checks trajectory invariants, precondition replay, total-cost consistency.
  • T3: 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)

  • T6: .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).
  • T5: Minimal SwiftGOAP.docc catalog with module overview, quick-start, and Topics grouping.

Numbers

  • Library: ~600 lines across 9 source files.
  • Tests: 54 (40 original → 47 after P1 → 54 after P2). All passing.
  • Strict concurrency: clean.

Reviewer notes

  • The cost(in:) default impl uses extension GOAPAction { public func cost(in state: State) -> Int { cost } } — adding a cost(in:) to your action type just overrides this naturally; no protocol break for existing conformers.
  • F1's GOAPPlan change ripples through every test and the README. If you'd rather keep [Action]? as the primary return, the wrapper is trivial — but totalCost/states make plan validation noticeably easier.
  • Deferred to v0.2: F3-full (mountain-goap-style permutation selectors), F4 (stronger numeric heuristic — needs action-set analysis), T1 (negative-cost trap test — not feasible in XCTest without subprocess tricks).

Test plan

  • swift test — 54 / 54 passing
  • swift build -Xswiftc -strict-concurrency=complete — clean
  • swift build -c release — clean
  • CI workflow runs once this PR is up (first activation)
  • DocC preview via xcrun docc preview (manual)

🤖 Generated with Claude Code

ultrainfinity and others added 6 commits May 7, 2026 10:56
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>
@ultrainfinity ultrainfinity merged commit 960a010 into main May 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant