diff --git a/.agents/agents/spec-drift-investigator.md b/.agents/agents/spec-drift-investigator.md new file mode 100644 index 0000000..9f4bfe2 --- /dev/null +++ b/.agents/agents/spec-drift-investigator.md @@ -0,0 +1,33 @@ +--- +name: spec-drift-investigator +description: Investigate drift between PHP wallet models (src/Pass/**) and upstream Apple/Google/Samsung specs. Read-only — reports, never edits. +tools: Bash, Read, Grep, Glob +--- + +Read-only. Never edit `tools/spec/**` or `src/Pass/**`. Output is a report a human acts on. + +## Steps + +1. Run in parallel: `castor spec:check:{google,apple,samsung}`. +2. For each failure: + - Google: `castor spec:diff:google --properties` + - Apple/Samsung: diff `tools/spec/*-keyset.json` vs `src/Pass//**` +3. Group by provider. Per drifted field: class path, change kind (added/removed/type/enum), action (update model | refresh baseline). +4. All pass → say so, stop. + +## Output + +``` +## Spec drift report + +### Google +- :: — action: + +### Apple / Samsung +- … + +### Suggested commands +castor spec:baseline:google # only if ALL google drift is intentional +``` + +No preamble. No command explanations. No speculation beyond the diff. diff --git a/.agents/agents/wallet-payload-reviewer.md b/.agents/agents/wallet-payload-reviewer.md new file mode 100644 index 0000000..5ce413c --- /dev/null +++ b/.agents/agents/wallet-payload-reviewer.md @@ -0,0 +1,43 @@ +--- +name: wallet-payload-reviewer +description: Review src/Builder/** and src/Pass/** changes for serialized-JSON correctness. Catches regressions PHPStan/PHPUnit miss. +tools: Bash, Read, Grep, Glob +--- + +Core contract of the lib = correct serialized JSON per provider. Check: + +1. **Diff.** `git diff [--stat] origin/main...HEAD -- src/Builder src/Pass tests/Builder` (fall back to `git diff`). +2. **Builder ↔ Pass parity.** Every new/renamed setter has a matching Pass DTO property with correct nullability + serializer attrs (`#[SerializedName]`, `#[Context]`, `#[Ignore]`). +3. **Normalizers.** Non-trivial shapes (enums, colors, dates, money) → verify `src/Common/**` context still emits the expected key casing + value format. +4. **Fixtures.** New builder paths need a JSON-pinning assertion in `tests/Builder//**`. Flag missing ones. +5. **Provider gotchas.** + - Apple: `pass.json` case-sensitive — casing regressions break `.pkpass`. + - Google: unknown fields silently dropped; match `tools/spec/google-wallet-baseline.json`. + - Samsung: `tools/spec/samsung-wallet-keyset.json` is source of truth. + +## Run + +```bash +vendor/bin/phpunit tests/Builder +castor qa:phpstan +castor qa:cs:check +``` + +Report failures verbatim. Do not fix. + +## Output + +``` +## Payload review + +### Blockers +- : + +### Risks +- : + +### Verified +- +``` + +No diffs in scope → one line, stop. diff --git a/.agents/scripts/guard-protected-paths.sh b/.agents/scripts/guard-protected-paths.sh new file mode 100755 index 0000000..567958a --- /dev/null +++ b/.agents/scripts/guard-protected-paths.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Blocks edits to generated spec baselines and local credential files. +# +# Protocol: reads a JSON tool-call payload on stdin with at least +# { "tool_name": "...", "tool_input": { "file_path": "..." } } +# On a protected path: prints reason to stderr, exits 2 (hook block). +# Otherwise: exits 0. +# +# Wired into Claude Code via .claude/settings.json (PreToolUse). +# Cursor users: this script follows the same stdin/exit-code contract +# and can be reused from a Cursor hook when available. See +# .cursor/rules/protected-files.mdc for the soft-block fallback. + +set -euo pipefail + +payload="$(cat)" + +tool=$(printf '%s' "$payload" | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') +path=$(printf '%s' "$payload" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') + +case "$tool" in + Edit|Write|MultiEdit|NotebookEdit) ;; + *) exit 0 ;; +esac + +[ -z "$path" ] && exit 0 + +case "$path" in + */tools/spec/*.json|tools/spec/*.json) + echo "BLOCKED: $path is a generated spec baseline." >&2 + echo "Refresh it with 'castor spec:baseline:google|apple|samsung' instead of hand-editing." >&2 + exit 2 + ;; + */.env.local|*.env.local|.env.local) + echo "BLOCKED: $path holds local credentials and must not be edited by the agent." >&2 + echo "Ask the human operator to update it manually." >&2 + exit 2 + ;; +esac + +exit 0 diff --git a/.agents/skills/refresh-wallet-spec.md b/.agents/skills/refresh-wallet-spec.md new file mode 100644 index 0000000..5387bc7 --- /dev/null +++ b/.agents/skills/refresh-wallet-spec.md @@ -0,0 +1,62 @@ +--- +name: refresh-wallet-spec +description: Reconcile PHP wallet models with upstream spec changes. Detect → diff → decide → patch → baseline → verify. +disable-model-invocation: true +--- + +Do not skip or reorder. Order prevents silently re-baselining a real regression. + +## 1. Detect + +```bash +castor spec:check:google +castor spec:check:apple +castor spec:check:samsung +``` + +All pass → stop. + +## 2. Diff (failing providers only) + +```bash +castor spec:diff:google --properties +# Apple: tools/spec/apple-pass-keyset.json vs src/Pass/Apple/** +# Samsung: tools/spec/samsung-wallet-keyset.json vs src/Pass/Samsung/** +``` + +Large diff → dispatch `spec-drift-investigator`. + +## 3. Decide per field + +| Situation | Action | +|---|---| +| Upstream added, we want it | Patch DTO + Builder + fixture → re-baseline | +| Upstream removed/renamed, we use it | Patch models + callers → re-baseline | +| Upstream enum/type change | Patch models → re-baseline | +| Baseline stale, models correct | Re-baseline only | +| Drift looks unintentional | Stop. Escalate. Do NOT re-baseline. | + +**Never** re-baseline before patching — it hides the drift. + +## 4. Patch + +Edit `src/Pass//**`, `src/Builder//**`, `tests/Builder//**`. `tools/spec/*.json` are hook-blocked by design. + +## 5. Re-baseline + +Only after step 4, only for intentional drift: + +```bash +castor spec:baseline:{google|apple|samsung} +``` + +## 6. Verify + +```bash +vendor/bin/phpunit +castor qa:phpstan +castor qa:cs:check +castor spec:check:{google,apple,samsung} +``` + +All green → commit model changes, fixtures, and refreshed baseline **together** so provenance shows in `git log`. diff --git a/.claude/agents/spec-drift-investigator.md b/.claude/agents/spec-drift-investigator.md new file mode 120000 index 0000000..46faf30 --- /dev/null +++ b/.claude/agents/spec-drift-investigator.md @@ -0,0 +1 @@ +../../.agents/agents/spec-drift-investigator.md \ No newline at end of file diff --git a/.claude/agents/wallet-payload-reviewer.md b/.claude/agents/wallet-payload-reviewer.md new file mode 120000 index 0000000..cb2c83c --- /dev/null +++ b/.claude/agents/wallet-payload-reviewer.md @@ -0,0 +1 @@ +../../.agents/agents/wallet-payload-reviewer.md \ No newline at end of file diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..8379a38 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,26 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:*)", + "Bash(wc -l *)", + "Bash(castor tests*)", + "Bash(castor qa:phpstan:*)", + "Bash(castor qa:cs:check:*)", + "Bash(castor qa:cs:fix:*)" + ] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write|MultiEdit|NotebookEdit", + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR/.agents/scripts/guard-protected-paths.sh\"" + } + ] + } + ] + }, + "prefersReducedMotion": false +} diff --git a/.claude/skills/refresh-wallet-spec/SKILL.md b/.claude/skills/refresh-wallet-spec/SKILL.md new file mode 120000 index 0000000..cb79957 --- /dev/null +++ b/.claude/skills/refresh-wallet-spec/SKILL.md @@ -0,0 +1 @@ +../../../.agents/skills/refresh-wallet-spec.md \ No newline at end of file diff --git a/.cursor/commands/refresh-wallet-spec.md b/.cursor/commands/refresh-wallet-spec.md new file mode 120000 index 0000000..2480aa4 --- /dev/null +++ b/.cursor/commands/refresh-wallet-spec.md @@ -0,0 +1 @@ +../../.agents/skills/refresh-wallet-spec.md \ No newline at end of file diff --git a/.cursor/commands/spec-drift-investigator.md b/.cursor/commands/spec-drift-investigator.md new file mode 120000 index 0000000..46faf30 --- /dev/null +++ b/.cursor/commands/spec-drift-investigator.md @@ -0,0 +1 @@ +../../.agents/agents/spec-drift-investigator.md \ No newline at end of file diff --git a/.cursor/commands/wallet-payload-reviewer.md b/.cursor/commands/wallet-payload-reviewer.md new file mode 120000 index 0000000..cb2c83c --- /dev/null +++ b/.cursor/commands/wallet-payload-reviewer.md @@ -0,0 +1 @@ +../../.agents/agents/wallet-payload-reviewer.md \ No newline at end of file diff --git a/.cursor/rules/protected-files.mdc b/.cursor/rules/protected-files.mdc new file mode 100644 index 0000000..7eeadc3 --- /dev/null +++ b/.cursor/rules/protected-files.mdc @@ -0,0 +1,11 @@ +--- +description: Hard rule — do not edit generated spec baselines or local credential files. +alwaysApply: true +--- + +# Protected files + +Refuse edits to these paths. Explain why, then stop. + +- **`tools/spec/*.json`** — generated baselines (Apple/Google/Samsung). Refresh via `castor spec:baseline:{google|apple|samsung}`, only after confirming intentional drift (see `refresh-wallet-spec`). Hand-edits hide real drift. +- **`**/.env.local`** — local credentials for `examples/`. Human-only. If new fields are needed, update the committed `.env` template and tell the user what to fill in. diff --git a/AGENTS.md b/AGENTS.md index cb06298..41ee5ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,88 +1,87 @@ # AGENTS.md -This file provides guidance to IA agents when working with code in this repository. +Guidance for AI agents in this repo. -## Project Overview +## Project -wallet-kit is a PHP 8.3+ library that provides a fluent builder API for modeling Apple Wallet, Google Wallet, and Samsung Wallet JSON payloads. It focuses on payload normalization using Symfony Serializer — it does **not** handle signing, bundling (.pkpass), or API calls. +wallet-kit — PHP 8.3+ library. Fluent builder for Apple/Google/Samsung Wallet JSON payloads via Symfony Serializer. No signing, no `.pkpass` bundling, no API calls. ## Commands ```bash -# Run tests (PHPUnit 12) -vendor/bin/phpunit - -# Run a single test -vendor/bin/phpunit --filter=TestClassName -vendor/bin/phpunit --filter=TestClassName::testMethodName - -# Static analysis (PHPStan level 5) -castor qa:phpstan - -# Code style check / fix -castor qa:cs:check -castor qa:cs:fix - -# API spec drift detection -castor spec:check:google # Google Wallet discovery doc revision -castor spec:check:apple # Apple pass.json phpstan shapes -castor spec:check:samsung # Samsung model keyset -castor spec:baseline:google # Refresh Google baseline -castor spec:baseline:apple # Refresh Apple keyset -castor spec:baseline:samsung # Refresh Samsung keyset -castor spec:diff:google # Diff live discovery enums against PHP models -castor spec:diff:google --properties # Include schema property comparison +vendor/bin/phpunit # tests (PHPUnit 12) +vendor/bin/phpunit --filter=Foo[::test] # single test + +castor qa:phpstan # PHPStan level 5 +castor qa:cs:check | qa:cs:fix # PHP-CS-Fixer + +castor spec:check:{google|apple|samsung} # drift vs upstream spec +castor spec:diff:google [--properties] # detailed Google diff +castor spec:baseline:{google|apple|samsung} # refresh baseline +``` + +CI: cs-check, spec-check, phpstan, tests (PHP 8.3/8.4/8.5). + +## Agent workflows + +Canonical sources in `.agents/`. `.claude/` and `.cursor/` entries are symlinks — edit the source, both tools see it. + +``` +.agents/ + agents/ # subagent / command prompts + spec-drift-investigator.md — report model/spec drift (read-only) + wallet-payload-reviewer.md — review builder/Pass serialization changes + skills/ # slash-command workflows + refresh-wallet-spec.md — detect → diff → decide → patch → baseline → verify + scripts/ # shared hook scripts + guard-protected-paths.sh ``` -CI runs 4 jobs: cs-check, spec-check, phpstan, and tests (PHP 8.3/8.4/8.5 matrix). +Protected paths: `tools/spec/*.json`, `**/.env.local`. Hard block via `.agents/scripts/guard-protected-paths.sh` (Claude Code PreToolUse hook); rule via `.cursor/rules/protected-files.mdc` (Cursor). ## Architecture -### Builder Pattern (entry point) +### Builder ``` -WalletPass::{vertical}(WalletPlatformContext, ...args) - → ConcreteBuilder (extends AbstractWalletBuilder) - → .with*() / .add*() fluent methods (via CommonWalletBuilderTrait) - → .build() → BuiltWalletPass - → .apple() → Pass - → .google() → GoogleWalletPair (vertical + issuerClass + passObject) +WalletPass::{vertical}(WalletPlatformContext, ...) + → ConcreteBuilder (AbstractWalletBuilder + CommonWalletBuilderTrait) + → .with*() / .add*() → .build() → BuiltWalletPass + → .apple() → Pass + → .google() → GoogleWalletPair (vertical + issuerClass + passObject) → .samsung() → Card ``` -**7 verticals:** Generic, Offer, Loyalty, EventTicket, Flight, Transit, GiftCard — each has its own builder in `src/Builder/{Vertical}/`. +Verticals: Generic, Offer, Loyalty, EventTicket, Flight, Transit, GiftCard — each in `src/Builder/{Vertical}/`. -**WalletPlatformContext** is an immutable container built with `->withApple(...)`, `->withGoogle(...)`, `->withSamsung(...)`. Only configured platforms produce output; accessing unconfigured platforms throws typed exceptions. +`WalletPlatformContext`: immutable, built via `->withApple|withGoogle|withSamsung`. Unconfigured platforms throw typed exceptions. -### Namespace Layout +### Namespaces | Namespace | Purpose | -|-----------|---------| -| `Builder\` | WalletPass entry point, platform contexts, BuiltWalletPass | -| `Builder\Internal\` | CommonWalletState, barcode mappers, helpers | -| `Builder\{Vertical}\` | Vertical-specific builders | -| `Pass\Apple\Model\` | Apple Pass models (Pass, PassStructure, Field, Barcode, enums) | -| `Pass\Apple\Normalizer\` | Symfony Serializer normalizers for Apple | -| `Pass\Android\Model\` | Google Wallet class/object models by vertical | -| `Pass\Android\Normalizer\` | Google normalizers | -| `Pass\Samsung\Model\` | Samsung Card envelope + 8 card type attributes | -| `Pass\Samsung\Normalizer\` | Samsung normalizers | -| `Common\` | Shared value objects (Color) | -| `Exception\` | Typed exceptions implementing WalletKitException | +|---|---| +| `Builder\` | Entry point, platform contexts, BuiltWalletPass | +| `Builder\Internal\` | CommonWalletState, barcode mappers | +| `Builder\{Vertical}\` | Vertical builders | +| `Pass\Apple\{Model,Normalizer}\` | Apple models + normalizers | +| `Pass\Android\{Model,Normalizer}\` | Google class/object models + normalizers | +| `Pass\Samsung\{Model,Normalizer}\` | Samsung Card + 8 card types | +| `Common\` | Shared VOs (Color) | +| `Exception\` | Typed, implement WalletKitException | ### Serialization -All JSON output is produced via Symfony Serializer normalizers (100+ normalizers total). Tests use `BuilderTestSerializerFactory` in `tests/Builder/` which wires up the full normalizer stack. +All JSON via Symfony Serializer normalizers (100+). Tests wire the stack via `BuilderTestSerializerFactory` in `tests/Builder/`. -### Platform Differences +### Platform shapes -- **Apple Wallet:** Single `Pass` object → one `pass.json` -- **Google Wallet:** Class + Object pairs per vertical (e.g., `EventTicketClass` + `EventTicketObject`), wrapped in `GoogleWalletPair` -- **Samsung Wallet:** Unified `Card` envelope with type-specific attributes, 8 card types (7 cross-platform + DigitalId and PayAsYouGo are Samsung-only) +- Apple: one `Pass` → one `pass.json` +- Google: Class + Object per vertical, wrapped in `GoogleWalletPair` +- Samsung: unified `Card` + type attributes; 8 card types (7 cross-platform + DigitalId, PayAsYouGo Samsung-only) -### Key Conventions +### Conventions -- PHPStan level 5 with extensive `@phpstan-type` shape annotations for validation -- Enums used throughout (CardTypeEnum, PassTypeEnum, GoogleVerticalEnum, ReviewStatusEnum, etc.) -- `mutateApple()` and `mutateSamsung()` callbacks allow post-build platform-specific customization -- Color value object supports `rgb()`, `hex()`, and `googleColor()` output formats +- PHPStan level 5 with `@phpstan-type` shapes +- Enums throughout (CardType, PassType, GoogleVertical, ReviewStatus, …) +- `mutateApple()` / `mutateSamsung()` for post-build tweaks +- `Color` outputs `rgb()`, `hex()`, `googleColor()`