diff --git a/.github/workflows/image-generator.yml b/.github/workflows/image-generator.yml index cdc2e9fc..7cc4a419 100644 --- a/.github/workflows/image-generator.yml +++ b/.github/workflows/image-generator.yml @@ -1,13 +1,13 @@ name: Image generator # The service is deliberately not a pnpm workspace member (see -# 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 +# 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 -# `image-generator/`. Filtered on this package alone the suite simply does not run, which +# `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. # @@ -17,17 +17,17 @@ on: pull_request: branches: [main] paths: - - 'image-generator/**' + - 'services/image-generator/**' - '.github/workflows/image-generator.yml' - 'contracts/solana/cryptopets/programs/cryptopets/src/state/**' - - 'indexer-go/internal/solana/idl/**' + - 'services/indexer-go/internal/solana/idl/**' push: branches: [main] paths: - - 'image-generator/**' + - 'services/image-generator/**' - '.github/workflows/image-generator.yml' - 'contracts/solana/cryptopets/programs/cryptopets/src/state/**' - - 'indexer-go/internal/solana/idl/**' + - 'services/indexer-go/internal/solana/idl/**' permissions: contents: read @@ -38,7 +38,7 @@ concurrency: defaults: run: - working-directory: image-generator + working-directory: services/image-generator jobs: check: @@ -55,7 +55,7 @@ jobs: node-version: 22 cache: pnpm # Its own lockfile, not the monorepo root's. - cache-dependency-path: image-generator/pnpm-lock.yaml + cache-dependency-path: services/image-generator/pnpm-lock.yaml # --ignore-workspace, or pnpm walks up to the monorepo root and installs # that instead, leaving this package with no node_modules. diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml new file mode 100644 index 00000000..acc7bef7 --- /dev/null +++ b/.github/workflows/mobile.yml @@ -0,0 +1,44 @@ +name: Mobile + +# `mobile` is the one package with a test script that no workflow ran. Root `pnpm lint` +# covers its ESLint, and `pnpm build` skips it entirely (React Native builds through the +# `android`/`ios` scripts), so nothing executed its jest suite. That suite is a single +# boot smoke test, but it is the only mechanical check that App.tsx's imports still +# resolve and its provider tree renders, which is exactly what a rename in `shared` or a +# moved module breaks silently here while every other package stays green. + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: mobile-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # Deliberately not path-filtered on `mobile/**`. The failure this catches comes + # from changing `shared` or a module mobile imports, not from touching mobile + # itself, so a filter would skip precisely the run that matters. See the + # image-generator workflow for what path filtering has already cost here. + - name: Mobile tests + run: pnpm --filter mobile test diff --git a/.github/workflows/parity.yml b/.github/workflows/parity.yml index 16d4a379..1170e026 100644 --- a/.github/workflows/parity.yml +++ b/.github/workflows/parity.yml @@ -7,7 +7,7 @@ name: Combat parity # neither: # # protocol tests/combat/goldenVectors.test.ts (the canonical TS engine) -# indexer-go internal/combat/combat_golden_test.go (the independent Go port) +# services/indexer-go internal/combat/combat_golden_test.go (the independent Go port) # contracts/ethereum test/XpFormula.test.ts (the XP fixture) # # §F's circuit breaker only has value while the TS and Go ports are independent and both @@ -84,19 +84,19 @@ jobs: - uses: actions/setup-go@v5 with: - go-version-file: indexer-go/go.mod - cache-dependency-path: indexer-go/go.sum + go-version-file: services/indexer-go/go.mod + cache-dependency-path: services/indexer-go/go.sum - name: Vet - working-directory: indexer-go + working-directory: services/indexer-go run: go vet ./... # Unit tests only. The Postgres-backed tests are gated on TEST_DATABASE_URL and # truncate tables, so they are deliberately not given one here. - name: Test - working-directory: indexer-go + working-directory: services/indexer-go run: go test ./... - name: Build - working-directory: indexer-go + working-directory: services/indexer-go run: go build -o /dev/null ./cmd/indexer diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml new file mode 100644 index 00000000..510e8c1a --- /dev/null +++ b/.github/workflows/static-checks.yml @@ -0,0 +1,67 @@ +name: Static checks + +# The two whole-repo checks that nothing ran. +# +# Lint: the root `pnpm lint` aggregate ran in no workflow. `protocol` and `verifier` +# are linted by their own workflows, and `image-generator` by its own, but `frontend`, +# `backend`, `shared`, `website`, and `mobile` were linted only by hand. That includes +# frontend's custom CSS-naming check (`lint:css`), which AGENTS.md lists under +# Enforcement. +# +# Build: the only thing that typechecks `frontend`, `backend`, and `website`. None of +# the three has a `typecheck` script — their type check *is* the build (`tsc -b && +# vite build`, `tsc`, `next build`) — and vitest does not typecheck, since esbuild +# strips types without checking them. A type error on a path no test executes passed +# every existing workflow and surfaced at deploy. +# +# One job rather than two: both need the same install, which dominates the runtime. +# `if: always()` on the build step so a lint failure still reports the build result. +# +# Deliberately not path-filtered. These break from a change in `shared` or `protocol` +# landing on a consumer that was not touched, so filtering on the consumer's own paths +# would skip the run that matters. + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: static-checks-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + # 22, not the 20 most workflows use: `pnpm build` starts with the contracts + # compile, and Hardhat 3 needs >= 22.10. It calls `.flatMap` on the iterator + # from `Map.values()`, an Iterator Helpers method absent before Node 22, so + # `compile` dies with a TypeError rather than a version check. Same reason + # parity.yml pins 22. + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # frontend (eslint + CSS naming), backend, protocol, verifier, shared, + # website, mobile. Not contracts/ethereum, which has no lint script. + - name: Lint + run: pnpm lint + + # Root aggregate: contracts compile, then backend, frontend, and website. + # Runs unattended (no secrets), about a minute and a half. + - name: Build + if: always() + run: pnpm build diff --git a/.github/workflows/verifier.yml b/.github/workflows/verifier.yml index 6f465d39..05850668 100644 --- a/.github/workflows/verifier.yml +++ b/.github/workflows/verifier.yml @@ -39,6 +39,14 @@ jobs: if: always() run: pnpm --filter @cryptopets/verifier lint + # Separate from the tests: vitest strips types rather than checking them, so a + # type error in a path the suite does not execute reaches a third party's + # checkout instead of CI. `protocol` is typechecked in the parity workflow for + # the same reason; this package is consumed as raw TS too. + - name: Verifier typecheck + if: always() + run: pnpm --filter @cryptopets/verifier typecheck + # The committed corpus, run through the actual CLI rather than the library, so the # thing a third party would run is the thing CI proves still works. No network # access: the ruleset these battles were fought under is pinned in the checkout. diff --git a/.gitignore b/.gitignore index dee9a142..3006f5d8 100644 --- a/.gitignore +++ b/.gitignore @@ -144,7 +144,7 @@ dist vite.config.js.timestamp-* vite.config.ts.timestamp-* -# Hardhat files (scoped — a bare `cache/` also hid indexer-go/internal/cache/) +# Hardhat files (scoped — a bare `cache/` also hid services/indexer-go/internal/cache/) contracts/**/cache/ contracts/**/artifacts/ typechain-types/ @@ -156,9 +156,6 @@ coverage.json .openzeppelin/ deployments/ -# EVM subgraph generated files (regenerated by pnpm configure) -backend/indexing/evm/subgraph/src/addresses.ts - # Hardhat local network .openzeppelin/unknown-*.json diff --git a/AGENTS.md b/AGENTS.md index f8af8216..0dd0c405 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Root coordination contract for AI and human contributors in this repo. Detailed ## Scope -- Applies to the whole monorepo: `frontend/`, `backend/`, `mobile/`, `website/`, `shared/`, `contracts/ethereum/`, `contracts/solana/`, `indexer-go/`, `proto/`, `image-generator/`. +- Applies to the whole monorepo: `frontend/`, `backend/`, `mobile/`, `website/`, `shared/`, `contracts/ethereum/`, `contracts/solana/`, `services/indexer-go/`, `proto/`, `services/image-generator/`. - No nested `AGENTS.md` files exist yet. If one is added under a package, it may tighten rules for that subtree but must not relax the rules here. Normative language: `MUST`/`MUST NOT` are mandatory. `SHOULD`/`SHOULD NOT` are expected by default; deviations should be explained in the PR. `MAY` is optional. @@ -12,11 +12,11 @@ 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 `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. `indexer-go/internal/combat/xp.go` still covers the formula and the decay but not level-up. +- `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 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 chain-blind, but the low-level chain wiring in `frontend/src/chains/{ethereum,solana}/`, the async battle/breed VRF flows, and the combat simulator remain intentionally separate per chain. 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`, `indexer-go`, `proto`, `protocol`, and `verifier` are MIT; everything else, `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