diff --git a/.github/workflows/image-generator.yml b/.github/workflows/image-generator.yml index 7cc4a419..94f04569 100644 --- a/.github/workflows/image-generator.yml +++ b/.github/workflows/image-generator.yml @@ -4,12 +4,20 @@ name: Image generator # services/image-generator/README.md), so the root `pnpm lint` / `pnpm test` aggregates # and the Coverage workflow do not reach it. Without this workflow nothing runs its # suite at all. -# The path filter must also list the files `src/solanaLayout.test.ts` reads, not just -# this package. That suite pins the PetAccount byte layout against the Anchor IDL and -# `pet.rs`, so a Solana account change breaks it while touching nothing under -# `services/image-generator/`. Filtered on this package alone the suite simply does not run, which -# is how `open_to_challenges` was removed from the program while the decoder kept its -# byte — every field after it misaligned, and a pet rendered as a different pet. +# The path filter must also list the files this package's cross-boundary suites READ, not +# just the package itself. Two of them reach outside it: +# +# src/solanaLayout.test.ts pins the PetAccount byte layout against the Anchor IDL and +# `pet.rs`, so a Solana account change breaks it while touching nothing under +# `services/image-generator/`. Filtered on this package alone the suite simply does not +# run, which is how `open_to_challenges` was removed from the program while the decoder +# kept its byte — every field after it misaligned, and a pet rendered as a different pet. +# +# src/items.test.ts asserts `src/items.ts` still matches the backend item catalog, which +# this package cannot import at build time (it installs `--ignore-workspace`). The drift +# it guards against is caused by editing the *backend* catalog, so without that path +# listed here the guard never runs on the change that trips it — a new item merges with a +# stale copy, green. # # The two lists below are duplicated on purpose: GitHub Actions does not support YAML # anchors, so factoring them out would silently disable the filter rather than share it. @@ -21,6 +29,7 @@ on: - '.github/workflows/image-generator.yml' - 'contracts/solana/cryptopets/programs/cryptopets/src/state/**' - 'services/indexer-go/internal/solana/idl/**' + - 'backend/src/features/inventory/catalog.data.ts' push: branches: [main] paths: @@ -28,6 +37,7 @@ on: - '.github/workflows/image-generator.yml' - 'contracts/solana/cryptopets/programs/cryptopets/src/state/**' - 'services/indexer-go/internal/solana/idl/**' + - 'backend/src/features/inventory/catalog.data.ts' permissions: contents: read diff --git a/.github/workflows/parity.yml b/.github/workflows/parity.yml index 1170e026..24e15eba 100644 --- a/.github/workflows/parity.yml +++ b/.github/workflows/parity.yml @@ -6,9 +6,15 @@ name: Combat parity # `verifier`, and the three suites that actually replay the vectors were covered by # neither: # -# protocol tests/combat/goldenVectors.test.ts (the canonical TS engine) -# services/indexer-go internal/combat/combat_golden_test.go (the independent Go port) -# contracts/ethereum test/XpFormula.test.ts (the XP fixture) +# protocol tests/combat/goldenVectors.test.ts (the canonical TS engine) +# protocol tests/combat/equipmentVectors.test.ts (gear, roadmap §4) +# services/indexer-go internal/combat/combat_golden_test.go (the independent Go port) +# services/indexer-go internal/combat/equipment_golden_test.go (gear, the same file) +# contracts/ethereum test/XpFormula.test.ts (the XP fixture) +# +# `equipment.json` is read by the two live ports only — the frozen Solana port predates +# equipment and never applies it, so it is not a witness to that file. Its first case +# reproduces a `battle.json` row, which is how an ungeared fight is proven unchanged. # # §F's circuit breaker only has value while the TS and Go ports are independent and both # match the vectors. A drift that CI never runs is a circuit breaker nobody armed. diff --git a/AGENTS.md b/AGENTS.md index 0dd0c405..e7d40832 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,8 +12,9 @@ Normative language: `MUST`/`MUST NOT` are mandatory. `SHOULD`/`SHOULD NOT` are e ## Non-Negotiables - `MUST NOT` change Solana's frozen combat port (`game/battle_sim.rs`, `game/xp.rs`). It has no caller left in the program, but its golden-vector tests are what still prove `contracts/test-vectors/{battle,xp}.json` describe what actually settled on that chain. A bug found there is fixed forward in the live ports below, under a new `rulesetVersion`, never by patching the frozen one. **The Solidity port is gone**: `CombatSim.sol` was deleted once it had no on-chain caller, which also removed `battle.json`'s Solidity generator and validator. `battle.json` itself is unchanged and still gates the live ports. -- `MUST` keep the two **live** combat ports in step with each other and with the golden vectors: `protocol/src/combat/` (the canonical engine, re-exported from `shared/src/utils/combat` for existing importers) and `services/indexer-go/internal/combat/` (the independent verifier). Changing one without the other re-breaks the circuit breaker in §F, whose whole value is that the two were written to disagree if either drifts. This covers XP and level progression too (`protocol/src/combat/xp.ts`, validated against `contracts/test-vectors/xp.json`), so an XP or decay change is a both-ports change. `services/indexer-go/internal/combat/xp.go` still covers the formula and the decay but not level-up. -- `MUST NOT` edit `contracts/test-vectors/{battle,xp}.json` to make a failing test pass — this holds more strongly now, not less. The vectors are the only mechanical link left between the frozen ports and the live ones. A live port that fails them has drifted away from the rules real battles were settled under. +- `MUST` keep the two **live** combat ports in step with each other and with the golden vectors: `protocol/src/combat/` (the canonical engine, re-exported from `shared/src/utils/combat` for existing importers) and `services/indexer-go/internal/combat/` (the independent verifier). Changing one without the other re-breaks the circuit breaker in §F, whose whole value is that the two were written to disagree if either drifts. This covers XP and level progression too (`protocol/src/combat/xp.ts`, validated against `contracts/test-vectors/xp.json`), so an XP or decay change is a both-ports change. `services/indexer-go/internal/combat/xp.go` still covers the formula and the decay but not level-up. It also covers **equipment modifiers** (roadmap §4): `protocol/src/combat/equipment.ts` and `services/indexer-go/internal/combat/equipment.go`, both validated against `contracts/test-vectors/equipment.json`. The modifiers apply at one specific point — after `extract`, before the skill multipliers — and moving that point in one port without the other changes every geared fight, so the ordering is pinned by a vector case in both. +- `MUST NOT` edit `contracts/test-vectors/{battle,xp,equipment}.json` to make a failing test pass — this holds more strongly now, not less. `battle.json` and `xp.json` are the only mechanical link left between the frozen ports and the live ones, and a live port that fails them has drifted away from the rules real battles were settled under. `equipment.json` is newer and has no frozen witness, but the same rule applies for the same reason: it is what holds the two live ports to one another. Its first case deliberately reproduces a `battle.json` row, so an ungeared fight is proven unchanged rather than assumed. +- `MUST` treat a `snapshot` or `ruleset` schema-version bump as append-only. An absent version means **1**, never "whatever this build implements": every snapshot and published bundle written before those fields existed is version 1 and has receipts signed over it, so defaulting to the current version silently re-encodes them under a layout they were never hashed under and invalidates every signature. Old versions stay listed in `SUPPORTED_VERSIONS` permanently (`protocol/src/domain/schemaVersions.ts`). - `MUST NOT` assume the `ChainAdapter` interface (`shared/src/hooks/adapters/`) covers more than pet-action mutations and reads. It is a real, shared interface (`useEvmAdapter`/`useSolanaAdapter` both implement it) and every public pet-action hook consumes it for the mutation, but the low-level chain wiring in `frontend/src/chains/{ethereum,solana}/`, the async breed/mint randomness flows, and the combat simulator remain intentionally separate per chain. `useCreatePet` and `useBreedPets` are only chain-blind on the action: both carry the EVM settle lifecycle inline behind `isEvm` guards. See CLAUDE.md's cross-chain interfaces section for the exact boundary. - `MUST` match the license of the package being edited when adding new files: `contracts/ethereum`, `contracts/solana`, `services/indexer-go`, `proto`, `protocol`, and `verifier` are MIT; everything else, `services/image-generator` included, is PolyForm Noncommercial 1.0.0 (root `LICENSE`). See the table in `README.md`. `protocol` is MIT on purpose (third parties have to be able to replay signed battle receipts), so it `MUST NOT` import from a PolyForm package; a test in that package enforces it. `verifier` is MIT for the same reason and depends on nothing but `protocol`. - `MUST NOT` assume the root `pnpm lint` / `pnpm test` cover `image-generator`, and `MUST NOT` verify it with `pnpm --filter image-generator