Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .agents/agents/spec-drift-investigator.md
Original file line number Diff line number Diff line change
@@ -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/<Provider>/**`
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
- <class>::<field> — <change> — action: <update model | refresh baseline>

### 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.
43 changes: 43 additions & 0 deletions .agents/agents/wallet-payload-reviewer.md
Original file line number Diff line number Diff line change
@@ -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/<Provider>/**`. 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
- <file>:<line> — <problem> — <why JSON breaks>

### Risks
- <file>:<line> — <concern>

### Verified
- <paths>
```

No diffs in scope → one line, stop.
41 changes: 41 additions & 0 deletions .agents/scripts/guard-protected-paths.sh
Original file line number Diff line number Diff line change
@@ -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
62 changes: 62 additions & 0 deletions .agents/skills/refresh-wallet-spec.md
Original file line number Diff line number Diff line change
@@ -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/<Provider>/**`, `src/Builder/<Provider>/**`, `tests/Builder/<Provider>/**`. `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`.
1 change: 1 addition & 0 deletions .claude/agents/spec-drift-investigator.md
1 change: 1 addition & 0 deletions .claude/agents/wallet-payload-reviewer.md
26 changes: 26 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions .claude/skills/refresh-wallet-spec/SKILL.md
1 change: 1 addition & 0 deletions .cursor/commands/refresh-wallet-spec.md
1 change: 1 addition & 0 deletions .cursor/commands/spec-drift-investigator.md
1 change: 1 addition & 0 deletions .cursor/commands/wallet-payload-reviewer.md
11 changes: 11 additions & 0 deletions .cursor/rules/protected-files.mdc
Original file line number Diff line number Diff line change
@@ -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.
117 changes: 58 additions & 59 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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()`
Loading